← Tidelines/Deep dives

Your bound is for the rule you have not written yet

Five rules, five numbers, and one deletion each. Nothing you can run on the resulting file tells you which one you broke. The bill arrives on a Tuesday in eight months, in somebody else's pull request.

by TypeGlish team9 min read#deep-dives
Same grade. No proof left.

TL;DR Swapping a bound for the word it stood in for costs nothing on any gate you run today: all five de-quantified variants of a clean support prompt check at 0 error, 0 warning, 0 info, and three of them score an identical A (100/100) down to the facet. What a number actually buys is a slot a second statement can land in, so the measurement is a later edit: with the bounds in place the same five follow-up rules are 16 blocking errors, and with the bounds gone four of the five ship in silence. The gate is to write the rule you are afraid of and require the check to fail.

Everybody who writes agent prompts for a living has been told to put numbers in them. At most 3 sentences, not short. Within 30 seconds, not promptly. It is good advice and this blog has given it more than once. What nobody says out loud is what the number is actually doing, and the honest answer turns out to be almost nothing on the file in front of you.

So we measured it. One clean broadband support prompt, five quantified rules, and five variants: each one keeps four of the bounds and replaces the fifth with the word a human would have written instead. Leave one out, re-check, re-score, and read the columns.

§1Five rules, five numbers

The baseline is deliberately boring. It is the prompt you get when somebody has read the guides: every rule names a doer, carries a modal, and ends in a quantity. Nothing in it is clever.

support.tg - the baseline✓ A (100/100)
# Role
@@ role: first-line support for a broadband provider
You are a support agent for Northwind Broadband.

# Constraints
@@ brevity: three sentences keeps a chat reply scannable
- You MUST keep every reply to at most 3 sentences.
@@ one_ask: a caller answers one question at a time
- You MUST ask at most 1 question in each reply.
@@ ack: a slow first line reads as an empty room
- You MUST send a first reply within 30 seconds.
@@ goodwill: the desk settles small credits without a manager
- You MUST refund at most 20 pounds without supervisor approval.
@@ retries: a third restart is a fault, not a fix
- You MUST ask a customer to restart their router at most 2 times.
Five different kinds of quantity on purpose: a sentence count, an action count, a duration, a money amount and a repetition count. Each one lives on a different layer of the checker, and the ablation is about to show that they behave differently.
tg check + tg score support.tg - output
$ npx typeglish check support.tg
 1 file — 0 error, 0 warning, 0 info

$ npx typeglish score support.tg
support.tg — A (100/100)  proven errors: none  tiers: base+z3
  planes  runtime 99 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 95 x.08 · consistency 100 x.17
          structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
A perfect card, which makes it a useful baseline: every number the ablation moves has somewhere to fall.

§2Take the number out

Five variants, in the directory bare/. Each one is support.tg with exactly one bound replaced by the phrase that bound was standing in for, written the way a person writes it in a hurry. At most 3 sentences becomes short. At most 1 question in each reply becomes one question at a time. Within 30 seconds becomes promptly. At most 20 pounds becomes a small amount. At most 2 times becomes a couple of times. No other edits.

tg check bare/ - five de-quantified variants
$ npx typeglish check bare
 5 files — 0 error, 0 warning, 0 info
program: 5 independent files — no $IMPORT compositions

$ npx typeglish check bare --strict
 5 files — 0 error, 0 warning, 0 info
program: 5 independent files — no $IMPORT compositions
Not a warning. Not an info. --strict, which exists to escalate exactly the correctness advisories a normal run soft-pedals, escalates nothing, because there is nothing to escalate.

Which leaves the score, and the score is where you would expect the loss to show, because enforceability is the facet whose own lever text says the words concrete bounds.

