← Tidelines/Best practices

Bake-off: 12 months vs. 1 year

A water company's payments agent caps every plan at a year. Two copies of its prompt, identical except for how that year is spelled, then five edits from the teams who want longer plans. The checker referees.

by TypeGlish team8 min read#best-practices
One cap, two spellings.

TL;DR TypeGlish's numeric check compares a ceiling with a floor only when they share a unit word, so at most 12 months refuses a later at least 24 months and ships a later at least 2 years. Write every duration of one kind in the smallest unit the policy needs, usually months for payment plans, and pin the cap with a $TEST.

An arrears desk writes a payment plan's length in whatever unit the document in front of it uses: 12 months in the handbook, a year in the help-centre article, 52 weeks in the collections script. To the model reading the prompt they are the same plan. This bake-off measures whether they are the same plan to the checker, which is the part that notices when a later edit asks for more.

§1The contenders

A chat assistant for a water company's payments line. It looks up the account, keeps replies short, sends anyone who cannot pay at all to a person, and caps every payment plan at a year, because a longer plan needs an arrears specialist to sign it off. Two copies of that prompt go into the ring. They are byte for byte the same except for line 16: one writes the cap as at most 12 months, the other as at most 1 year.

months.tg - the payments agent, cap in months✓ A (99/100)
$TOOL find_account
  - description:: Find a customer account by its account number.
  - input::
    - account_number:: string: the account number.

# Role
@@ role: one chat assistant for a water company's payments line
You are Wren, the payments assistant for Brookmere Water.

