← Tidelines/Deep dives

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.

by TypeGlish team9 min read#deep-dives
Six flinched. Three did not.

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.

care.tg - the baseline✓ A (92/100)
<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>
Thirteen statements, nine of them guarded. Nothing here is clever and nothing here is wrong: this is what a reviewed contact-centre prompt looks like after somebody has already been through it once.
tg check + tg score care.tg - output
$ 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).
Consistency 100 is the number to hold on to. Four pairs of rules in this file are flat contradictions with a conditional between them, and the prover is satisfied, which is exactly what a guard is for.

§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.

tg check strip/ - one guard missing each✗ 12 error
$ 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
Six files, twelve errors, two per file, one on each side of the pair. 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.

tg score strip/*.tg - one line each
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
There is no middle. Six removals cap the grade at F on a file that has changed by six words; 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.

diff dist/care.txt dist/strip/*.txt
=== 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.
Read the middle one as a data-protection officer rather than as an engineer. The prompt now instructs the agent to read an account balance aloud, unconditionally, to whoever is on the line. It checks at 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.

tg score - baseline vs. the unguarded copy
$ 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
Eight facets, one of them moves, and it moves the wrong way: 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.

tg test --dry - the same suite on both files
$ 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.

comma.tg - a conditional with no THEN✗ 2 error
<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>
One character different from the pair in §1, which is clean.
tg check comma.tg - output
$ 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
The 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.

onesided.tg - one bound scoped, one not✗ 2 error
<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>
Two blocking logic/numeric errors. The guard is present, correctly spelled, and does not clear the conflict.
twosided.tg - both bounds scoped✓ 0 error
<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>
Adding 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.

scope-gate.mjs - fourteen lines
// 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);
It runs on the artifact, not the source, which is the point: by then the conditionals a $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 - the baseline, then the three silent variants
$ 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
Four listed rules on the baseline and exit 0. Each of the three silent removals adds a row the list does not contain, and the gate refuses. The four 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 WHEN clause 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/contradiction or logic/numeric) and three produced 0 error, 0 warning, 0 info at A (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 aloud leaves You 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, style and security are all unmoved, and directness goes 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 credit next to WHEN an outage lasted more than 24 hours THEN you MUST offer a credit is 0 error, 0 warning, 0 info. For a numeric conflict, one guard is not enough: You MUST keep every reply to at most 3 sentences next to WHEN a customer asks for a fault history THEN you MUST write at least 8 sentences is still 2 blocking logic/numeric errors. Scope the general side too, with UNLESS, and the same pair checks clean.
Is WHEN the customer asks, do X the same as WHEN 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 2 logic/contradiction errors against the general prohibition plus an info typeglish/if-then reading WHEN needs a THEN, because the line never became a conditional and the whole sentence is read as an unconditional obligation. Written with THEN, the identical pair is clean. If you keep the info-level typeglish/if-then findings in your prompt reviews, that one is not a nit.
Field note

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.

∿ washed up Aug 27, 2026 ∿