tg score bare/*.tg - one line each
bare/ack.tg — A (100/100)  proven errors: none  tiers: base+z3
bare/brevity.tg — A (100/100)  proven errors: none  tiers: base+z3
bare/goodwill.tg — A (98/100)  proven errors: none  tiers: base+z3
bare/one-ask.tg — A (100/100)  proven errors: none  tiers: base+z3
bare/retries.tg — A (98/100)  proven errors: none  tiers: base+z3
Three of five land on the baseline exactly. Not near it: bare/brevity.tg, the file whose only rule about length now reads You MUST keep every reply short., returns runtime 99, hygiene 100, enforceability 100, hardness 100, directness 95, consistency 100, which is the baseline card copied out.

The two that move, move by two points, and only one of them says why.

tg score bare/goodwill.tg - the only file that names it
bare/goodwill.tg — A (98/100)  proven errors: none  tiers: base+z3
  planes  runtime 98 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 94 x.21 · hardness 100 x.12 · directness 95 x.08 · consistency 100 x.17
          structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  lever   enforceability 94/100 (up to +1 overall) — Write rules as MUST / NEVER <verb> … with
          concrete bounds ("at most 3 sentences"), not vague qualities — a rule the checker can
          defend.
The lever text is literally the advice this post opened with, and it is worth +1 overall on a file that just lost its goodwill cap. score --min B passes. score --min A passes. There is no floor anybody sets in CI that separates these five files from the baseline.
A grade is a property of one document. A bound is a promise about the next one.

This is a different flat line from the one in the deontic-force ablation, where softening a rule at least moved hardness and pulled the rule out of the population being measured. Here nothing leaves the population. The rule still has a subject, a modal and an object; it still counts as a hard rule; it is still graded. It just stopped saying how many.

§3Which words survive the deletion

Before drawing conclusions, one correction to the setup. Vague is not the property that matters, and two of the five substitutions were not vague to the compiler at all. The clock is the clearest case, because the delay adverbs are typed time.

tg check clock/ - six words against "within 5 minutes"
clock/promptly.tg:5:1  error  logic/time-strength  Conflicts with line 5. One bound per slot — "promptly: You MUST send a first reply" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.
clock/promptly.tg:6:1  error  logic/time-strength  Conflicts with line 4. One bound per slot — "promptly: You MUST send a first reply" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.
clock/straight-away.tg:5:1  error  logic/time-strength  Conflicts with line 5. One bound per slot — "straight away: You MUST send a first reply" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.
clock/straight-away.tg:6:1  error  logic/time-strength  Conflicts with line 4. One bound per slot — "straight away: You MUST send a first reply" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.
clock/without-delay.tg:5:1  error  logic/time-strength  Conflicts with line 5. One bound per slot — "without delay: You MUST send a first reply" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.
clock/without-delay.tg:6:1  error  logic/time-strength  Conflicts with line 4. One bound per slot — "without delay: You MUST send a first reply" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.

 6 files — 6 error, 0 warning, 0 info
program: 6 independent files — no $IMPORT compositions
Six files, each one You MUST send a first reply <word>. beside You MUST send a first reply within 5 minutes. Three report. The three that are silent are quickly.tg, swiftly.tg and asap.tg, the last of which says as soon as possible, which is a synonym of without delay in every SLA document ever written and is not one here.

So promptly is a duration. It lowers to an interval, it occupies the reply-time slot, and it entails a five-minute deadline the way within 30 seconds does. The word looked like the vaguest thing in the ablation and it was the only substitution that kept its rule in the solver.

The same test over counts, repetitions and money draws the boundary in the other three layers.

tg check words/ - eight substitutions, three read
words/a-single.tg:5:1  error  logic/action-count  Conflicts with line 5. One action, two counts — "ask a single question" but "ask at least 2 questions". A directive carries ONE count for "ask questions in each reply" — state how many once.
words/a-single.tg:6:1  error  logic/action-count  Conflicts with line 4. One action, two counts — "ask a single question" but "ask at least 2 questions". A directive carries ONE count for "ask questions in each reply" — state how many once.
words/once.tg:5:1  error  logic/action-count  Conflicts with line 5. One action, two counts — "ask one question" but "ask at least 2 questions". A directive carries ONE count for "ask questions in each reply" — state how many once.
words/once.tg:6:1  error  logic/action-count  Conflicts with line 4. One action, two counts — "ask one question" but "ask at least 2 questions". A directive carries ONE count for "ask questions in each reply" — state how many once.
words/under-20.tg:5:1  error  logic/numeric  Conflicts with line 5. Numeric conflict — "under 20 pounds" and "at least 50 pounds" can't both hold.
words/under-20.tg:6:1  error  logic/numeric  Conflicts with line 4. Numeric conflict — "under 20 pounds" and "at least 50 pounds" can't both hold.

 8 files — 6 error, 0 warning, 0 info
program: 8 independent files — no $IMPORT compositions
The five silent files: one-at-a-time.tg (ask one question at a time), twice.tg, a-couple.tg, a-small-amount.tg and brief.tg. Each is paired against the same opposing rule as its neighbour that reported.

Read the first and third rows together, because they are one word apart in English and a whole plane apart here. You MUST ask one question in each reply is a count and collides with at least 2 questions. You MUST ask one question at a time is silent. The three added words are the ones a human adds to make the rule clearer, and they move the action into a frame the counting layer does not join, which is the same mechanism that decides whether an exception narrows a rule or moves it.

  • The boundary is the lexicon, not the precision. Once and a single are counts. Twice is not. Promptly is a duration. Quickly is not. Under 20 pounds is money. A small amount is not. You cannot derive any of those from how exact the phrase feels.
  • Adjectives on a quality are the reliable loss. Brief, short and small are legible words on named axes, which is why they cost nothing on the scorecard, and they are not quantities, which is why they cost the whole proof.
  • This is not the frequency square. The rules that say usually tank enforceability to 28 and light up the lever line, because a frequency adverb is a quantity claim about occasions and the compiler has a full opposition square for it. A degree adjective has no square. It is a claim on an axis, and nothing on that axis contradicts a number.

§4The edit eight months later

So the ablation needs a second column, and the second column is not a property of the file. It is a property of what the file can still refuse.

Take the same five rules and, for each one, write the follow-up edit a real team ships: the one that quietly widens the policy. Marketing wants longer answers. Discovery wants two questions. Ops relaxes the acknowledgement SLA. Retention wants bigger goodwill. Field engineering wants a third restart before an engineer visit. Append one to each variant and run the directory twice, once with the bounds and once without.

tg check later/kept - the bounds are still in the file✗ 16 error
later/kept/ack.tg:11:1  error  logic/time-strength  Conflicts with line 9. One bound per slot — "within 30 seconds" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.
later/kept/ack.tg:17:1  error  logic/time-strength  Conflicts with line 6. One bound per slot — "within 30 seconds" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.
later/kept/brevity.tg:7:1  error  logic/contradiction  Conflicts with line 9. Counted rule — "keep every reply to at most 3 sentences." vs "keep every reply to at least 6 sentences." is bound to at most 3 but at least 6 per reply. The counts cannot both hold.
later/kept/brevity.tg:7:1  error  logic/numeric  Conflicts with line 9. Numeric conflict — "at most 3 sentences" and "at least 6 sentences" can't both hold.
later/kept/brevity.tg:17:1  error  logic/contradiction  Conflicts with line 4. Counted rule — "keep every reply to at most 3 sentences." vs "keep every reply to at least 6 sentences." is bound to at most 3 but at least 6 per reply. The counts cannot both hold.
later/kept/brevity.tg:17:1  error  logic/numeric  Conflicts with line 4. Numeric conflict — "at most 3 sentences" and "at least 6 sentences" can't both hold.
later/kept/goodwill.tg:13:1  error  logic/numeric  Conflicts with line 9. Numeric conflict — "at most 20 pounds" and "at least 50 pounds" can't both hold.
later/kept/goodwill.tg:17:1  error  logic/numeric  Conflicts with line 7. Numeric conflict — "at most 20 pounds" and "at least 50 pounds" can't both hold.
later/kept/one-ask.tg:9:1  error  logic/action-count  Conflicts with line 9. One action, two counts — "ask at most 1 question" but "ask at least 2 questions". A directive carries ONE count for "ask questions in each reply" — state how many once.
later/kept/one-ask.tg:9:1  error  logic/numeric  Conflicts with line 9. Numeric conflict — "at most 1 question" and "at least 2 questions" can't both hold.
later/kept/one-ask.tg:17:1  error  logic/action-count  Conflicts with line 5. One action, two counts — "ask at most 1 question" but "ask at least 2 questions". A directive carries ONE count for "ask questions in each reply" — state how many once.
later/kept/one-ask.tg:17:1  error  logic/numeric  Conflicts with line 5. Numeric conflict — "at most 1 question" and "at least 2 questions" can't both hold.
later/kept/retries.tg:15:1  error  logic/contradiction  Conflicts with line 9. Counted rule — "ask a customer to restart their router at most 2 times." vs "ask a customer to restart their router at least 3 times." is bound to at most 2 but at least 3. The counts cannot both hold.
later/kept/retries.tg:15:1  error  logic/numeric  Conflicts with line 9. Numeric conflict — "at most 2 times" and "at least 3 times" can't both hold.
later/kept/retries.tg:17:1  error  logic/contradiction  Conflicts with line 8. Counted rule — "ask a customer to restart their router at most 2 times." vs "ask a customer to restart their router at least 3 times." is bound to at most 2 but at least 3. The counts cannot both hold.
later/kept/retries.tg:17:1  error  logic/numeric  Conflicts with line 8. Numeric conflict — "at most 2 times" and "at least 3 times" can't both hold.

 5 files — 16 error, 0 warning, 0 info
program: 5 independent files — no $IMPORT compositions
Five files, five refusals, no build. Every conflict reports once per participating line, and the three counted rules report on two codes at once, which is why five edits produce sixteen errors. The duration edit is a logic/time-strength rather than a contradiction, because five minutes is not impossible beside thirty seconds, it is dead weight.
tg check later/gone - the same five edits, one bound missing each✗ 2 error
later/gone/ack.tg:11:1  error  logic/time-strength  Conflicts with line 9. One bound per slot — "promptly: You MUST send a first reply" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.
later/gone/ack.tg:17:1  error  logic/time-strength  Conflicts with line 6. One bound per slot — "promptly: You MUST send a first reply" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.

 5 files — 2 error, 0 warning, 0 info
program: 5 independent files — no $IMPORT compositions
Four of the five widening edits are now invisible. The survivor is ack.tg, and it survives for the reason §3 gave: the word that replaced the bound was itself a bound. Every file in both runs scored an A before the edit landed.

That is the whole measurement. 16 errors against 2, on files that were indistinguishable an hour earlier. The bound was never protecting the rule it was attached to. It was reserving a slot, so that anything else written about the same action has to be reconciled with it before the build succeeds. Delete it and you have not made the rule weaker; you have removed it from the argument.

§5Write down the rule you are afraid of

Which means the gate cannot read the current file. It has to read a file that does not exist yet, so you have to write one. Keep, beside the prompt, one opposing rule per bound you care about, append each to a copy of the prompt, and require the check to fail. A fixture that passes is a bound that is not in the solver.

bounds-gate.sh - the compile-fail fixture as a loop
#!/bin/sh
# Every bound in the prompt has to refuse its own opposite.
fail=0
while IFS='|' read -r name opposite; do
  fixture=$(mktemp -d)/fixture.tg
  cat "$1" > "$fixture"
  printf '%s\n' "$opposite" >> "$fixture"
  if npx typeglish check "$fixture" >/dev/null 2>&1; then
    echo "UNGUARDED  $name"
    fail=1
  else
    echo "guarded    $name"
  fi
done < bounds.txt
exit $fail
bounds.txt is five lines, one per bound, each holding a name and the rule that must not be allowed to coexist with it: at least 6 sentences, at least 2 questions, within 5 minutes, at least 50 pounds, and restart their router at least 3 times. The fixture is generated and thrown away, so nothing in the repository ever contains a policy you do not hold.
./bounds-gate.sh - two prompts, one word apart
$ ./bounds-gate.sh support.tg
guarded    brevity
guarded    one_ask
guarded    ack
guarded    goodwill
guarded    retries
exit 0

$ ./bounds-gate.sh bare/brevity.tg
UNGUARDED  brevity
guarded    one_ask
guarded    ack
guarded    goodwill
guarded    retries
exit 1
The second file is the one that scored A (100/100) with the baseline's facets, and this is the only instrument in the toolchain that tells them apart. It is a compile-fail test, the same shape a type-checked codebase uses to prove an invalid program is rejected.

There is a tidier spelling that is worth knowing about and worth not trusting all the way. Instead of generating a copy, make each fixture a three-line file that imports the real prompt and adds the opposing rule, then check the fixture directory as a program.

tg check bounds/ - the $IMPORT fixture, four of five
bounds/ack.tg:7:1  error  logic/time-strength  Conflicts with ../support.tg line 6. One bound per slot — "within 30 seconds" already entails "within 5 minutes"; the looser rule on "you send a first · respond" is dead weight. Keep one.
bounds/brevity.tg:7:1  error  logic/contradiction  Conflicts with ../support.tg line 4. Counted rule — "keep every reply to at most 3 sentences." vs "keep every reply to at least 6 sentences." is bound to at most 3 but at least 6 per reply. The counts cannot both hold.
bounds/one-ask.tg:7:1  error  logic/action-count  Conflicts with ../support.tg line 5. One action, two counts — "ask at most 1 question" but "ask at least 2 questions". A directive carries ONE count for "ask questions in each reply" — state how many once.
bounds/retries.tg:7:1  error  logic/contradiction  Conflicts with ../support.tg line 8. Counted rule — "ask a customer to restart their router at most 2 times." vs "ask a customer to restart their router at least 3 times." is bound to at most 2 but at least 3. The counts cannot both hold.

 5 files — 4 error, 0 warning, 0 info
program: 5 roots — bounds/ack.tg (2 files), bounds/brevity.tg (2 files), bounds/goodwill.tg (2 files), bounds/one-ask.tg (2 files), bounds/retries.tg (2 files)
Five roots, four findings. bounds/goodwill.tg is silent, because the money bound is the one whose only proof was logic/numeric, and numeric bounds do not compose across an $IMPORT. The counted rules survive the file boundary; the Z3 arithmetic does not. If you use this shape, know that it is a four-fifths gate on this fileset and that the fifth is the one about money.

Then, for the bounds a regex can see, write the second copy of the number where a running model can be caught breaking it. A sentence cap is one of the deterministic asserts, so it costs no API key and no judge. Appended to support.tg, the whole addition is a five-line $TEST block and the file still checks at 0 error, 0 warning, 0 info and scores A (100/100).

support.tg - the two asserts, appended
$TEST brevity_bound
  - input:: My broadband keeps dropping every evening and I have already rebooted the router twice. What is going on?
  - expect::
    - at most 3 sentences
    - matches /^[^?]*\?[^?]*$/
tg test support.tg --dry - output
$ npx typeglish test support.tg --dry
 support.tg  coverage: 1/6 rules exercised
  · brevity_bound — "My broadband keeps dropping every evening and I have already" (not run)
       at most 3 sentences
       matches /^[^?]*\?[^?]*$/
 1 prompt — 0 failed
Two asserts, both settled offline: at most three sentences, and exactly one question mark. coverage: 1/6 is honest about the rest, since the duration, the money cap and the restart count have no shape a regex can hold. Those three are what the fixture directory is for.

§6Common questions

Do I really need numbers in my agent's rules if the checker never complains about the vague version?
Yes, and the reason is not the report you get today. Replacing at most 3 sentences with short leaves the file at 0 error, 0 warning, 0 info and at an identical A (100/100), so nothing you run on the current file will tell you the number is gone. What the number buys is a slot the prover can put a second statement into. With the bound, a rule added later saying at least 6 sentences is four blocking errors and no build. With the word short in its place, the same edit is 0 error, 0 warning, 0 info and ships.
Why does the TypeGlish score not drop when I write keep replies short?
Because short is a legible quality on a named axis and the enforceability facet is not a digit counter. Across five single-rule de-quantifications of one clean prompt, three came back at A (100/100) with byte-identical facets and two came back at A (98/100) with enforceability 94, which is the only row where the lever line mentions concrete bounds at all. Both grades pass score --min B and score --min A, so no shipped gate separates the bounded file from the vague one.
Which vague words does TypeGlish still read as a bound?
The ones that are quantities rather than impressions, and the list is not the one intuition predicts. On the clock, promptly, without delay and straight away are all read as a duration and conflict with within 5 minutes, while quickly, swiftly and as soon as possible are silent. On counts, ask one question and ask a single question are counts and conflict with at least 2 questions, while ask one question at a time and twice are silent. On money, under 20 pounds is read and a small amount is not. There is no way to predict a row: append the opposing rule and see.
How do I prove a bound in my system prompt is actually enforced?
Write the rule you are afraid of and require the check to fail. Keep a list of one opposing rule per bound, append each to a copy of the prompt at CI time, and run typeglish check on the copy: a fixture that passes means that bound is not in the solver. On a five-bound prompt the loop prints guarded five times and exits 0, and swapping one bound for its vague version turns exactly one row into UNGUARDED and the exit code into 1. The $IMPORT file version of the same fixture is tidier and catches four of the five, because the Z3 numeric plane does not cross a file boundary.
Field note

The uncomfortable reading of this ablation is that the number in a rule is doing two jobs and only one of them is yours. It tells the model how to behave, which is the job you wrote it for, and it registers a claim on a slot, which is the job that makes the file checkable. The model job survives short perfectly well: a competent reader keeps replies short. The slot job does not survive it at all, and the slot job is the only one a compiler can help with. So the practical rule is smaller than always use numbers. It is this: for every quantity your policy actually owns, there exists a rule somebody could add next quarter that would break it, and you either wrote that rule down as a fixture or you did not. Five of five, or four of five over an import, against zero if the bound is a word.

∿ washed up Aug 17, 2026 ∿