# Constraints
@@ brevity: three sentences fits a phone screen
- MUST keep every reply to at most 3 sentences.
@@ lookup: never quote a balance from memory
- WHEN the customer gives an account number THEN MUST call @[find_account].
@@ plan_cap: a longer plan needs an arrears specialist to sign it off
- MUST keep every payment plan to at most 12 months.
@@ hardship: a customer who cannot pay at all needs a person
- WHEN the customer says they cannot afford any payment THEN MUST escalate to an arrears specialist.
tg check and score - both contenders
$ npx typeglish check months.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ npx typeglish score months.tg
months.tg — A (99/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 91 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   4 of 4 rule-shaped lines read

$ npx typeglish check year.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ npx typeglish score year.tg
year.tg — A (99/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 91 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   4 of 4 rule-shaped lines read

$ diff months.tg year.tg
16c16
< - MUST keep every payment plan to at most 12 months.
---
> - MUST keep every payment plan to at most 1 year.
Same grade, same facets, four of four rules read in each. On every signal the checker gives today, these are the same prompt. The difference only shows up when somebody edits them.

§2Five edits, two files

Then the edits arrive, each from a team with a good reason. The vulnerability team wants customers on benefits to get smaller instalments, so their plan should run longer. Three people on that team could write the same request three ways: at least 18 months, at least 24 months, at least 2 years. The collections team wants weekly plans for customers paid weekly: at least 60 weeks. And somebody splits the difference: at least 1.5 years. Every edit is a guarded WHEN ... THEN MUST offer a payment plan of ... with its own @@ note, and every one asks for more than a 12-month cap allows. Ten files: five edits, two contenders.

the results - five edits against each contender✗ 2 of 5 vs 1 of 5
# edit (each guarded, each with its own @@ note)        months.tg              year.tg
offer a payment plan of at least 18 months              logic/numeric F (83)   passes A (99)
offer a payment plan of at least 24 months              logic/numeric F (83)   passes A (99)
offer a payment plan of at least 2 years                passes A (99)         logic/numeric F (83)
offer a payment plan of at least 60 weeks               passes A (99)         passes A (99)
offer a payment plan of at least 1.5 years              passes A (98)         passes A (98)
# caught                                                2 of 5                 1 of 5
"Passes" means check --strict reports 0 error, 0 warning, 0 info. Every caught row is a blocking logic/numeric error with the grade capped at F and the build refused. In the passing rows, the grade with the contradiction in the file is the grade without it, give or take a point.

Neither contender catches everything. The months file refuses the two edits written in months and ships the one written in years. The year file refuses the edit written in years and ships the two written in months. Weeks and a decimal year get past both.

The pattern is not arithmetic. The checker is not converting a year into twelve months and comparing; it is comparing a number to a number when the word after them is the same word. 12 months and 24 months share a unit, so the pair is a proven impossibility. 12 months and 2 years do not, so there is nothing to prove.

§3The edit that shipped

Take the months file and the vulnerability team's third phrasing. The arrears policy letter says customers on benefits can spread their debt over two years, so that is what the edit says. The two files below differ only in how the original cap was spelled.

tg check and build - the same edit against both contenders✗ one refused, one shipped
$ diff months-plus-edit.tg year-plus-edit.tg
16c16
< - MUST keep every payment plan to at most 12 months.
---
> - MUST keep every payment plan to at most 1 year.

$ npx typeglish check year-plus-edit.tg
year-plus-edit.tg:16:1  error  logic/numeric  Conflicts with line 18. Numeric conflict — "at most 1 year" and "at least 2 years" can't both hold.
                   ↳ line 18: - WHEN the customer receives benefits THEN MUST offer a payment plan of at least 2 years.
year-plus-edit.tg:18:1  error  logic/numeric  Conflicts with line 16. Numeric conflict — "at most 1 year" and "at least 2 years" can't both hold.
                   ↳ line 16: - MUST keep every payment plan to at most 1 year.

✗ 1 file — 2 error, 0 warning, 0 info

$ npx typeglish check months-plus-edit.tg --strict
✓ 1 file — 0 error, 0 warning, 0 info

$ npx typeglish build months-plus-edit.tg
✓ built .typeglish/dist/months-plus-edit.txt ← months-plus-edit.tg (9f48d4a80b81, full)

$ grep -n "payment plan" .typeglish/dist/months-plus-edit.txt
7:- MUST keep every payment plan to at most 12 months.
8:- WHEN the customer receives benefits THEN MUST offer a payment plan of at least 2 years.
The shipped artifact puts both lines next to each other: plans of at most 12 months, and for customers on benefits, plans of at least 2 years. The model resolves that one chat at a time, which is the failure in Your prompt argues with itself. Whichever it picks, somebody's policy loses.

Stripped to the two rules, the effect fits in the playground. Same cap, same request, the request spelled two ways.

pair-months.tg - 12 months against 2 years✓ clean, B (87/100)
# Role
You are Wren, the payments assistant for Brookmere Water.

# Constraints
- MUST keep every payment plan to at most 12 months.
- WHEN the customer receives benefits THEN MUST offer a payment plan of at least 2 years.
pair-months24.tg - 12 months against 24 months✗ logic/numeric
# Role
You are Wren, the payments assistant for Brookmere Water.

# Constraints
- MUST keep every payment plan to at most 12 months.
- WHEN the customer receives benefits THEN MUST offer a payment plan of at least 24 months.
The first checks with 0 error, 0 warning, 0 info at B (87/100); the B is the missing @@ notes, not the conflict. The second is refused at F (70/100). To anyone reading them, the two files ask for the same plan.

§4Where units do cross

There is one place the checker does compare across units. TypeGlish has a second layer for durations, the time layer, and it reads bounds on the same verb and object as one slot. Change the vulnerability team's edit from offer a payment plan to keep every payment plan, the cap's own wording, and the year-against-months pair collides.

tg check - same units, the verb changed to match the cap✗ logic/time
$ diff months-plus-edit.tg months-keep.tg
18c18
< - WHEN the customer receives benefits THEN MUST offer a payment plan of at least 2 years.
---
> - WHEN the customer receives benefits THEN MUST keep every payment plan to at least 2 years.

$ npx typeglish check months-keep.tg
months-keep.tg:16:1  error  logic/time  Conflicts with line 18. Time conflict — "at most 12 months" and "at least 2 years" cannot both bound "you keep every payment · plan": the windows never intersect.
                ↳ line 18: - WHEN the customer receives benefits THEN MUST keep every payment plan to at least 2 years.
months-keep.tg:18:1  error  logic/time  Conflicts with line 16. Time conflict — "at most 12 months" and "at least 2 years" cannot both bound "you keep every payment · plan": the windows never intersect.
                ↳ line 16: - MUST keep every payment plan to at most 12 months.

✗ 1 file — 2 error, 0 warning, 0 info
Same request, same mismatch of units, one verb changed. Both rules now bound one slot, you keep every payment plan, and the time layer reports that at most 12 months and at least 2 years never intersect. Across a different verb, only the numeric layer looks, and it needs the same unit word.

That is real, and it is not a plan. Nobody adding a hardship exception copies the cap's verb. They write offer, set up, agree, in whatever unit their own document used. A defence that only works when the next author happens to reuse your wording is not a defence you can rely on.

Twelve months and one year are the same plan. They are not the same word.

§5Verdict: months

Months wins, for three reasons that all come down to which edits each file can hear.

First, the values in between exist only in months. Any length from 13 to 23 months can be written in months and can only be written in years with a decimal, and in this run 1.5 years was compared with nothing. A cap written as 1 year can only collide with an edit asking for whole years; one asking for 18 months walks past it. A cap in months collides with every edit written in months, which is how arrears policies, instalment counts and plan tools tend to count. Second, a plan tool takes its length as a whole number of months, as the one in fixed.tg below does, so a cap in months agrees with the tool on the unit. Third, one house unit makes the audit a search: grep the rules for year and week, and every hit is a duration the numeric check will not compare with your cap.

The verdict also forces the decision the refused build was asking for. The vulnerability team was right: the arrears policy does allow customers on benefits up to 24 months. So the cap gets an UNLESS, the exception gets its own ceiling in the same unit and the same verb, the tool limits its input to match, and two tests pin both ceilings to the reply. The asserts are deterministic and spell the forbidden lengths in months and years, because the reply is where the model will mix them, the approach in $40 and 40 dollars are not the same rule.

fixed.tg - one house unit, the exception scoped, both ceilings tested✓ A (99/100)
$TOOL find_account
  - description:: Find a customer account by its account number.
  - input::
    - account_number:: string: the account number.

$TOOL set_up_plan
  - description:: Set up a payment plan on the customer's account.
  - input::
    - account_number:: string: the account number.
    - months:: integer between 1 and 24: the plan length in months.

# Role
@@ role: one chat assistant for a water company's payments line
You are Wren, the payments assistant for Brookmere Water.

# Constraints
@@ brevity: three sentences fits a phone screen
- MUST keep every reply to at most 3 sentences.
@@ lookup: never quote a balance from memory
- WHEN the customer gives an account number THEN MUST call @[find_account].
@@ plan_cap: house unit is months; a longer plan needs an arrears specialist to sign it off
- UNLESS the customer receives benefits THEN MUST keep every payment plan to at most 12 months.
@@ benefits_cap: a customer on benefits needs smaller instalments, so the arrears policy allows twice as long
- WHEN the customer receives benefits THEN MUST keep every payment plan to at most 24 months.
@@ plan_tool: the plan exists only once the tool has set it up
- WHEN the customer agrees to a payment plan THEN MUST call @[set_up_plan].
@@ hardship: a customer who cannot pay at all needs a person
- WHEN the customer says they cannot afford any payment THEN MUST escalate to an arrears specialist.

$TEST standard_plan
  - input:: I owe 900 pounds and I can only pay 50 pounds a month. What can you do?
  - expect::
    - matches /^(?![\s\S]*(\b([2-9]|two|three|four|five)[ -]years?\b|\b(1[3-9]|[2-9][0-9])[ -]months?\b))/i
    - keeps every payment plan to at most 12 months

$TEST benefits_plan
  - input:: I'm on benefits, I owe 900 pounds and I can only manage 20 pounds a month.
  - expect::
    - matches /^(?![\s\S]*(\b([3-9]|three|four|five)[ -]years?\b|\b(2[5-9]|[3-9][0-9])[ -]months?\b))/i
    - keeps every payment plan to at most 24 months
tg check, score, test --dry and build - fixed.tg
$ npx typeglish check fixed.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ npx typeglish score fixed.tg
fixed.tg — A (99/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 89 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   6 of 6 rule-shaped lines read · 3/6 covered by $TEST

$ npx typeglish test fixed.tg --dry
✓ fixed.tg  coverage: 3/6 rules exercised
  uncovered (no case shares the rule's wording):
    L20  call @[find_account].
    L26  call @[set_up_plan].
    L28  escalate to an arrears specialist.
  · standard_plan — "I owe 900 pounds and I can only pay 50 pounds a month. What " (not run)
      ✓ matches /^(?![\s\S]*(\b([2-9]|two|three|four|five)[ -]years?\b|\b(1[3-9]|[2-9][0-9])[ -]months?\b))/i
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · benefits_plan — "I'm on benefits, I owe 900 pounds and I can only manage 20 p" (not run)
      ✓ matches /^(?![\s\S]*(\b([3-9]|three|four|five)[ -]years?\b|\b(2[5-9]|[3-9][0-9])[ -]months?\b))/i
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed

$ npx typeglish build fixed.tg
✓ built .typeglish/dist/fixed.txt ← fixed.tg (81e9db6af5c2, full)
Against fixed.tg, a later edit asking for a benefits plan of at least 30 months is refused as logic/numeric. The same edit written as at least 3 years still checks clean, which is exactly why the second test bans three years and more in words and digits. A dry run proves the asserts are well formed; a live run checks them against the model. test --dry lists the three rules still uncovered, by line.

This is the companion to A maximum of 3 is not a maximum. That run showed the words before the number decide whether a limit is compared. This one shows the word after it does too.

§6Common questions

Should I write 12 months or 1 year in a system prompt?
To the model they are the same. To the checker they are different units: in this bake-off a cap of at most 12 months caught later edits written in months and missed one written in years, and a cap of at most 1 year did the reverse. Pick the smallest unit your policy needs, usually months for payment plans, and write every duration of that kind in it.
Does TypeGlish convert between months, weeks and years?
The numeric check that compares an at most rule with an at least rule did not in this run: at most 12 months against at least 2 years, at least 60 weeks or at least 1.5 years all passed clean. The time check did compare across units when both rules bounded the same verb and object, keep every payment plan, and reported logic/time.
How do I stop my AI collections agent offering a longer payment plan than policy allows?
Write the cap as a counted ceiling in one house unit, give any exception its own guarded ceiling in the same unit and with the same verb, limit the plan tool's months input to match, and add a $TEST whose deterministic assert fails any reply that names a longer plan in months or years.
Why did my prompt pass the check when two rules disagree about a plan length?
Look at the units. If one rule says months and the other says years or weeks, the numeric check had nothing to compare. Rewrite both in the same unit and run the check again; in this bake-off the conflict then appeared as a blocking logic/numeric error and the build was refused.
Field note

A duration is the easiest number in a CX prompt to spell three ways, because every source document has its own habit: the policy counts months, the help centre says a year, the script says weeks. TypeGlish's numeric layer proves a ceiling against a floor with Z3 when the two share a unit word, and its time layer compares bounds across units when they sit on the same verb and object. Everything else is the quiet case: a file whose durations are never compared looks exactly like a file whose durations agree. Pick one house unit per kind of duration, keep the exception on the cap's own verb, and give the cap a $TEST, so a live run fails when the reply offers the plan the prompt forgot to forbid.

∿ washed up Oct 29, 2026 ∿