1 · The tier table
Write your rules on one page, sorted into three tiers by what a violation costs. Hard reject, misattribution, human escalation. Nothing else — no code yet.
A deterministic linter decides what reaches the theme. No model sits in its decision path.
Everyone building with a model right now is building the generator. This class is about the other half — the part that reads what the model produced and refuses most of it. It is taken from a working headless orchestrator that writes Shopify copy and Liquid for a live store, whose own documentation opens with the sentence “the linter is the asset; the generator is replaceable.” Eleven control sets. Thirty-seven checks before one line ships. Four file paths it will not write under any circumstances. And a register, written by its author against himself, of seventeen rules with no gate behind them at all — one of which came true on this very site, one week after it was written, twice.
October 2026 note. For current agent practice, class 100 runs Claude Code headless, class 102 writes the test before the prompt, and class 103 reads its permission rules. Published Sep 22, 2026. Nothing on this page was re-run for this note.
Put the model on the generating side of a wall and put deterministic code on the deciding side. A language model proposes copy and Liquid; a linter with no model in it evaluates every proposal against rules sorted by what a violation costs, and only output that survives reaches a theme. Sort the rules into three tiers — a claim that is false, a claim that is true but misattributed, and a claim that is merely clumsy — and give only the third tier a warning. Gate everything in the first two. Then prove each check works by breaking the code it guards and confirming it goes red, because a check that has never failed is a check you have never tested.
Here is the architecture, and the interesting thing about it is how little of it is the model.
A local inference server runs a code-specialised model for Liquid and a small embedding model for retrieval. Product records are indexed into a vector store so that when the system writes about an item, the specification comes from the record rather than from the model's memory of what that kind of item usually is. That is the entire generation stack, it runs on one machine, and the system's own documentation describes it as the secondary concern.
“[The system] treats generation as a secondary concern to verification. While probabilistic models synthesize text and Liquid code, deterministic code governs publication.”
Read that twice, because it inverts the way nearly everyone is currently building. The usual shape is: get a better model, write a better prompt, add a retry, maybe have a second model review the first one. Every one of those moves spends effort on the probabilistic half. This system spends its effort on the half that can be proven, and treats the model the way you would treat a contractor — useful, replaceable, and not allowed near the keys.
You do not need eleven control sets to get the benefit. The benefit arrives the moment the decision to publish stops being made by the same kind of system that wrote the thing. Two components, one wall:
The wall is the product. Once it exists, a model upgrade is a Tuesday afternoon rather than a re-audit, because nothing about your safety depends on which model produced the text.
The gate is thirty-seven deterministic checks across seven surfaces where a claim can appear. The design decision worth stealing is not the number. It is that the rules are tiered by consequence, and only the cheapest tier is allowed to pass with a warning.
Notice what tiering buys you: it makes “warn” a legitimate outcome again. In an untiered checker every rule eventually gets downgraded to a warning, because one over-strict rule blocking a good batch at midnight is all it takes. Then the warnings pile up and nobody reads them. Tiering stops that slide by deciding, in advance and in writing, which rules are allowed to be advisory.
Certifications are used here as a worked example of tiering and nothing else. No certificate name, scheme or number appears on this page, and none of the tier descriptions above should be read as a claim about any business. Use your own tier 1 list. Write it down before you write the linter.
Below is the same decision the gate makes, with the certificate names replaced by placeholders. Each candidate sentence runs through the tiers in order and stops at the first one that catches it. Work out where each stops before you reveal it — the interesting ones are the sentences that are entirely true.
"Made with [STANDARD-A] certified organic cotton." — the business does not hold [STANDARD-A].
"Sewn in a facility belonging to a [SCHEME-B] member." — true; the membership is the supplier's.
"This shirt is [SCHEME-B] certified." — the supplier is a member; the shirt is not certified.
"Printed on [STANDARD-C] certified stock." — held, but the blank in question is a different composition.
"[MARK-D] approved — see logo below." with the mark rendered.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
The case that publishes is the point of the exercise. A gate that rejected it would be a gate that suppresses true, carefully-phrased sentences — and the system's own specification says so in as many words: “do not over-block. A linter that rejects all certification language suppresses true statements.” Over-blocking feels safe and is not free. It trains everyone around it to turn the gate off.
This is the defect that recurs more than any other, in every linter anyone has ever written, and it survives review because the broken version works on the example you tested it with.
Three instances from the handover, each shipped, each found later:
| The rule as written | The rule as implemented | What got through, or didn't |
|---|---|---|
| No certification may be claimed without a gate. | A search for one certification's name. | Every other certification in the language. The check passes and means nothing. |
| No consumer of this value may exist outside the allowed list. | A search for the word render. |
Inclusion, assignment, capture, echo — every other way a template reads a value. |
| These two words must not be used together as a phrase. | Proximity: both words within n characters. | Nothing got through. 518 defects were reported that were not there, because proximity is not grammar. |
The third row is the one people underestimate. An over-fitted rule is quietly useless; a mis-modelled rule is loudly useless, and 518 false positives in one run will get the whole linter switched off by lunchtime. Both failures come from the same mistake — implementing the nearest thing you can express instead of the thing the rule says.
“Where a catch and a true sentence conflicted, the true sentence won.”
You need a stated policy for what happens when a rule and a legitimate sentence disagree, and you need it written down before the argument, because in the moment the person who wants to ship will win it. Deciding in advance that the true sentence wins means every conflict becomes a bug report against the rule instead of a negotiation about the content.
Each one is a real check from a real linter, described by what it does rather than what it was meant to do. Decide whether the implementation actually expresses the rule.
The rule bans claiming any certification the business does not hold. The check searches for the literal name of the one certification that caused the incident.
The rule requires a declared source for every numeric claim. The check finds each number in the document and asserts a source attribute within the same element.
The rule forbids attaching a supplier's standard to the product. The check tests whether the standard's holder recorded for that item is the item itself.
The rule forbids a two-word phrase. The check flags any occurrence of both words within forty characters of each other.
The rule says no template outside an allowed list may consume this value. The check greps for the word used by the most common consuming tag.
The rule says a percentage may only be published alongside the population it was measured over. The check requires an adjacent element carrying a sample size, and fails the document if it is absent.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
Read every check and write, in one sentence, the set of things it matches. Then read the rule it implements and write the set the rule names. If the two sentences are not the same sentence, you have found one. This is tedious and it is the highest-yield hour you will spend on a linter, because a rule and its implementation drift apart silently and no test catches it — the test was written from the implementation.
Two hundred and seventy-five controls across eleven sets, one command to run them, and an instruction in the handover that everything else depends on: if it is not green, nothing below is trustworthy. That part is ordinary. This part is not:
“No control is trusted until the source has been mutated to make it go red while its guard-pair stays green — a step that twice this month caught a control passing with the rule it tested switched off.”
Twice. In one month. In a suite built by someone who takes this more seriously than almost anyone, and who wrote the register of his own ungated rules by hand. A control that passes with its subject disabled is not a weak control — it is a zero control that reports one, and it is indistinguishable from a real one until you break something on purpose.
The mechanics are small. For each control: comment out the rule it exercises, run the suite, and assert that this control fails and its neighbours do not. If it stays green, it was never testing what its name says. If the neighbours also go red, your controls are entangled and a single rule change will produce a wall of noise you will learn to ignore.
because fieldEvery control in the suite carries the verbatim input that made it fail, stored next to the assertion. Not a description of the failing input — the input. This is a small habit with a large payoff:
That last point is the design lesson. When a discipline depends on somebody remembering to do a step, make the artifact impossible to finish without it.
Three rounds. Each shows a set of controls after a mutation run — the rule under test has been switched off, so exactly one control in each round should have gone red and one is reporting something it cannot know.
Which control should have failed and did not?
Three of these read live state; one reads a fixture that was captured before the mutation.
One control passes for the wrong reason.
Look for the one whose assertion cannot distinguish the two outcomes.
Two controls went green. Only one of them is honest.
One control compares the output against something downstream of the thing being tested.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
The system has a similarity metric. It measures how much language a batch of generated descriptions shares — overlapping eight-word sequences across the corpus — and it was being read as a quality bar: below this number, good; above it, repetitive.
Then somebody checked what the number does when nothing changes except how many documents you feed it.
The metric more than doubled because the corpus grew. That is not a defect in the metric — more documents means more chances for any given phrase to recur, and the measurement is doing exactly what it says. The defect was in what it was being used for. A number that moves with the size of the thing you measure cannot be a threshold, because the threshold is then secretly a statement about corpus size.
“A collapse score is not comparable across corpus sizes. Say the N with the number, always.”
The fix is a good demonstration of the difference between the two kinds of measurement, and it split one number into two jobs:
Both halves are now honest, and neither could have been made honest while they were the same number. If you have a metric on a dashboard that people treat as a pass mark, ask what it does when your input volume doubles. If the answer is “it goes up”, you do not have a pass mark. You have a thermometer somebody has drawn a line on.
An instrument tells you something changed. A gate decides whether something ships. An instrument may be noisy, relative and trend-only. A gate must be absolute, reproducible and defensible at three in the morning to somebody who disagrees with it. Dashboards are full of instruments being asked to do a gate's job, and it is always discovered the same way — the number moves and nobody can say what it means.
This is the module that will change how you work tomorrow, and it is backed by measurements rather than opinion.
The system had a repetition problem: generated descriptions were converging on the same phrasing. The obvious response is prompt engineering — tell the model to vary its language, give it a list of phrases to avoid, add a style instruction. Here is what was actually tried, and what each move was worth.
Removing the existing copy from the brief
measured delta -52.2 % -79.7%
Making the graphic the required subject of the description
measured delta -17.7 % -49.3%
Fixing one over-used phrase across every live instance of it
measured delta -0.3 % -0.5%
Pinning the corpus size before comparing two scores
measured delta -286 documents -89.9%
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
A list of prohibited outputs does not constrain a generator's output space. Only a constraint on the input does. Telling a model not to produce something is a preference expressed inside the generated text. Removing the source material, or changing what the task is about, changes the distribution it samples from.
The third row is the expensive lesson. Hundreds of individual corrections, carefully applied to live records, moved the measurement by three-tenths of a percentage point. All that effort went into the output, where the leverage is not.
This is not an argument that prompts do nothing. The system's own register is more precise than that, and the precision is the useful part:
“Prompt guidance moves a model where it has no prior and does not move it where it has a strong one. Two defects were fixed first try by adding guidance; a third survived 11 attempts across a skill section and a reject list. So for these rules the only defence is the mechanism we have proof sometimes fails.”
The practical test costs you one run: write the guidance, run it, and if the defect is still there, stop rewording and write a check. Two attempts is information. Eleven is a decision you have already made and are declining to notice.
The most unusual document in this repository is a list its author wrote against himself. Thirty-three routed rules; seventeen of them have no deterministic check behind them and are defended only by instructions in a prompt. It names each one, and it says plainly what that defence is worth.
And then it draws the line in exactly the right place:
“None of these is a claim rule. Every certification and brand-fact rule is gate-backed by the linter, which is the one place this project cannot afford a gap. What is exposed here is craft.”
Ungated where a mistake is ugly. Gated where a mistake is a lie. That is the same tiering from module 01, applied one level up — to the question of which rules get a checker at all. You will never gate everything. Deciding out loud which gaps you are accepting, and writing the list down, is what separates a known risk from a surprise.
The seventeenth ungated rule in that document is about CSS. It says
position: sticky fails in several ways — an overflow
declaration on an ancestor, a transform, a static ancestor, a missing offset
— and that when it fails, the sticky element simply stops sticking
and the page still looks fine in review.
The contents rail you are using to navigate this page shipped broken on the two classes before this one. In the narrow layout it was declared static, so on a phone it scrolled off the top and never came back. Both pages passed a full assembler audit, two checksum comparisons and a registration. Every gate was green. The rule had been written down a week earlier, in the same estate, by the person whose page it was — and there was no checker behind it, so it caught nothing. The same register marks another rule with the note “shipped live twice”.
A rule written down with no checker behind it is a prediction of a future defect. That is the whole class in one sentence, and this is the evidence for it. The assembler that builds this page now carries a gate that refuses to write the file if the rail is declared static at any width. It is fourteen lines long. It would have cost ten minutes a week earlier.
The cheapest check is the one that runs before the model does. This system keeps
a short list of things it will not do at all, and the list has no override
argument — not a flag, not an environment variable, not a
--force. That absence is the design.
“A backup re-taken after a write is a mirror, not a history.” If your restore point is refreshed from live on a schedule, then the moment a bad batch lands, the next refresh quietly destroys the only copy of what came before. One contamination in this system had to be recovered from a full census dump taken for an unrelated reason, because the backup had already become a mirror of the damage.
Write the refusal list before you write the generator. It is the only part of the design that gets harder to add later, because by then somebody will have a deadline and a good reason.
The corollary the system states about its own architecture. If the check lives in a helper that each caller is supposed to invoke, then it is a convention, and conventions are one hurried pull request from being optional. Put the refusal where the operation physically cannot proceed without passing it.
None of these is in anybody's documentation in a form that would have saved the time. Each cost a halted run, a wrong verdict, or a day.
Tooling recognises a leading #graphql tag inside a template literal
and switches on syntax highlighting and validation for it. It is genuinely
useful. It is also a comment, in GraphQL, to the end of its line — so
anything you write after it on that same line is not part of the query. If your
query opens on that line, the opening is gone, and the error you get back is a
parse failure pointing somewhere unrelated.
The Assets API updates an asset's metadata before its body. For a second or two after a write, the checksum and the timestamp are the new ones and the value is the old one. A lost-update guard that fetches the body and hashes it is therefore blind in exactly the window it exists to cover: green on every edit older than two seconds, silently wrong on every fresh one.
A bare ampersand is canonicalised to its entity form on the way in. So a byte-for-byte comparison between what you wrote and what the platform now holds will report a difference that is not a difference. Use a surface-aware compare where two layers meet — and only there. A backup-versus-live comparison is two reads of the same system, so that one must stay a byte compare or you will normalise away a real corruption.
“A control that reads a cache tests the cache.” Three controls in this suite were green and measuring nothing, because the fixture they read had been captured before the code they were testing existed in its current form.
A verification run reported every record identical to its backup. That proved nothing, because the backup had been re-taken from live after the write. The rule is short and absolute: never verify against a reference derived from the thing under test. If your baseline is regenerated from your own output, your comparison is a tautology that reports as a pass.
Three of the five traps are in these nine lines, and the function looks reasonable. Work through it and mark the lines that are wrong.
async function publish(key, body) { themeFilesUpsert(files: {filename: $k}) { userErrors { message } } }`; const res = await admin(q, { k: key, body }); const live = await get(key);}
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
Each project stands alone and each one is small. Do them in order: the later ones assume the artifacts the earlier ones produce. None of them requires the model to be good, which is the point — use whatever generator you already have.
Write your rules on one page, sorted into three tiers by what a violation costs. Hard reject, misattribution, human escalation. Nothing else — no code yet.
Enumerate what your generator may never write, by path or by record type. Implement it as a throw inside the function that builds the prompt, so no caller can skip it.
Implement your two highest-cost tier-1 rules. Before you write either, write down the set it must cover, then build a member of that set and confirm the check catches it.
because fieldGive every control a field holding the verbatim input that made it fail. You cannot fill it without having seen the control go red, which is the point.
A script that disables one rule at a time, runs the suite, and asserts that exactly the controls for that rule go red. Run it on the checks from project 3 and expect a surprise.
Pick a repetition problem you have. Instead of editing the prompt, remove something from the brief. Measure before and after at a pinned corpus size and record both numbers with their N.
Wire it together. Generator on one side, tiered linter on the other, no path from the first to a published surface that does not pass through the second. Then confirm it refuses most of its own output.
The tier table is the artifact everything else hangs off, and it is the one people skip. Fill this in and take the output with you — it is a specification, not a prompt, and it is meant to be read by a person before it is read by anything else.
Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.
Build the thing that says no. The generator is the part everyone is working on and the part that will be obsolete by spring. The linter — your rules, tiered by what a violation costs, implemented as classes rather than tokens, each one proven by having been made to fail — is the part you actually own, and it keeps working when you swap the model underneath it.
And keep the register. Write down every rule you have not gated, and be honest about which of them are merely ugly and which are lies waiting to happen. The author of the system in this class kept that list, predicted a defect in it, and then watched the prediction come true on his own site a week later — twice — because writing a rule down is not the same as enforcing it. The list is not an admission of weakness. It is the only reason he knew where to look.
Because a probabilistic system cannot be held to a rule whose violation is a false public claim. A model can be persuaded, and on a live storefront the cost of a persuasive wrong answer is a sentence a customer reads and believes. The system this class is drawn from puts it in one line: the model generates, the linter decides. Generation is a secondary concern to verification, and the practical consequence is that the generator is replaceable — swap the model tomorrow and every guarantee still holds, because none of them were ever coming from the model.
It is a rule for deciding which defects deserve a deterministic checker and which deserve a better prompt, and it comes from a measurement rather than an argument. Three generator defects were each refused by the gate and then addressed by editing the prompt. Two of them concerned things the prompt had never mentioned, and both were fixed on the first try. The third concerned a pattern that appears in nearly every tutorial on the subject — and it survived eleven refusals across a rewritten prompt section and a reject list, never fixed. Where the model has no prior, guidance moves it. Where it has a strong one, the weight of what it learned beats the instruction in front of it, and only code will stop it.
Three reasons, and they compound. A probabilistic judge cannot be controlled — the same input can produce different verdicts. It cannot be unit-tested against a fixed matrix, so you can never demonstrate that it catches the case you built it for. And it cannot be trusted with a rule whose violation is a false public claim, because being usually right is not a standard that survives contact with a regulator or a customer. Deterministic code has none of those properties. It is also dramatically cheaper, faster, and inspectable line by line by whoever has to defend the output.
The Assets API updates metadata before body. For roughly one to two seconds after a write, a read returns the new checksum and the new updated-at timestamp alongside the old value. The consequence is specific and nasty: a lost-update guard that hashes the fetched body is blind in exactly the window it exists to cover. It is green on every edit older than two seconds and silently wrong on every fresh one — which is to say it works perfectly except when it matters. Compare the checksum instead, because that is the platform's own hash and it is atomic with the write.
Retry until you see this run's own build stamp, and never sleep a fixed interval. Inject a unique stamp into every write, then poll the read-back until that exact stamp comes back. A fixed sleep encodes a guess about a latency you do not control, and the guess is wrong on a slow day. Be aware too that neither transport offers a conditional write — there is no ETag on the REST assets endpoint and no precondition on the GraphQL upsert — so compare-then-write narrows the race to a single round trip and never closes it. And the rendered storefront lags further still, measured at up to twenty-five seconds, so if you are verifying what a customer sees you must poll for a marker rather than assume.
A rule about a category, implemented as a match on one member of it. A rule meant to cover any certification claim, written as a search for one certification's name. A rule meant to cover any template consumer, written as a search for one tag. A rule about grammar, implemented as proximity between two words — which reported five hundred and eighteen live defects that did not exist. Each one passes review because the example it was written against does light up. The test is to write down the class the rule is supposed to cover, then construct a member of that class the rule does not catch. If you can, the rule is a token.
You break the thing it watches and confirm it goes red, while a guard-pair control stays green. That second half matters more than it looks: it proves your mutation was targeted rather than catastrophic. This discipline caught two controls in a single month that were passing with the rule they tested switched off — they had never been capable of failing and nothing about their output said so. Every control should also carry the verbatim failing input that caused it to be written, in a field beside the rule, so the next person can see the exact sentence that made it necessary.
Because a list of prohibited outputs does not constrain a generator's output space; it only redirects it. Measured twice on the same corpus: removing the existing copy from the brief moved shared-phrase collapse from 65.5 percent to 13.3 percent, and making the product's graphic the required subject moved the worst class from 35.9 percent to 18.2 percent. Both are constraints on the input. By contrast a banned-phrase list shipped alongside them worked, and then its residue reappeared structurally — the next run's most-shared span was a new phrase family that had not existed before the ban. Keep the bans, but know that the mechanism is the input constraint and the list is mop-up.
Anything where a mistake is unrecoverable or invisible. In the reference system four refusals are permanent and carry no override: the theme layout directory and the two settings files, refused at the path check before their contents are read; and copy for a product class the pipeline cannot describe, refused before a prompt is even constructed. The principle behind all four is that eligibility must be decided from the record by code, inside the function that builds the prompt, and that function must throw rather than return something a caller can decline to read. A gate a script can choose not to call is not a gate, and a brief a script can assemble for itself is a bypass around every gate you have.
No. The architecture in this class runs generation on a local model, which is a cost and privacy decision rather than a safety one — the safety comes entirely from the deterministic layer in front of the publish step, and that layer does not care where the text came from. You can build the whole gate against a hosted API, a local model, or a human typing into a box, and it behaves identically. If anything, start with whatever generator you already have: the point of the class is that it is the replaceable part.