← Tidelines/Deep dives

You retired the plan. The prompt still sells it.

Platinum closed to new customers in March and was withdrawn in July. The support prompt has four rules about it, and exactly one of them is something a compiler can find.

by TypeGlish team8 min read#deep-dives
One word out. One error found.

TL;DR A retirement is only provable where the retired thing was a typed value: drop platinum from $REQUIRE variable plan: one of essential, complete, platinum and the arm that tested for it is a blocking structure/impossible-case that refuses to build, while three prose rules about the same dead plan are 0 error, 0 warning, 0 info before and after, hold B (82/100) on every facet, and ship in the artifact. Move every plan rule inside a $SWITCH ON @{plan} arm and the retirement locks in both directions.

Products leave. A plan gets closed to new customers, runs off for a quarter, and then goes. Pricing knows, billing knows, the CRM knows, and the entitlement flag stops arriving. The system prompt is the one artefact in the estate that has no idea, because nothing about withdrawing a product touches the document that tells an agent how to talk about it.

This is not the interesting part. The interesting part is that a checker can prove some of those leftover rules are dead and provably cannot prove it about the rest, and the line between the two has nothing to do with how important the rule is. It is drawn by whether the plan was ever a value the file declared.

§1Four rules about a product that no longer exists

Kestrel Fibre sells three broadband plans, and the chat support prompt gates the engineer-visit promise on which one the caller is on. The host sends the plan as a typed input, so the compiler knows the set.

plans.tg - as inherited, Platinum still live✓ B (82/100)
<$CONFIG>
  $REQUIRE variable plan: one of essential, complete, platinum
</$CONFIG>

# Role
You are a broadband support agent for Kestrel Fibre.

# Constraints
- You MUST keep every reply to at most 4 sentences.
- IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee.
- WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile.
- You MUST offer a Platinum upgrade to any customer who mentions buffering.
$IF @{plan} is equal to platinum:
  You MUST book a same-day engineer visit.
$ELSE IF @{plan} is equal to complete:
  You MUST book a next-day engineer visit.
$ELSE:
  You MUST book the next available engineer visit.
Four rules mention Platinum. Three are English sentences in the Constraints list. The fourth is the first arm of the deterministic chain, and it is the only one that names the plan as a value rather than as a word.
tg check plans.tg - output
$ npx typeglish check plans.tg
 1 file — 0 error, 0 warning, 0 info
Nothing to report, which is correct. While Platinum exists this file is a good file.
tg score plans.tg - output
plans.tg — B (82/100)  proven errors: none  tiers: base+z3
  planes  runtime 93 (what the model reads) · hygiene 50 (source only)
  facets  enforceability 83 x.21 · hardness 86 x.12 · directness 96 x.08 · consistency 100 x.17
          structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  lever   annotation 0/100 (up to +12 overall) — Put a "@@ why" note directly above each
          statement ("@@ name: why" also names it).
B (82/100), and the only lever offered is that nobody wrote @@ notes. Hold on to this number: it is the last honest reading the file will produce.

§2One word out of the domain

July arrives. Platinum is gone, the entitlement stops being sent, and somebody does the tidy thing and takes the member out of the declared set. That is the whole diff: one word, on line 2.

retired.tg - the same file, minus one word✗ structure/impossible-case
<$CONFIG>
  $REQUIRE variable plan: one of essential, complete
</$CONFIG>

# Role
You are a broadband support agent for Kestrel Fibre.

# Constraints
- You MUST keep every reply to at most 4 sentences.
- IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee.
- WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile.
- You MUST offer a Platinum upgrade to any customer who mentions buffering.
$IF @{plan} is equal to platinum:
  You MUST book a same-day engineer visit.
$ELSE IF @{plan} is equal to complete:
  You MUST book a next-day engineer visit.
$ELSE:
  You MUST book the next available engineer visit.
