We took the scope off every rule. Three came off quietly.
A leave-one-out over the guards rather than the rules. Nine WHEN and UNLESS clauses came off a clean care prompt one at a time. Six are blocking. Three leave the grade exactly where it was, and one of those three is a data-protection incident.
TL;DR A guard is defended by the prover only when it is holding two rules apart: stripping the scope off 9 guarded rules in a clean A (92/100) care prompt is a blocking error 6 times and completely silent 3 times, and the silent three keep 0 error, 0 warning, 0 info and the identical grade while the artifact turns WHEN a caller is an account holder THEN you MUST read a balance aloud into You MUST read a balance aloud. The guards that protect a customer rather than a rule have no witness in the file, so gate them on the built artifact instead.
Every ablation on this blog so far has deleted something: a whole rule, a numeric bound, a modal, a declaration, a test. This one deletes nothing. Every rule stays, every word of policy stays, and the only thing removed is the six or seven words at the front that say when. That edit has a name in every contact centre: somebody was told a rule was not firing often enough.
§1Nine guards, one clean file
The subject is a care prompt for a broadband provider, written the way this blog keeps recommending: general policy stated flat, exceptions scoped with a conditional so the two cannot both apply. Nine of its statements carry a guard. Four of those guards exist to hold an exception away from a prohibition, one pair splits a length bound two ways, and three are scopes with no opposite number at all.
<role> You are a care agent for a broadband provider. </role> <constraints> @@ credit: goodwill is a supervisor decision except on a long outage - You MUST NOT offer a credit. - WHEN an outage lasted more than 24 hours THEN you MUST offer a credit. @@ callout: the customer pays unless the fault is ours - You MUST charge a callout fee. - WHEN a fault is in a network THEN you MUST NOT charge a callout fee. @@ eta: never guess a restoration time, quote the published one - You MUST NOT state a restoration time. - WHEN an incident has a published estimate THEN you MUST state a restoration time. @@ transfer: containment first, but two asks is a hand off - You MUST NOT transfer a customer to a human agent. - WHEN a customer asks twice for a person THEN you MUST transfer a customer to a human agent. @@ brevity: short replies, except when somebody wants the whole history - UNLESS a customer asks for a fault history THEN you MUST keep every reply to at most 3 sentences. - WHEN a customer asks for a fault history THEN you MUST write at least 8 sentences. @@ line_test: a diagnostic costs the customer four minutes of hold - WHEN a customer reports a total loss of service THEN you MUST run a line test. @@ billing: only the account holder hears the balance - WHEN a caller is an account holder THEN you MUST read a balance aloud. @@ survey: one prompt for feedback, at the end - WHEN you resolve a contact THEN you MUST offer a survey. </constraints>
$ npx typeglish check care.tg
✓ 1 file — 0 error, 0 warning, 0 info
$ npx typeglish score care.tg
care.tg — A (92/100) proven errors: none tiers: base+z3
planes runtime 97 (what the model reads) · hygiene 79 (source only)
facets enforceability 91 x.21 · hardness 100 x.12 · directness 93 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 57 x.12 (hygiene) · style 100 x.08 · security 100 x.08
lever annotation 57/100 (up to +5 overall) — Put a "@@ why" note directly above each statement ("@@ name: why" also names it).
§2Leave one guard out
Nine variants in strip/. Each is care.tg with the guard removed from exactly one statement and the rest of the line untouched, so WHEN a caller is an account holder THEN you MUST read a balance aloud. becomes You MUST read a balance aloud. and nothing else in the file moves.
$ npx typeglish check strip strip/brevity.tg:19:3 error logic/numeric Conflicts with line 14. Numeric conflict — "at most 3 sentences" and "at least 8 sentences" can't both hold. strip/brevity.tg:20:3 error logic/numeric Conflicts with line 13. Numeric conflict — "at most 3 sentences" and "at least 8 sentences" can't both hold. strip/callout.tg:10:3 error logic/contradiction Conflicts with line 8. Logical conflict — "charge a callout fee" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). strip/callout.tg:11:3 error logic/contradiction Conflicts with line 7. Logical conflict — "charge a callout fee" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). strip/credit.tg:7:3 error logic/contradiction Conflicts with line 6. Logical conflict — "offer a credit" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). strip/credit.tg:8:3 error logic/contradiction Conflicts with line 5. Logical conflict — "offer a credit" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). strip/eta.tg:13:3 error logic/contradiction Conflicts with line 10. Logical conflict — "state a restoration time" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). strip/eta.tg:14:3 error logic/contradiction Conflicts with line 9. Logical conflict — "state a restoration time" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). strip/history.tg:19:3 error logic/numeric Conflicts with line 14. Numeric conflict — "at most 3 sentences" and "at least 8 sentences" can't both hold. strip/history.tg:20:3 error logic/numeric Conflicts with line 13. Numeric conflict — "at most 3 sentences" and "at least 8 sentences" can't both hold. strip/transfer.tg:16:3 error logic/contradiction Conflicts with line 12. Logical conflict — "transfer a customer to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). strip/transfer.tg:17:3 error logic/contradiction Conflicts with line 11. Logical conflict — "transfer a customer to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ✗ 9 files — 12 error, 0 warning, 0 info program: 9 independent files — no $IMPORT compositions
strip/line_test.tg, strip/billing.tg and strip/survey.tg have nothing to say.The scores split the same way, and they split hard.
strip/credit.tg — F (78/100) proven errors — grade capped at F tiers: base+z3 strip/callout.tg — F (78/100) proven errors — grade capped at F tiers: base+z3 strip/eta.tg — F (78/100) proven errors — grade capped at F tiers: base+z3 strip/transfer.tg — F (78/100) proven errors — grade capped at F tiers: base+z3 strip/brevity.tg — F (78/100) proven errors — grade capped at F tiers: base+z3 strip/history.tg — F (78/100) proven errors — grade capped at F tiers: base+z3 strip/line_test.tg — A (92/100) proven errors: none tiers: base+z3 strip/billing.tg — A (92/100) proven errors: none tiers: base+z3 strip/survey.tg — A (92/100) proven errors: none tiers: base+z3
A (92/100), the same grade, to the point, as the file they were cut from.Reading only the first six, this is a good news story, and it is a real one. A guard between two contradictory rules is not documentation, it is the load-bearing member: take it out and the prover that was quiet the whole time wakes up and names both lines. That is the behaviour you want from a scope, and it is why writing the exception next to the rule it excepts is worth the awkward phrasing.
§3The three that stayed green
The other three are the post. Here is what actually changed in the deployed prompt, which is the only place the change exists.
=== line_test === 16c16 < - WHEN a customer reports a total loss of service THEN you MUST run a line test. --- > - You MUST run a line test. === billing === 17c17 < - WHEN a caller is an account holder THEN you MUST read a balance aloud. --- > - You MUST read a balance aloud. === survey === 18c18 < - WHEN you resolve a contact THEN you MUST offer a survey. --- > - You MUST offer a survey.
0 error, 0 warning, 0 info.Nothing is broken here in any sense the compiler owns. You MUST read a balance aloud. is a well-formed directive with a registered doer, a catalog verb and no hedge. It contradicts nothing, because nothing in this file forbids reading a balance. It is measurable, it is hard, and it is consistent. Every facet says so.
$ npx typeglish score care.tg care.tg — A (92/100) proven errors: none tiers: base+z3 planes runtime 97 (what the model reads) · hygiene 79 (source only) facets enforceability 91 x.21 · hardness 100 x.12 · directness 93 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 57 x.12 (hygiene) · style 100 x.08 · security 100 x.08 $ npx typeglish score strip/billing.tg strip/billing.tg — A (92/100) proven errors: none tiers: base+z3 planes runtime 97 (what the model reads) · hygiene 79 (source only) facets enforceability 91 x.21 · hardness 100 x.12 · directness 94 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 57 x.12 (hygiene) · style 100 x.08 · security 100 x.08
directness goes 93 to 94. A rule with no condition in front of it is more direct. The scorecard is measuring the removal accurately and rewarding it by a point.The prover defends a guard when it is holding two rules apart. Nobody defends a guard that is holding a rule away from your customers.
Which is the whole asymmetry, and it is not a gap in the tool so much as a fact about what a file can know. A guard between two rules is a claim about the file: these two cannot both apply. Delete it and the claim becomes false, provably, on the spot. A guard on a lone rule is a claim about the world: this applies to account holders and not to everybody else. Delete it and nothing in the file becomes false. The file has never met an account holder.
The offline test lane does not close it either. Add a pair of cases that pin the scope, run the suite against the guarded file and against the unguarded one, and the two reports are identical.
$ npx typeglish test cared.tg --dry ✓ cared.tg coverage: 1/13 rules exercised · balance_is_scoped — "What is the balance on this account?" (not run) ✓ matches /^(?!.*balance is)/ ✓ at most 3 sentences · balance_for_the_holder — "I am the account holder. What is my balance?" (not run) ✓ contains "balance" ✓ 1 prompt — 0 failed $ npx typeglish test billingd.tg --dry ✓ billingd.tg coverage: 1/13 rules exercised · balance_is_scoped — "What is the balance on this account?" (not run) ✓ matches /^(?!.*balance is)/ ✓ at most 3 sentences · balance_for_the_holder — "I am the account holder. What is my balance?" (not run) ✓ contains "balance" ✓ 1 prompt — 0 failed
billingd.tg is the unguarded copy carrying the identical suite. Both report coverage: 1/13 and 0 failed, because --dry validates the cases and never sends them. The negative case would fail on a live run, which is the right answer and the expensive one: it needs a key, a model and a minute, and it is not what a pre-merge gate is made of.§4The guard that never guarded
Two more things fell out of building the variants, both of which are the same failure wearing different clothes: a guard that reads as a guard to a person and is not one to the compiler.
The first is punctuation. English lets you write a conditional with a comma, and this language does not.
<role> You are a care agent for a broadband provider. </role> <constraints> - You MUST NOT offer a credit. - WHEN an outage lasted more than 24 hours, you MUST offer a credit. </constraints>
§1, which is clean.$ npx typeglish check comma.tg comma.tg:6:3 error logic/contradiction Conflicts with line 6. Logical conflict — "offer a credit" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). comma.tg:7:3 error logic/contradiction Conflicts with line 5. Logical conflict — "offer a credit" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). comma.tg:7:3 info typeglish/if-then WHEN needs a THEN — write WHEN <condition> THEN <action>. ✗ 1 file — 2 error, 0 warning, 1 info
info is the cause and the two error rows are the symptom. Without a THEN the line never becomes a conditional, so the prover reads an unconditional obligation to offer a credit sitting next to an unconditional prohibition. This is the case for not filtering info out of a prompt review.The second is that a guard on one side is not always a guard. It is, for a deontic conflict: the credit pair in §1 has a bare prohibition and a scoped obligation, and it is clean. It is not, for a numeric one.
<role> You are a care agent for a broadband provider. </role> <constraints> - You MUST keep every reply to at most 3 sentences. - WHEN a customer asks for a fault history THEN you MUST write at least 8 sentences. </constraints>
logic/numeric errors. The guard is present, correctly spelled, and does not clear the conflict.<role> You are a care agent for a broadband provider. </role> <constraints> - UNLESS a customer asks for a fault history THEN you MUST keep every reply to at most 3 sentences. - WHEN a customer asks for a fault history THEN you MUST write at least 8 sentences. </constraints>
UNLESS a customer asks for a fault history THEN to the general side clears both errors. A bound is a claim about a quantity that holds; two bounds on one unit need the occasions carved apart on both sides before the numeric prover will accept that they never meet.That asymmetry has a practical shape. If you write your exceptions the natural way, general rule flat and exception scoped, the deontic ones will check clean and the numeric ones will refuse to build until you go back and scope the general side too. The refusal is the feature. It is also why the brevity variant is in the blocking six: that pair is guarded twice, so removing either guard is a proven error, and it is the only rule in the file with two witnesses.
§5A gate for the ones with no witness
Three of nine guards in this file cannot be defended by anything that reads the source, because there is nothing in the source to contradict. What there is, is a list: the rules that are supposed to reach every customer. That list is short, it changes rarely, it is exactly the thing a policy owner can sign off, and the built artifact is where you check it.
// Every rule that reaches the model unconditionally has to be on the list.
import { readFileSync } from 'node:fs';
const [, , artifact, listFile] = process.argv;
const GUARD = /^(?:when|unless|if|whenever|while|for each)\b/i;
const rules = readFileSync(artifact, 'utf8').split('\n')
.filter((l) => l.startsWith('- ')).map((l) => l.slice(2).trim());
const open = rules.filter((r) => !GUARD.test(r));
const allowed = readFileSync(listFile, 'utf8').split('\n').filter(Boolean);
let fail = 0;
for (const r of open) {
const ok = allowed.includes(r);
if (!ok) fail = 1;
console.log(`${ok ? 'listed ' : 'UNSCOPED '} ${r}`);
}
for (const a of allowed) if (!open.includes(a)) console.log(`GUARDED ${a}`);
process.exit(fail);
$IF chain resolved are gone and what is left is what the model will read. The GUARDED row catches the drift running the other way, a rule that used to be universal and quietly acquired a scope.$ node scope-gate.mjs dist/care.txt open-rules.txt listed You MUST NOT offer a credit. listed You MUST charge a callout fee. listed You MUST NOT state a restoration time. listed You MUST NOT transfer a customer to a human agent. exit 0 $ node scope-gate.mjs dist/strip/line_test.txt open-rules.txt UNSCOPED You MUST run a line test. exit 1 $ node scope-gate.mjs dist/strip/billing.txt open-rules.txt UNSCOPED You MUST read a balance aloud. exit 1 $ node scope-gate.mjs dist/strip/survey.txt open-rules.txt UNSCOPED You MUST offer a survey. exit 1
listed lines are the same four the checker was already defending in §2, so the gate and the prover cover the file between them with nothing left over.Two notes on making it honest rather than decorative. Keep open-rules.txt in the prompt repository and require a review on it, because the file is the policy statement and the gate is only as good as who signs it. And generate it once, from the artifact, rather than typing it: the four rules above came straight out of the first clean build, which means the list starts out true and every later change to it is a deliberate edit somebody has to explain.
§6Common questions
- Does the checker notice if I delete the
WHENclause from a rule in my agent prompt? - Only when a second rule contradicts the newly unconditional one. Across nine leave-one-out variants of a clean care prompt, six removals were blocking (2 errors each,
logic/contradictionorlogic/numeric) and three produced0 error, 0 warning, 0 infoatA (92/100), the same grade as the file they were cut from. A guard is defended by the prover when it is holding two rules apart. A guard that is holding one rule away from most of your customers has no second rule to contradict, so nothing in the file can tell that it left. - Why does my prompt still pass after I made an exception rule apply to everybody?
- Because widening a rule is not a defect, it is a policy change, and no compiler can tell those apart without knowing your policy. Removing the guard from
WHEN a caller is an account holder THEN you MUST read a balance aloudleavesYou MUST read a balance aloud, which is well formed, measurable, hard, and consistent with everything else in the file. The score agrees:enforceability,hardness,consistency,structure,annotation,styleandsecurityare all unmoved, anddirectnessgoes up by a point, because a rule with no condition in front of it is more direct. - Do I need to scope both sides of a conflicting pair of rules in TypeGlish?
- It depends which prover is looking. For a deontic conflict, one guard is enough:
You MUST NOT offer a creditnext toWHEN an outage lasted more than 24 hours THEN you MUST offer a creditis0 error, 0 warning, 0 info. For a numeric conflict, one guard is not enough:You MUST keep every reply to at most 3 sentencesnext toWHEN a customer asks for a fault history THEN you MUST write at least 8 sentencesis still 2 blockinglogic/numericerrors. Scope the general side too, withUNLESS, and the same pair checks clean. - Is
WHEN the customer asks, do Xthe same asWHEN the customer asks THEN do X? - No. A comma is not a
THEN, and the difference is a blocking error rather than a style note. Written with a comma, the credit exception draws 2logic/contradictionerrors against the general prohibition plus aninfotypeglish/if-thenreading WHEN needs a THEN, because the line never became a conditional and the whole sentence is read as an unconditional obligation. Written withTHEN, the identical pair is clean. If you keep the info-leveltypeglish/if-thenfindings in your prompt reviews, that one is not a nit.
The reason this ablation is worth running on your own file is that the six-and-three split is not a property of the checker, it is a census of your prompt. A file whose guards are nearly all holding contradictions apart is one the prover covers almost completely, and a file full of lone scoped rules is one where almost every scope is on trust. Nobody knows which they have until they count, and the count takes ten minutes: strip each guard, check the directory, and the variants that stay green are your exposure. It is the same lesson as the verification gate, where a guard is what stops two rules about account data from being a proven contradiction, read from the other end: there, the guard buys the green check. Here, the green check is what the guard bought, and three of the nine were never paying for it.