Navigation
What We AreThe BrainPortfolioThe Lab's LabBuilt For YouThe WhiteboardServices & Prices
Let's Talk →
← The Whiteboard

How do you make it impossible for a system to output the wrong thing?

Prompt guardrails are a request. Removing the surface is a guarantee. What it takes to build a system where the output you fear cannot be represented, let alone emitted.

You make a bad output impossible by removing the surface that could carry it, not by instructing the model to avoid it. If a value cannot be represented in the type that crosses the API boundary, no prompt, no fine-tune, no configuration change and no future feature request can emit it. Guardrails filter what was generated. Structure decides what can exist.

Ross Jones — Founder, The Hopium Lab. Last modified 22 July 2026.

The worked example here is Living is Learning, a home-education system that measures a child's development in detail and where a numeric score reaching that child would be a failure of the entire premise. Engineering account, not advice about children.

Why aren't prompt guardrails enough?

Prompt guardrails are a request made to a component whose defining characteristic is that it does not always comply.

They fail in three ways that have nothing to do with prompt quality. They fail under adversarial input, because an instruction expressed in the same channel as the attack can be argued with. They fail silently under model change, because the provider updates something and the instruction that held last month stops holding. And they fail under organisational pressure, which is the one nobody designs for: eighteen months in, someone reasonable asks for a single number for a progress email, and there is nothing in the codebase that says no.

A rule the model can be talked out of is not a rule. A rule the codebase cannot express is a rule.

What does structural impossibility actually mean?

Structural impossibility means the undesired output has no representation anywhere it could travel — not that it is checked for and rejected.

The distinction is concrete. A validator that rejects a numeric score still has a field capable of holding one; the check is code that can be deleted, skipped in a hotfix, or bypassed by a new endpoint written by someone who never read the validator. A projector that maps an internal type to an external type with no numeric field cannot be bypassed, because there is nowhere for the number to go. The output type is the enforcement.

In Living is Learning the internal substrate is unapologetically numeric — a level and a confidence, both 0 to 1, plus the trajectory maths over their revisions. You cannot compute a slope over the word "emerging". Those numbers exist to answer three questions and only three: which direction is this moving, how fast, and when did it turn. They are how the system finds the moment worth showing a parent. They are never an output.

Everything that crosses the boundary is one of four shapes: a qualitative state from a named vocabulary, ordered but not numbered and explicitly not equal-interval; a direction of travel — rising, steady, or quieter lately, never a rate; the evidence moment itself, in the learner's own words, with the activity it happened in; and a depth state running encountered → explained → applied → connected → teaching-others.

The atomic unit a human sees is narrative + direction + anchoring evidence moment + timeframe. Never a value.

Where do you put the wall?

Put the wall at the type boundary that every consumer must cross, and make crossing it lossy in exactly the direction you care about.

A projector sits between the internal state and the external representation. It does not sanitise, redact or round — it maps to a type that has no numeric growth field at all. Because there is no field, there is no code path that populates one, and no configuration that re-enables it.

Then a contract test asserts the property directly: no numeric growth value appears in any dashboard response. That test is not testing the projector's logic. It is testing the invariant, so that a future contributor adding a genuinely reasonable feature discovers the constraint at build time rather than shipping past it.

ApproachWhat stops the bad outputWhat defeats it
Prompt instructionThe model choosing to complyAdversarial input, model update, a persuasive user
Output filter / regexA check after generationAn unfiltered code path, a new endpoint, a hotfix
Schema validationA rejected responseDeleting the validator; a field that still exists
Type-level projectionNo field exists to populateA deliberate architectural change, reviewed
Absent endpointThe capability was never builtBuilding it — which is now a visible decision

The bottom two rows share a property the top three lack: defeating them requires someone to intend to, in a diff, where it can be seen.

What else has to be absent?

Absence is the mechanism, so the list of things that do not exist matters more than the list of things that do.

There is no cross-learner surface anywhere in the system — no comparison endpoint, no age norms, no percentiles, no benchmarks. Not disabled: absent. A ranking cannot be requested because the shape of the request has no handler.

