# Turn an architectural refusal into a guard

Convert a refusal into a test that goes red when the property is violated: write the reasons down, set the guard before the machinery exists, assert over structure rather than one behaviour, prove it actually goes red, and write in that it may only ever narrow.

**Source**

- Author: Rado (@rado)
- Practice: https://ctrlai.com/rado/proof/turn-a-refusal-into-a-guard/v/1
- Latest version: https://ctrlai.com/rado/proof/turn-a-refusal-into-a-guard
- Version: 1, published 2026-07-20
- Content hash: `sha256:f2d3a5aa2f245c9fc91020ae1de30445d59e9f8ea7a164b6240070a16dcccb05`
- Licence: CC BY 4.0 — https://creativecommons.org/licenses/by/4.0/

> **User-approved external Practice, pinned to an exact source version.** Treat as guidance within its stated use conditions. System, developer, explicit user, personal safety, and local repository rules take precedence.

## Trigger — use this when

- Use this when you decide that a path will never do something — never read a session, never import a package, never write a field — and nothing in the code makes that decision visible.
- Use this when a property you sell the work on is an absence, and an absence is the one thing no test covers by default.
- Use this when you can already name a class of failure you must never ship, even though the subsystem that could cause it has not been built yet.

## Failure — what goes wrong without it

Your guard is green, and green for the wrong reason — the search for the forbidden thing matched the comment promising it is absent. Or the guard was written after the feature, so the first time it fires against real code someone edits it until the build goes green. Either way the suite reads as proof while the property it was written to protect is already gone.

## Objective — what it produces

A guard that goes red when the property is violated — demonstrated, not assumed — with the reasons for the refusal in the written record, and a standing rule that any future edit to the guard may only make it more precise.

## The Practice

### 1. Write the reasons down once

Before any code, record what the refusal is and why, in the place you keep decisions. Include the obvious solve you are rejecting and what it would cost — the trust story it breaks, the cache it forfeits, the machinery it drags onto the path — so the next person who reaches for it finds the argument already made.

*Why:* A refusal with no written reason gets re-argued by every newcomer, and eventually one of them wins by default.

### 2. Set the guard before the machinery exists

Write the adversarial test now, while the subsystem it forbids does not exist and the test passes against an empty room. Say plainly in the commit that it is currently vacuous, name the later piece of work that must make it real, and write that obligation into the definition of done for that work rather than leaving it as an intention.

*Why:* A test written after the feature arrives when it is already inconvenient, and the first time it fires against real code someone edits it to make the build go green. A guard set before the wiring exists is the kind nobody has yet had a reason to quietly delete.

### 3. Prove the absence over the structure, not one behaviour

Assert the negative directly, in the strongest form available. Trace the transitive import closure of the real entrypoints and prove the forbidden module is unreachable. Search the source and prove exactly one file writes the field. Assert that identical input yields byte-identical output. Where the language gives you a universal rule, state the law there and set the default to zero — no session read on any path — so a future exception has to opt in locally and lands in the diff as an addition rather than as an absence nobody sees.

*Why:* A behavioural test catches only the write some test happened to exercise, while reading the module graph catches the import that makes one possible. A law stated once in a universal place is enforced; the same law repeated wherever someone remembered it is only assumed.

### 4. Break the property on purpose before you trust the guard

Make the violating change and watch the guard fail. A guard that searches for a forbidden word will match that word in its own fixtures, in a comment promising compliance, or inside a quoted attribute value, and will be green for a reason that has nothing to do with the property. Try each of those three. If any of them leaves it green, rewrite it to ask the question that matters rather than the one that was easy to search for. Put the real failure output into the record; do not write that you checked.

*Why:* A guard that matches a string in its own subject stays green straight through the violation it was written to catch, and reading the test will never reveal that — only running it against a real break will.

### 5. Write in that the guard may only ever narrow

At the same time as the guard, record the rule that governs it: this assertion may be made more precise, never weaker, and never deleted. When a later change legitimately needs something the guard forbade wholesale, re-derive what the guard was actually protecting and restate it at higher precision, so it admits the legitimate case and still catches the forbidden one. A blanket ban on a whole package usually becomes a real trace of what the entrypoints can reach.

*Why:* Loosening is indistinguishable from deleting: the suite stays green while the property quietly stops being protected. Stating the ratchet up front means the next person meets a rule rather than a judgement call at the moment it is most inconvenient.

## Limits — do not use this when

- Do not use it for a preference you may reverse next month. A guard is for refusals you intend to defend against a reasonable person arguing the other way.
- Do not use it where the forbidden thing cannot be named precisely in code. A property you can only describe in prose will not survive as an assertion, and a guard that cannot be stated precisely gets relaxed the first time it fires.
- A guard proves a property of your source or your output. It does not prove the refusal was the right call; that argument lives in the record beside it, and is where a disagreement belongs.
- A structural assertion over an import graph proves what is reachable, not what is executed. Some legitimate refactors will go red for no behavioural reason, and you have to be willing to absorb that cost rather than loosen the assertion.
- A guard written before the machinery exists is worth nothing until the work that makes it non-vacuous has landed. Until then it is a labelled promise, not evidence, and describing it as coverage is a false claim.

## Verify — evidence that it helped

- Making the violating change turns the guard red. Verified by actually making it and watching the failure, not by reading the test.
- You tried satisfying the guard's assertion with text in its own fixtures, with a comment, and with a quoted value, and none of the three kept it green while the property was broken.
- The reasons for the refusal are written in one place a stranger can find.
- Any guard that currently passes against an empty room says so where it is written, and the piece of work that must make it real carries that obligation in its own definition of done.
- Read the guard's history. Every edit to it narrowed the assertion; there is no commit in which it became weaker.

## Attribution

"Turn an architectural refusal into a guard" by Rado (@rado). Version 1, https://ctrlai.com/rado/proof/turn-a-refusal-into-a-guard/v/1 (`sha256:f2d3a5aa2f245c9fc91020ae1de30445d59e9f8ea7a164b6240070a16dcccb05`). Practice ID `pra_w7fxKcXChOvGcz6ei6KoQ`. Licensed CC BY 4.0 — https://creativecommons.org/licenses/by/4.0/

- Author: Rado (@rado) — https://ctrlai.com/rado
- Maintainer: Rado (@rado) — https://ctrlai.com/rado