Line 2 lost , platinum. Nothing else in the file moved: the three prose rules and all three chain arms are byte-identical to §1.
tg check retired.tg - one word removed from the domain✗ 1 error
$ npx typeglish check retired.tg
retired.tg:13:1  error  structure/impossible-case  @{plan} is never platinum (it's one of essential, complete).

 1 file — 1 error, 0 warning, 0 info

$ npx typeglish build retired.tg
typeglish build: retired.tg refused — nothing written
retired.tg:13:1  error  structure/impossible-case  @{plan} is never platinum (it's one of essential, complete).

 1 file — 1 error, 0 warning, 0 info
Line 13 is the chain arm, and the message is a statement about the world rather than about style: is never platinum. An arm testing a value its input cannot hold can never fire, so it is dead code, and the build refuses at exit 1.

That error is the good news, and it is worth saying plainly what it is: the compiler has found a site in your prompt that still believes in a product you withdrew, and it will not let you ship until somebody looks. This is the same machinery that catches a member misspelled in a tier gate and a member added to a domain nobody re-read. Retirement is the third direction, and it is the one nobody plans for, because adding a plan is a project and removing one is a cleanup ticket.

§3The green file that still sells it

Now watch what the fix does. There is one error, it names one line, and the obvious repair is to delete the arm that can no longer fire. Two lines out, and the file is clean.

cleared.tg - the dead arm deleted✓ B (82/100)
<$CONFIG>
  $REQUIRE variable plan: one of essential, complete
</$CONFIG>

# Role
You are a broadband support agent for Kestrel Fibre.

# Constraints
- You MUST keep every reply to at most 4 sentences.
- IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee.
- WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile.
- You MUST offer a Platinum upgrade to any customer who mentions buffering.
$IF @{plan} is equal to complete:
  You MUST book a next-day engineer visit.
$ELSE:
  You MUST book the next available engineer visit.
0 error, 0 warning, 0 info, and B (82/100) again with runtime 93 and consistency 100, identical to the file from before the retirement. Three rules about a plan that cannot be sold, and every gate you own is green.

The build is the part to put in front of whoever asked for the retirement, because it is the only document in the loop that is about what the agent will actually be told.

tg build cleared.tg --vars - what the model reads now
$ npx typeglish build cleared.tg --vars '{"plan":"essential"}'
 built .typeglish/dist/cleared.txt ← cleared.tg (b99007c84615, full)

# Role
You are a broadband support agent for Kestrel Fibre.

# Constraints
- You MUST keep every reply to at most 4 sentences.
- IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee.
- WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile.
- You MUST offer a Platinum upgrade to any customer who mentions buffering.
You MUST book the next available engineer visit.
An agent serving an essential customer is instructed to waive a callout fee on a plan that no longer exists, to quote a 900 Mbps profile nobody is provisioned on, and to upsell the withdrawn product to anybody who mentions buffering. The chain resolved correctly. The prose did not resolve at all, because it was never a chain.
The one rule the compiler could see is the one rule that got fixed. That is not a coincidence, it is the mechanism.

§4Why three of the four were unreachable

A closed domain is a promise about which values can occur. Once plan is one of essential, complete, every comparison against it is a claim that can be settled: is equal to platinum is decidable and the answer is no. That is the whole of what happened on line 13.

The other three rules never made a claim of that kind. IF the customer is on Platinum is a guard the model weighs at runtime against whatever it thinks it knows about the conversation, and Platinum there is a noun in a sentence. It is not a member of a set, so there is no set to check it against, so there is nothing to prove. The checker is not missing the rules. It has never been told they are about the same thing.

  • The machine plane is where retirement is checkable. A typed input, a $IF condition, a $SWITCH arm, a <tag when=…> guard. These are read by the compiler, so a value that leaves the domain leaves an error behind at every site that referenced it.
  • The prose plane is where retirement is invisible. Anything that reads as an instruction is passed through, and a passed-through rule about a dead product is still a live instruction. This is the same split that lets a compile-time value and a runtime one sit in one file looking identical.
  • Neither the score nor the count moves. B (82/100) before, B (82/100) after, and the counts are 0 / 0 / 0 in both. There is no facet for rules about things that no longer exist, and there could not be one: the compiler has no way to know Platinum went.
  • Deleting is the direction with no gate. A rule added to a prompt gets reviewed, because somebody wrote it this week. A rule that should have been deleted has no author and no ticket, which is why a deletion nobody notices and a deletion nobody performs fail the same way.

§5Put the plan back inside the domain

So the fix is not a better search for the word Platinum. It is to stop writing plan rules as sentences about plans and start writing them as arms of the plan. A $SWITCH ON block is the strongest form of that, because it takes members of one typed domain and proves coverage by construction.

fixed.tg - every plan rule inside an arm✓ A (93/100)
<$CONFIG>
  $REQUIRE variable plan: one of essential, complete, platinum
</$CONFIG>

# Role
@@ role: inbound broadband support, plan-gated on the entitlement the host sends
You are a broadband support agent for Kestrel Fibre.

# Constraints
@@ brevity: four sentences keeps a chat reply scannable
- You MUST keep every reply to at most 4 sentences.
@@ visits: every plan rule lives in an arm, so retiring a plan is one compile error here
$SWITCH ON @{plan}
  - essential::
    @@ essential_visit: no visit SLA is sold on the entry plan
    You MUST book the next available engineer visit.
    @@ upsell: buffering is the one complaint the next plan up actually answers
    IF the customer mentions buffering THEN you MUST offer a Complete upgrade.
  - complete::
    @@ complete_visit: next-day is the sold SLA
    You MUST book a next-day engineer visit.
  - platinum::
    @@ platinum_visit: same-day is the sold SLA
    You MUST book a same-day engineer visit.
    @@ platinum_fee: the waived callout fee is the paid-for part of the plan
    You MUST NOT charge an engineer callout fee.
    @@ platinum_speed: 900 Mbps is the only profile provisioned on this plan
    IF the customer asks about speed THEN you MUST quote the 900 Mbps profile.

$TEST platinum_callout
  - input:: My line is down again and I am not paying another callout fee.
  - expect::
    - contains "no callout fee"
0 error, 0 warning, 0 info at A (93/100), hygiene 100, with enforceability 68 named as the remaining lever. No policy changed and no rule reworded. Every sentence that used to say Platinum now sits under the member instead of naming it.

Then the retirement has two locks on it, and they close from opposite sides.

tg check - the two-way lock
# 1. take the member out of the domain, leave the arm alone
fixed-retired.tg:22:3  error  structure/impossible-case  "platinum" is not a member of @{plan}'s domain — one of essential, complete.

 1 file — 1 error, 0 warning, 0 info

# 2. delete the arm, leave the domain alone
fixed-armless.tg:13:1  error  structure/non-exhaustive-switch  @{plan} can be platinum, but no arm handles it — add a "- <member>::" row for each (or a deliberate "- otherwise::" fallback).

 1 file — 1 error, 0 warning, 0 info
You cannot half-retire a plan. Drop the member and every arm that mentions it is an error; drop the arm and the domain says a value is unhandled. The only way through is both edits, together, which is exactly the diff a retirement is.

And the payoff is the thing you can hand to the person who has to sign off that nothing else changed.

tg build --vars essential - before and after the retirement
# Platinum still in the domain, arm still present
$ npx typeglish build fixed.tg --vars '{"plan":"essential"}'
 built .typeglish/dist/fixed.txt ← fixed.tg (ad17a800993a, full)

# Platinum out of the domain, arm deleted: the two-line retirement
$ npx typeglish build retired-properly.tg --vars '{"plan":"essential"}'
 built .typeglish/dist/retired-properly.txt ← retired-properly.tg (ad17a800993a, full)

# Role
You are a broadband support agent for Kestrel Fibre.

# Constraints
- You MUST keep every reply to at most 4 sentences.
You MUST book the next available engineer visit. IF the customer mentions buffering THEN you MUST offer a Complete upgrade.
The same artifact hash, ad17a800993a, before and after. Retiring the plan is provably a no-op for every customer who was not on it, and the word Platinum is not in the deployed prompt at all. The retired-properly file still checks 0 error, 0 warning, 0 info at A (93/100).
tg test fixed.tg --dry - output
$ npx typeglish test fixed.tg --dry
 fixed.tg  coverage: 1/7 rules exercised
  · platinum_callout — "My line is down again and I am not paying another callout fe" (not run)
       contains "no callout fee"
 1 prompt — 0 failed
One deterministic assert, settled offline, pinned to the rule most likely to survive a retirement it should not survive. When Platinum goes, this case goes with the arm, and its absence from the suite is a diff somebody reviews.

§6Common questions

Why does my AI agent still offer a product we discontinued?
Because withdrawing a product removes it from your price list and from nothing else. A support prompt with four rules about a retired broadband plan checks at 0 error, 0 warning, 0 info and scores B (82/100), and the three of those rules written in English keep checking clean after the plan is gone, because a prose rule is an instruction to the model rather than a claim about a declared set. The build proves it: with the plan retired and the file green, the artifact still reads IF the customer is on Platinum THEN you MUST NOT charge an engineer callout fee, WHEN a Platinum customer asks about speed THEN you MUST quote the 900 Mbps profile, and You MUST offer a Platinum upgrade to any customer who mentions buffering.
Does TypeGlish catch a rule about a plan that no longer exists?
Only where the plan was a member of a typed domain. Remove platinum from $REQUIRE variable plan: one of essential, complete, platinum and the chain arm that tested for it is a blocking structure/impossible-case reading @{plan} is never platinum (it's one of essential, complete), and typeglish build answers retired.tg refused, nothing written at exit 1. The three prose rules naming Platinum in the same file produce no diagnostic at all, before or after, and the grade does not move: B (82/100) with runtime 93 and consistency 100 on both files.
How do I remove a retired product from an agent system prompt safely?
Put every rule about the product inside an arm of a $SWITCH ON over the typed input, so the compiler holds the domain and the rules together. Then the retirement locks in both directions: take platinum out of the domain and the arm is a blocking structure/impossible-case, delete the arm and leave the domain alone and it is a blocking structure/non-exhaustive-switch reading @{plan} can be platinum, but no arm handles it. Doing both is the two-line diff that actually retires a plan, and it is provably safe for everybody else: the essential build lands on ad17a800993a before the retirement and ad17a800993a after it.
Why did removing one word from my prompt break the build?
Because the word was a member of a closed set and something still compared against it. A domain is a promise about which values can occur, so an arm testing for a value the domain excludes can never fire, and an arm that can never fire is dead code rather than a harmless leftover. That error is the useful half of a retirement: it is the compiler naming every site that still believes in the thing you withdrew. The rules it cannot name are the ones written as English about the product, and those are the ones that keep reaching the model.
Field note

The habit worth taking from this is about where a fact lives rather than how it is worded. Anything in your product catalogue that a rule depends on is a closed set, and a closed set belongs in the file as one, declared once, with the rules hanging off its members. Then the catalogue and the prompt cannot drift, because the compiler holds them together and says so at exit 1. Write the same rules as sentences and you have made your prompt a place where retired products go to keep working: no error, no warning, no score movement, and an agent cheerfully upselling something the billing system would refuse to provision. The four rules in this post were written by four different people over two years, and every one of them was correct on the day it landed. That is the ordinary way a prompt becomes wrong.

∿ washed up Aug 26, 2026 ∿