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.
The first version of this post wrote each default flat and scoped only its exception: You MUST NOT offer a credit. beside WHEN an outage lasted more than 24 hours THEN you MUST offer a credit., and called the pair clean. That was wrong. A flat rule applies on the long outage too, so both rules fire there and the pair is a contradiction. typeglish 0.9.0 accepted it only because it proved IF guards and no others; 0.10.0 proves WHEN, WHENEVER, WHILE, UNLESS and ELSE IF, and reports the old shape as logic/contradiction. The baseline below now carves each default out with the complementary UNLESS, and every figure on this page is re-run against 0.10.0. The six-and-three split survived. The sentence in §4 that said a deontic pair only needs one guard did not.
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.
§1Thirteen guards, one clean file
The subject is a care prompt for a broadband provider, written the way this blog now recommends: every exception carved out of its default with complementary guards, UNLESS the case on the default and WHEN the case on the exception, so the two can never both apply. All thirteen of its statements carry a guard. Eight of them are four deontic pairs, a prohibition and its exception holding each other off; two split 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 - UNLESS an outage lasted more than 24 hours THEN 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 - UNLESS a fault is in a network THEN 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 - UNLESS an incident has a published estimate THEN 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 - UNLESS a customer asks twice for a person THEN 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>
UNLESS on each default is the part the first version of this post left out.$ 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 96 (what the model reads) · hygiene 79 (source only)
facets enforceability 91 x.21 · hardness 100 x.12 · directness 90 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) — 6 statements without a @@ note (L2 L8 L11 L14 L17 L20) — put a "@@ why" line directly above each
rules 13 of 13 rule-shaped lines read · 4 not provable
L10 ~ unprovable UNLESS a fault is in a network THEN you MUST charge a callout fee.
"charge" is not a verb the checker knows → use a verb from the catalog (redirect, escalate, ask, cite …) or add a numeric bound
L11 ~ unprovable WHEN a fault is in a network THEN you MUST NOT charge a callout fee.
"charge" is not a verb the checker knows → use a verb from the catalog (redirect, escalate, ask, cite …) or add a numeric bound
L22 ~ unprovable WHEN a customer reports a total loss of service THEN you MUST run a line test.
the checker knows "run" but cannot prove this shape: "run" is a light verb with nothing bound to it → name the tool or object it acts on (call @[tool], make a note)
L24 ~ unprovable WHEN a caller is an account holder THEN you MUST read a balance aloud.
"read" is not a verb the checker knows → use a verb from the catalog (redirect, escalate, ask, cite …) or add a numeric bound
rules table lists four lines the prover cannot use: charge and read are not verbs it knows, and run has nothing bound to it. Keep an eye on L24.§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. The nine are the WHEN side of each of the four deontic pairs, each side of the brevity pair, and the three lone guards. The four UNLESS defaults are not cut separately: each is the other half of a pair whose WHEN half already is, and taking the UNLESS off instead draws the same two errors.
$ npx typeglish check strip strip/brevity.tg:19:3 error logic/numeric Conflicts with line 20. Numeric conflict — "at most 3 sentences" and "at least 8 sentences" can't both hold. ↳ line 20: - WHEN a customer asks for a fault history THEN you MUST write at least 8 sentences. strip/brevity.tg:20:3 error logic/numeric Conflicts with line 19. Numeric conflict — "at most 3 sentences" and "at least 8 sentences" can't both hold. ↳ line 19: - You MUST keep every reply to at most 3 sentences. strip/callout.tg:10:3 error logic/contradiction Conflicts with line 11. 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 ...). ↳ line 11: - You MUST NOT charge a callout fee. strip/callout.tg:11:3 error logic/contradiction Conflicts with line 10. 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 ...). ↳ line 10: - UNLESS a fault is in a network THEN you MUST charge a callout fee. strip/credit.tg:7:3 error logic/contradiction Conflicts with line 8. 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 ...). ↳ line 8: - You MUST offer a credit. strip/credit.tg:8:3 error logic/contradiction Conflicts with line 7. 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 ...). ↳ line 7: - UNLESS an outage lasted more than 24 hours THEN you MUST NOT offer a credit. strip/eta.tg:13:3 error logic/contradiction Conflicts with line 14. 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 ...). ↳ line 14: - You MUST state a restoration time. strip/eta.tg:14:3 error logic/contradiction Conflicts with line 13. 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 ...). ↳ line 13: - UNLESS an incident has a published estimate THEN you MUST NOT state a restoration time. strip/history.tg:19:3 error logic/numeric Conflicts with line 20. Numeric conflict — "at most 3 sentences" and "at least 8 sentences" can't both hold. ↳ line 20: - You MUST write at least 8 sentences. strip/history.tg:19:3 warn prompt/count-alignment Count alignment — "sentence" carries two different count rules (line 20): one names a unit, one does not. Two spellings of one limit read as drift — state ONE bound with ONE unit. ↳ line 20: - You MUST write at least 8 sentences. strip/history.tg:20:3 error logic/numeric Conflicts with line 19. Numeric conflict — "at most 3 sentences" and "at least 8 sentences" can't both hold. ↳ line 19: - UNLESS a customer asks for a fault history THEN you MUST keep every reply to at most 3 sentences. strip/transfer.tg:16:3 error logic/contradiction Conflicts with line 17. 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 ...). ↳ line 17: - You MUST transfer a customer to a human agent. strip/transfer.tg:17:3 error logic/contradiction Conflicts with line 16. 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 ...). ↳ line 16: - UNLESS a customer asks twice for a person THEN you MUST NOT transfer a customer to a human agent. ✗ 9 files — 12 error, 1 warning, 0 info program: 9 independent files — no $IMPORT compositions
strip/history.tg also draws a prompt/count-alignment warning from a separate check, one that reads two spellings of one limit as drift. Three of the nine variants do not appear in this report at all: 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 (76/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
history gives up two more points to its warning); three land on 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 and no hedge. It contradicts nothing, because nothing in this file forbids reading a balance. The rules table marks it unprovable, read not being a verb the checker knows, but it marked the guarded line unprovable in the same words: the row changed text and kept its verdict. Nor is provability the reason it went quiet. survey is the control: offer is a verb the checker knows, the credit pair is proven on it, You MUST offer a survey. is not among the unprovable four, and that removal is exactly as silent. The rule is hard and it is consistent. Every facet says so.
$ npx typeglish score care.tg | head -3 care.tg — A (92/100) proven errors: none tiers: base+z3 planes runtime 96 (what the model reads) · hygiene 79 (source only) facets enforceability 91 x.21 · hardness 100 x.12 · directness 90 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 | head -3 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 91 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 90 to 91, and the runtime plane with it, 96 to 97. 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: 2/13 rules exercised uncovered (no case shares the rule's wording): L7 offer a credit. L8 offer a credit. L10 charge a callout fee. L11 charge a callout fee. L13 state a restoration time. L14 state a restoration time. L16 transfer a customer to a human agent. L17 transfer a customer to a human agent. L22 run a line test. L24 read a balance aloud. L26 offer a survey. · 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: 2/13 rules exercised uncovered (no case shares the rule's wording): L7 offer a credit. L8 offer a credit. L10 charge a callout fee. L11 charge a callout fee. L13 state a restoration time. L14 state a restoration time. L16 transfer a customer to a human agent. L17 transfer a customer to a human agent. L22 run a line test. L24 read a balance aloud. L26 offer a survey. · 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: 2/13, the same eleven uncovered rules with the balance rule among them, 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> - UNLESS an outage lasted more than 24 hours THEN you MUST NOT offer a credit. - WHEN an outage lasted more than 24 hours, you MUST offer a credit. </constraints>
§1, which is clean: the THEN on the exception is a comma.$ npx typeglish check comma.tg comma.tg:7:3 info typeglish/if-then WHEN needs a THEN — write WHEN <condition> THEN <action>. comma.tg:7:3 info prompt/unparsed-rule Reads like a rule, but the rule reader cannot parse it as one, so it earns no Enforceability credit and no measurability check: the condition has no THEN branch, so nothing is required when it holds. Fix: add THEN MUST / NEVER / SHOULD <verb> …. ✓ 1 file — 0 error, 0 warning, 2 info
info rows, and the exception is gone. Without a THEN the line never becomes a conditional, and the rule reader cannot parse it as a rule at all: nothing is required when the outage runs long, the line earns no enforceability credit, and tg score lists it as invisible, 1 of 2 rule-shaped lines read. The model still gets the sentence, because the artifact carries it verbatim; the compiler's ledger does not. This is the case for not filtering info out of a prompt review.The second is that a guard on one side is not a guard. The first version of this post said it was, for a deontic pair, and built the whole baseline that way: a bare prohibition beside a scoped obligation. It is not, for either kind of conflict, and 0.10.0 says so.
<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 THEN you MUST offer a credit. </constraints>
logic/contradiction errors. On a long outage the exception fires and the flat prohibition still applies, so both rules hold and the prover names both lines. The WHEN scopes the obligation; nothing scopes the prohibition.<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, because the flat cap still applies to the fault history.<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, and the same edit clears flat.tg: UNLESS an outage lasted more than 24 hours THEN you MUST NOT offer a credit. is the line the baseline carries. Two rules on one act, or two bounds on one unit, need the occasions carved apart on both sides before either prover will accept that they never meet.There is no asymmetry, and that is the correction. If you write your exceptions the natural way, general rule flat and exception scoped, every pair refuses to build until you scope the general side too. The refusal is the feature: the language never had a rule that the more specific line wins, so an exception is not a rule placed near its default, it is a case carved out of it. It is also why every pair in the file has two witnesses. Each half holds the other in place, and removing either guard is a proven error, from whichever side you remove it.
§5A gate for the ones with no witness
Three of the thirteen 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 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
dist/care.txt carries a guard, so open-rules.txt is empty, and the policy it states is that nothing in this prompt reaches every customer. Each of the three silent removals adds a row the empty list does not contain, and the gate refuses. Every guard the gate is not watching is one the prover is, so the two 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 empty list above is what the first clean build produces, which means the list starts out true and every line anybody adds to it later is a deliberate edit they have to explain. An empty file is an unusual policy statement, but it is the right one for a prompt where every rule was scoped on purpose, and a gate that refuses the first unconditional rule to arrive is the gate you want on it.
§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, 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?
- Yes, both sides, whichever prover is looking. A flat rule applies in every case, including the one its exception names, so
You MUST NOT offer a creditnext toWHEN an outage lasted more than 24 hours THEN you MUST offer a creditis 2 blockinglogic/contradictionerrors on typeglish 0.10.0, andYou 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 2 blockinglogic/numericerrors. Carve the default out with the complementary guard,UNLESS an outage lasted more than 24 hours THEN you MUST NOT offer a credit, and the pair checks clean, because the two guards can never hold at once. Earlier checkers proved onlyIFguards, which is why the first version of this post called the one-sided deontic pair clean. It never was. - Is
WHEN the customer asks, do Xthe same asWHEN the customer asks THEN do X? - No. A comma is not a
THEN. Written with a comma, the credit exception is not a rule to the compiler at all: the check stays at 0 error but reportstypeglish/if-then, WHEN needs a THEN, andprompt/unparsed-rule, the condition has no THEN branch, so nothing is required when it holds. The line still reaches the model, because the artifact carries it verbatim, but it earns no enforceability credit, gets no measurability check, and the prover has no obligation to weigh against the prohibition. Written withTHEN, the identical pair is clean and both rules are on the ledger. If you keep the info-level findings in your prompt reviews, those two are not nits.
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 we cut were never paying for it.