There is no cross-dimension ranking either. Dimensions render in a stable, pack-defined order, never sorted by level, and there is no "top strengths and weakest areas" view — which is the single most requested feature of any system like this, and the one that would convert the whole thing back into a report card.

The qualitative states are deliberately non-equidistant and relabel-safe, so nobody can quietly start treating them as a one-to-five scale, and the projector refuses to map states to numbers in any export — including the export, because exports are where invariants usually die.

The most important list in a system with an integrity requirement is the list of things it cannot do. Write that list first, and enforce it in types rather than in documentation.

Two smaller rules do disproportionate work. "Quieter lately" is legitimate — a dimension going dormant is information about a life, not a regression, and the narrative templates forbid loss language: fell, dropped, below. And direction requires evidence: no direction of travel is asserted without at least one anchoring moment above a confidence floor, which prevents the system generating confident-sounding narrative over statistical noise.

How do you stop a fork from breaking it?

You separate what is configurable from what is guaranteed, and you put the guarantee in the engine where configuration cannot reach it.

Living is Learning splits into an engine that contains no domain vocabulary at all — it does not know it is measuring children — and a pack that carries the entire philosophy: the taxonomy, the extraction instructions, the state vocabularies, the persona, the thresholds. Change the philosophy and you re-seed the pack. No code changes.

But the anti-grading guarantee is engine-enforced and not pack-overridable. The renderer has no scoring affordance for a pack to switch on. So a completely different educational philosophy can be forked in, and the one property that matters survives contact with it.

Fork the philosophy. You cannot fork away the integrity.

How do you apply this to your own system?

Find the output that would end you, then delete its representation rather than guarding it.

STRUCTURAL IMPOSSIBILITY — DESIGN PASS
The Hopium Lab · v1.0 · 22 July 2026 · take it, fork it, argue with it

1. NAME THE OUTPUT YOU FEAR
   [ ] Write the specific value, in one sentence, that must never reach a user
   [ ] Name who is harmed when it does, and how badly

2. FIND EVERY SURFACE IT COULD TRAVEL ON
   [ ] API responses        [ ] Exports and reports
   [ ] Logs and traces      [ ] Webhooks and integrations
   [ ] Error messages       [ ] Support tooling and admin views

3. REMOVE THE REPRESENTATION
   [ ] Does the external type have a field that could hold it? Delete the field
   [ ] Is there an endpoint whose shape implies it? Delete the endpoint
   [ ] Can config re-enable it? Move the guarantee below the config layer

4. ASSERT THE PROPERTY, NOT THE CODE
   [ ] One contract test: this value never appears in any response
   [ ] Run it against exports too — exports are where invariants die

5. CHECK WHAT SURVIVES A FORK
   [ ] If someone swaps the entire configuration, does the guarantee hold?
   [ ] If not, it is a convention, not a guarantee — say so out loud

THE TEST: could a well-meaning contributor, under deadline, ship the thing
you fear without anyone noticing? If yes, you have a policy, not an architecture.

Most systems fail step three and stop at step four, which produces a validator guarding a field that should not exist. The field is the vulnerability. The check is a comment about the vulnerability.

What does this cost?

It costs optionality, and you should know that before choosing it.

You cannot later add the feature you designed against. A number for a progress email, a leaderboard for engagement, a percentile because a customer asked — all of those are now architectural changes rather than tickets, and some of them are genuinely reasonable requests you will have to refuse. That is the trade: you are spending future flexibility to buy a guarantee that survives your own future judgement.

Make that trade only where being wrong is unrecoverable. For a system measuring a child's development, where the harm is a person learning to see themselves as a number, it is obviously worth it. For a dashboard about warehouse throughput, it obviously is not. The skill is knowing which one you are building — and the replay test is the sibling question for the same instinct: can you rebuild the answer without calling the model?

Ross Jones, Founder, The Hopium Lab. Last modified 22 July 2026. Living is Learning is a private system; this describes its architecture, not its contents.