We renamed the customer. The contradictions walked through.
The voice team says caller, product says user, compliance says account holder, collections says bill payer. We took a clean energy-supplier agent, gave its customer each of those names in one rule at a time, and then added an edit that contradicts that rule.
TL;DR The contradiction check compares the words of a rule's action, not the person they mean: MUST transfer the caller to a live agent and NEVER transfer the customer to a live agent check clean, grade A (99/100), and build. Across six rules and five names, a rename inside a WHEN or IF condition cost nothing (10 of 10 contradicting edits still caught), and a rename inside the action cost everything (0 of 20 caught). Use one noun for the customer, everywhere.
Every contact centre has more names for the customer than it has teams. The IVR script was written for a caller. The app team's tickets talk about the user. Compliance writes account holder because that is the person the data belongs to, and collections writes bill payer because that is the person who owes the money. When those teams each paste a few lines into the same agent prompt, the model gets four names for the person in the chat, and reads them as one. This run measures whether the checker does too.
§1The baseline
A chat assistant for a household energy supplier: one tool, one length bound, six guarded rules. Every one of the six mentions the customer in its condition, and four of them mention the customer again in the action.
$TOOL find_account - description:: Find an energy account by its account number. - input:: - account_number:: string: the account number. # Role @@ role: one chat assistant for household energy customers You are Ada, the chat support assistant for Fenwick Energy. # Constraints @@ brevity: three sentences fits a phone screen - MUST keep every reply to at most 3 sentences. @@ lookup: never answer about an account from memory - WHEN the customer gives an account number THEN MUST call @[find_account]. @@ gas: a smell of gas is an emergency, not a ticket - WHEN the customer reports a smell of gas THEN MUST give the customer our gas emergency number. @@ arrears: a payment plan comes before any talk of collection - IF the customer is behind on payments THEN MUST offer the customer a payment plan. @@ reading: a misheard digit becomes a wrong bill - WHEN the customer submits a meter reading THEN MUST read the reading back to the customer. @@ security: no balance before verification - IF the customer has not passed security checks THEN NEVER share their account balance. @@ human: a request for a person is always honoured - WHEN the customer asks for a human THEN MUST transfer the customer to a live agent.
$ npx typeglish check base.tg ✓ 1 file — 0 error, 0 warning, 0 info $ npx typeglish score base.tg base.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 7 of 7 rule-shaped lines read $ npx typeglish build base.tg ✓ built .typeglish/dist/base.txt ← base.tg (0b8dedc676f0, full)
§2Thirty renames
Each variant is base.tg with one rule rewritten in another team's vocabulary: every customer on that line becomes caller, user, account holder, bill payer or member. Six rules, five names, thirty files. Nothing else changes.
Then each variant gets the kind of line that arrives later, written by somebody who calls the customer the customer: the plain opposite of the renamed rule, added at the top of the section with its own @@ note. NEVER transfer the customer to a live agent against the human rule, NEVER offer the customer a payment plan against the arrears rule, MUST share their account balance against the security rule, and so on. Against base.tg, each of the six edits is a blocking logic/contradiction and the build is refused.
# rule customer named in renamed alone renamed + contradicting edit lookup condition clean A (99) logic/contradiction F (83) 5 of 5 caught security condition clean A (98-99) logic/contradiction F (81-82) 5 of 5 caught gas condition + action clean A (98-99) passes A (98-99) 0 of 5 caught arrears condition + action clean A (98-99) passes A (98-99) 0 of 5 caught reading condition + action clean A (98-99) passes A (98-99) 0 of 5 caught human condition + action clean A (98-99) passes A (98-99) 0 of 5 caught
prompt/unintroduced-definite, because the account holder, the bill payer or the member is a noun the file never introduced (and on the gas rule, prompt/declared-later once the edit sat above it). Caller and user drew nothing at all, in any position. None of the info lines is about the conflict.No variant changed the grade by more than a point on its own, which is the expected half of the result: a rule about the caller is a perfectly good rule. The other half is the one that matters. Where the new name sat in the condition, all ten contradicting edits were still caught. Where it sat in the action, all twenty shipped, at an A.
§3The overnight edit
Take the human rule, the one that matters most to a customer who is stuck. The voice team wrote it, so it says caller. Months later the overnight shift is cut, and somebody in ops adds one line at the top of the section: nobody is on after 10pm, so the bot should stop promising a person.
@@ overnight: nobody is on shift after 10pm - NEVER transfer the customer to a live agent. ... @@ human: a request for a person is always honoured - WHEN the caller asks for a human THEN MUST transfer the caller to a live agent.
Those two lines cannot both hold for anybody who asks for a human, and the ops line was meant to win only at night, which the file does not say. In base.tg that unresolved decision is refused. In the voice team's version it is not noticed.
$ npx typeglish check base-plus-edit.tg base-plus-edit.tg:12:1 error logic/contradiction Conflicts with line 26. Logical conflict — "transfer the customer to a live agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ↳ line 26: - WHEN the customer asks for a human THEN MUST transfer the customer to a live agent. ... ✗ 1 file — 2 error, 0 warning, 0 info $ npx typeglish build base-plus-edit.tg typeglish build: base-plus-edit.tg refused — nothing written $ npx typeglish check renamed-plus-edit.tg --strict ✓ 1 file — 0 error, 0 warning, 0 info $ npx typeglish review renamed-plus-edit.tg renamed-plus-edit.tg — A (99/100) check: 0 error, 0 warning, 0 info tiers: base+z3 FIXES none — every facet is essentially full RULES 8 of 8 rule-shaped lines read $ npx typeglish build renamed-plus-edit.tg ✓ built .typeglish/dist/renamed-plus-edit.txt ← renamed-plus-edit.tg (a4096c19eced, full)
--strict, keeps the A (99/100) it had before the edit, reads all eight rules as provable, and review has nothing to suggest. The grade, the rules table and the fix list are all silent at once.And here is what ships, both lines of it, a few lines apart in the artifact the model reads:
$ grep -n "live agent" .typeglish/dist/renamed-plus-edit.txt 5:- NEVER transfer the customer to a live agent. 12:- WHEN the caller asks for a human THEN MUST transfer the caller to a live agent.
Stripped down to the two rules, the pair is small enough to run yourself.
# Role You are Ada, the chat support assistant for Fenwick Energy. # Constraints - WHEN the customer asks for a human THEN MUST transfer the customer to a live agent. - NEVER transfer the customer to a live agent.
# Role You are Ada, the chat support assistant for Fenwick Energy. # Constraints - WHEN the caller asks for a human THEN MUST transfer the caller to a live agent. - NEVER transfer the customer to a live agent.
@@ notes, not the conflict.§4Where the name sits
The table in §2 renamed every customer on a line at once. Splitting the human rule apart shows which of the two mentions carries the weight, and how far a phrase can drift before the check loses it. Same base file, same overnight edit, one rewrite of line 26 at a time.
# line 26 as rewritten check WHEN the caller asks for a human THEN MUST transfer the customer to a live agent. logic/contradiction F (83) WHEN the customer asks for a human THEN MUST transfer the caller to a live agent. passes A (99) WHEN the customer asks for a human THEN MUST transfer them to a live agent. passes A (99) WHEN the customer asks for a human THEN MUST transfer the chat to a live agent. passes A (99) WHEN the customer asks for a human THEN MUST transfer the customer to a human agent. passes A (99)
NEVER conflicts with the action whatever the condition says, so renaming inside it changed nothing. The action is compared as a phrase, and a synonym, a pronoun, a different object or a different destination each made it a different action.Some variation does survive. In a two-rule file against MUST transfer the customer to a live agent, the prohibitions NEVER transfer a customer, NEVER transfer customers and NEVER transfer the Customer were all caught: articles, plurals and capitals fold. Changing the noun itself does not: the client escaped. So did NEVER offer a payment plan against MUST offer the customer a payment plan: dropping the recipient was enough. And a declaration saying the two names are one person, The caller is the customer., did not join them for the check either.
That is a narrower kind of matching than verbs get. In the escalation bake-off, escalate to a human agent and hand off to a human agent folded into one action, because the verb catalog groups them. There is no such catalog for the people in your prompt, and there could not be a safe one: whether the caller and the account holder are the same person depends on your business, not on English.
The model folds every name into the person in the chat. The checker keeps every name it has not been told is the same word.
§5One noun, and a reason to keep two
The fix is a vocabulary decision, made once. Pick the noun, say so in the Role annotation where every editor will see it, and rewrite the pasted lines into it. With one name back in the file, the overnight edit is refused again, and the refusal forces the decision nobody wrote down: a person when someone is on shift, something else when nobody is. That is a state the host knows, so it becomes an input, and each deployed prompt carries one of the two rules.
$REQUIRE variable live_agents: one of staffed, closed # Role @@ role: one chat assistant; every rule calls the person in the chat the customer You are Ada, the chat support assistant for Fenwick Energy. # Constraints @@ arrears: a payment plan comes before any talk of collection - IF the customer is behind on payments THEN MUST offer the customer a payment plan. @@ human: a person when one is on shift, a callback when nobody is $IF @{live_agents} is equal to staffed: - WHEN the customer asks for a human THEN MUST transfer the customer to a live agent. $ELSE: - NEVER transfer the customer to a live agent. - WHEN the customer asks for a human THEN MUST offer the customer a callback.
$ npx typeglish build fixed.tg --vars '{"live_agents":"staffed"}'
✓ built .typeglish/dist/fixed.txt ← fixed.tg (17aed6a8261c, full)
$ tail -2 .typeglish/dist/fixed.txt
- IF the customer is behind on payments THEN MUST offer the customer a payment plan.
- WHEN the customer asks for a human THEN MUST transfer the customer to a live agent.
$ npx typeglish build fixed.tg --vars '{"live_agents":"closed"}'
✓ built .typeglish/dist/fixed.txt ← fixed.tg (97995b864270, full)
$ tail -2 .typeglish/dist/fixed.txt
- NEVER transfer the customer to a live agent.
- WHEN the customer asks for a human THEN MUST offer the customer a callback.
There is one case for keeping a second noun, and it is the case where the checker's literal reading is right. On a voice line the person calling is not always the person who owns the account: a partner, a carer, a neighbour reading out a meter. Rules that should apply to one of them and not the other need two names on purpose, guarded so they cannot both apply, which is the subject of why your agent gives the balance to whoever calls. The defect is not two nouns. It is two nouns for one person.
The gate has to come from outside the checker, because nothing inside it measures this: the grade, the rules table and review were all clean on the contradicted file. The cheapest gate is the dumbest one. List the names your teams actually use and fail CI when one of them appears in the prompt source, with an allow-list for the deliberate second person. And when you write a rule that overrides another, copy the object phrase from the rule you are overriding rather than retyping it; typeglish score --json lists every rule's line and text under rules, so the phrase to copy is one command away.
§6Common questions
- Does TypeGlish know that the caller and the customer are the same person?
- No. In this run it folded articles, plurals and capitals, so a customer, customers and the Customer all matched the customer, but the caller, the user, the account holder, the bill payer, the member and them were each a different object. A declaration such as The caller is the customer did not join them either. Two rules about one person written with two nouns are two rules about two people, as far as the contradiction check is concerned.
- Why did my prompt pass the check when one rule forbids what another rule requires?
- One likely reason is that the two rules name the object of the action differently. NEVER transfer the customer to a live agent beside WHEN the customer asks for a human THEN MUST transfer the customer to a live agent is a blocking logic/contradiction and the build is refused. Change the second rule to transfer the caller and the same file checks clean, grades A (99/100), builds, and typeglish review reports no fixes. The object phrase has to match for the conflict to be found: the caller, them, the chat and a human agent instead of a live agent all escaped in this run.
- Does it matter what I call the customer inside a WHEN or IF condition?
- Not for an unconditional edit. Renaming the customer inside the guard of two rules, under five different names, left every one of ten contradicting edits caught, because a NEVER with no condition conflicts with the action whatever the guard says. The name only stopped the check when it sat in the action itself: four rules, five names, twenty edits, none caught.
- How do I stop different teams calling the customer different things in one agent prompt?
- Pick one noun, write it in the Role annotation so every editor sees it, and copy the object phrase from the existing rule when you write a rule that overrides it. The checker will not flag a synonym for you, so a one-line grep in CI for the names your teams use (caller, user, account holder, bill payer, member) is the cheapest gate. Keep a second noun only when it really is a second person, such as a third party on the line who is not the account holder.
Vocabulary drift is what a prompt looks like after it has been merged, not after it has been written. Nobody chooses caller over customer; it arrives with the IVR script, the way user arrives with a product spec and account holder with a compliance review. The checker does exactly what it says: it proves conflicts between the rules as written, and two different nouns are two different rules as written. That makes the noun part of the contract, the same as the verb and the modal. When two prompts become one, run the merge through the checklist for merging two agent prompts, and add one line to it: say the customer's name out loud, once, and make every rule use it.