> Markdown rendition of https://typeglish.dev/blog/teardown-promo-block ("Teardown: the promo block from the campaign brief - Tidelines"). Canonical page: https://typeglish.dev/blog/teardown-promo-block · All pages: https://typeglish.dev/llms.txt

[← Tidelines](https://typeglish.dev/blog)/ Teardowns Oct 18, 2026

# Teardown: the promo block from the campaign brief

Nine lines from marketing, pasted under Promotions the week the winter sale went live. Two of them read as rules. The other seven decide when your agent hands out money.

by **TypeGlish team** 8 min read #teardowns

Two of nine lines are rules.

**TL;DR** A promotions block pasted from a campaign brief usually holds an offer that expires, one word for both public and private codes, and a judgment call about money, so pass the live offer in at runtime, name the two kinds of code differently, let a lookup tool decide whether a code is valid, and test the pleading customer.

Every sale arrives at the agent the same way. Marketing writes a paragraph for the campaign brief, someone pastes it under a `# Promotions` heading, and it ships that afternoon. It reads like policy. Most of it is not anything a model, or a checker, can treat as a rule.

## §1 The block, as pasted

Fernway Outdoor sells clothing online and runs a chat agent on the site. This is its prompt the week the winter sale started: a one-line role, then the promotions block, verbatim from the brief.

promo.tg - the role, and the block from the brief ✗ C (78/100)

```typeglish
# Role
You are the chat assistant for Fernway Outdoor, an online outdoor clothing store.

# Promotions
The current promotion is WINTER20: 20% off orders over 50 pounds, valid until 31 January.
Only one promo code can be used per order.
Codes cannot be combined with sale prices.
If a customer's code has expired, use your best judgment.
Never share a promo code with a customer.
Always mention the current promo code in every reply.
You can apply a code to a past order within 14 days.
Staff codes are for employees only and must never be applied to customer orders.
Members get free delivery and 10% off everything.
```

tg review - promo.tg ✗ 2 of 9 lines read as rules

```
$ typeglish review promo.tg
promo.tg — C (78/100) → A (100) projected if every fix below lands  check: 0 error, 0 warning, 5 info  tiers: base+z3

  FIXES  ranked by projected gain
  +12  put a "@@ why" note above L2 L5 L6 L7 L8 L9 L10 L11 +2 more — annotation 0 → 100
  +3   make L10 provable — one verb the checker knows, one object, or a bound (the rules table names each edit)
  +3   use the imperative or a named doer on L6 L7 L12 — up to directness 100
  +2   fix the 3 prompt/agentless-passive findings (L6 L7 L12) — The doer is deleted (agentless passive) or demoted to a by-phrase - name the actor or use the imperative.
  +1   fix the 1 typeglish/if-then finding (L8) — A conditional (IF / WHEN / UNLESS / WHILE / …) opened without its THEN — an ADVISORY by the owner’s ruling (2026-08,…
  +1   fix the 1 prompt/unregistered-doer finding (L13) — A bare generic doer (Users should ...) never enters the world model - the imperative or a definite party registers wh…

  RULES  2 of 2 rule-shaped lines read · 1 not provable
    L10  ~ unprovable    Always mention the current promo code in every reply.
                         the checker knows "mention" but cannot prove this shape: the object runs past six words → name the object in fewer words, or split the rule

  FINDINGS
  promo.tg:6:1  info   prompt/agentless-passive  The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."
  promo.tg:7:1  info   prompt/agentless-passive  The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."
  promo.tg:8:1  info   typeglish/if-then  IF needs a THEN — write IF <condition> THEN <action>.
  promo.tg:12:1  info   prompt/agentless-passive  The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."
  promo.tg:13:1  info   prompt/unregistered-doer  A bare generic doer never enters the world model - instruction to the agent, or background about users? Use the imperative if the agent acts, or a definite party ("The user should ...") to register the doer.

  TESTS  no $TEST cases — add one beside a rule; `test --dry` validates it offline
```

No errors, a C, and a rules table with two entries. Nine lines of promotion policy, and the checker can read two of them as rules, one of which it cannot prove.

Read the rules table as the agent's own inventory. Lines 6, 7 and 12 are passives with nobody doing anything: a code *can be used*, codes *cannot be combined*, staff codes *must never be applied*. By whom? Line 13 describes what members get without saying what the agent does about it. Line 11 says the agent *can* apply a code to an old order, which is a description of a capability, not a permission or a limit. None of that is wrong as a note to a person. To a model it is background, and the checker counts it as prose because that is what it is.

## §2 Line 5: an offer with an end date the agent cannot see

Line 5 is the one marketing cared most about: WINTER20, 20% off over 50 pounds, *valid until 31 January*. The checker has nothing to say about it, and that is correct: it is a sentence of fact, not a rule.

The trouble is the date. On 1 February the line is still in the prompt, still saying the current promotion is WINTER20, and the model has no reliable way to know that today is past the 31st. It will offer an ended sale to every customer who asks about discounts, in a confident voice, until somebody remembers to edit the prompt. [You retired the plan. The prompt still sells it.](https://typeglish.dev/blog/retired-plan-still-sells) is the same failure with a tariff in place of a sale.

The fix is to stop writing the offer down. It belongs to marketing's clock, not the prompt's: a `$REQUIRE variable current_offer` that the platform fills from whatever system owns the campaign, so the prompt changes the day the offer does. The end date, the minimum spend and the sale exclusions on lines 5 and 7 belong in the code lookup, which knows today's date. The agent does not need to do that arithmetic; it needs to ask.

## §3 Lines 9 and 10: one word for two kinds of code

Line 9 says never share a promo code. Line 10 says mention the current promo code in every reply. Taken literally, the block forbids and requires the same act, and the agent resolves it differently from chat to chat.

Marketing meant two different things. Line 9 is about private codes: the staff discount, the goodwill codes the complaints team hands out, the influencer codes. Line 10 is about the public offer, which they want shouted. Both are called *a promo code*, so to the model they are one thing.

The checker does not catch it either, and here is why. Line 10 is the unprovable rule from the table. Tighten it to the shape the checker asks for, and the pair still passes:

tg check - codes.tg, the two lines tightened ✗ clean, and still both

```typeglish
# Role
You are the chat assistant for Fernway Outdoor.

# Promotions
- NEVER share a promo code.
- MUST share the current promo code.
```

```
$ typeglish check codes.tg
✓ 1 file — 0 error, 0 warning, 0 info
```

`a promo code` and `the current promo code` are different objects to the checker, so a ban on one says nothing about a requirement on the other. Both rules read; neither is compared.

That is the checker being exact, not lenient: [the OTP rewrite](https://typeglish.dev/blog/otp-code-rewrite) found the same thing with *the code* and *a code*. The object phrase is the identity of the thing a rule is about. Here that exactness points straight at the real defect: these are two things, so give them two names. A *private code* is never shared. The *public offer* is mentioned when the customer asks about price, not in every reply, which on its own would put a sales line under the answer to a customer whose parcel is lost.

## §4 Line 8: best judgment, on a discount

Line 8 is the expensive one: *If a customer's code has expired, use your best judgment.* The checker flags it as `typeglish/if-then`, an IF without a THEN, and it does not count as a rule. Even with a THEN, *use your best judgment* is not an action. It hands the model the decision with no criteria, and [the sentence that repeals the rest of your prompt](https://typeglish.dev/blog/best-judgment-rewrite) traced where that leads.

In practice it leads somewhere predictable. Models tend to accommodate a customer who pushes, so the judgment goes one way: the expired code goes through for anyone who asks twice, and the pleading customer gets a discount the polite one does not. That is a pricing policy, made by the model, one chat at a time.

Decide the policy and write it down. For Fernway: the code lookup decides validity, an invalid code is never applied, and goodwill is a person's call. The upset customer is a reason to route, not a reason to discount.

## §5 The rebuild, and the patch that still passes

The rebuilt block: the live offer as a runtime input, two tools that own the facts the model cannot know, and six rules, each with a note saying why it exists.

fernway.tg - the block rebuilt ✓ A (99/100)

```typeglish
$REQUIRE variable current_offer
$TOOL check_code
  - description:: Look up a promo code and return whether it is valid for this order.
$TOOL apply_code
  - description:: Apply a valid promo code to an order.

# Role
@@ role: an online clothing store's chat desk; every later "you" is the agent
You are the chat assistant for Fernway Outdoor.

# Offers
@@ offer_is_runtime: marketing changes the offer; the platform passes in the live one, so nothing here can outlive it
The %public offer% is @{current_offer}.
@@ offer_when_asked: mention the offer when the customer raises price, not in every reply
- WHEN the customer asks about discounts THEN MUST mention the public offer.
@@ private_codes: staff, goodwill and partner codes are private; the customer only uses one they already hold
- NEVER share a private code.
@@ check_first: the tool knows expiry dates, minimum spends and sale exclusions; the agent does not
- WHEN the customer gives a promo code THEN MUST call @[check_code].
@@ apply_only_valid: an expired or invalid code is a no, however upset the customer is
- IF @[check_code] reports the code as invalid THEN NEVER call @[apply_code].
@@ one_code: one code per order
- MUST apply at most 1 code per order.
@@ past_orders: codes can go on orders up to 14 days old
- IF the order is older than 14 days THEN NEVER call @[apply_code].

$TEST expired_code
  - input:: My code SNOW15 says expired but it was only last week. Please just put it through.
  - expect::
    - calls check_code
    - does not call apply_code

$TEST asks_for_codes
  - input:: Have you got any discount codes? A friend said staff get 30 percent.
  - expect::
    - mentions the public offer
    - does not share a private code
```

tg review and test --dry - fernway.tg ✓ 6 of 6 rules read

```
$ typeglish review fernway.tg
fernway.tg — A (99/100)  check: 0 error, 0 warning, 0 info  tiers: base+z3

  FIXES  none — every facet is essentially full

  RULES  6 of 6 rule-shaped lines read · 6/6 covered by $TEST

  TESTS  2 cases · 6/6 rules exercised

$ typeglish test fernway.tg --dry
✓ fernway.tg  coverage: 6/6 rules exercised
  · expired_code — "My code SNOW15 says expired but it was only last week. Pleas" (not run)
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · asks_for_codes — "Have you got any discount codes? A friend said staff get 30 " (not run)
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed
```

From C to A, two rules to six, and every rule named by a test case. Coverage here means a case shares the rule's wording; whether the agent passes is a live run.

Now the part teardowns usually skip: the next request from marketing. A customer complained, and the brief comes back with one more line. Here are two ways it could be written, each added to the rebuilt file below the last rule:

tg check - two versions of the goodwill patch ✗ one refused, one passes

```
$ diff fernway.tg patch-invalid.tg
25a26,27
> @@ goodwill: marketing asked for this after a complaint
> - IF @[check_code] reports the code as invalid THEN MUST call @[apply_code].

$ typeglish check patch-invalid.tg
patch-invalid.tg:21:1  error  logic/contradiction  Conflicts with line 27. Logical conflict — "call @[apply_code]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                  ↳ line 27: - IF @[check_code] reports the code as invalid THEN MUST call @[apply_code].
patch-invalid.tg:27:1  error  logic/contradiction  Conflicts with line 21. Logical conflict — "call @[apply_code]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                  ↳ line 21: - IF @[check_code] reports the code as invalid THEN NEVER call @[apply_code].

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

$ diff fernway.tg patch-upset.tg
25a26,27
> @@ goodwill: marketing asked for this after a complaint
> - IF the customer is upset THEN MUST call @[apply_code].

$ typeglish check patch-upset.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ typeglish score patch-upset.tg | head -1
patch-upset.tg — A (99/100)  proven errors: none  tiers: base+z3
```

Same condition, opposite modals: refused. A different condition that overlaps in real life: an A, because nothing in the file says an upset customer and an invalid code can arrive in the same message.

The first patch is caught because it says the quiet part out loud. The second says the same thing politely, and it is the one marketing will actually write. A checker can only compare the conditions you wrote, and *the customer is upset* and *the code is invalid* are two judgments in different words. That is why `expired_code` pleads in its input. It is the case that turns the upset-customer patch from a clean check into a failing test, on the first live run after someone adds it.

## §6 Common questions

**How do I stop my AI support agent giving out discount codes?**

Name the two kinds of code differently in the prompt, a public offer and a private code, and write NEVER share a private code as its own rule. Keep the private codes themselves out of the prompt: a code the model never reads is a code it cannot leak. Validate any code the customer gives through a lookup tool.

**Why does my chatbot keep offering a promotion that has ended?**

Because the offer and its end date are written into the prompt, and the model has no reliable sense of today's date to compare them against. Pass the live offer in at runtime as a variable, so the prompt changes the day marketing changes the offer and nobody has to remember to edit it.

**Should my agent honour an expired promo code if the customer is upset?**

That is a policy decision, not a judgment to hand the model. Write the policy as a rule the checker can read, such as never applying a code the lookup reports as invalid, and send goodwill requests to a person. A test case with a pleading customer checks that the rule holds.

**Can TypeGlish tell when two promo rules conflict?**

When two rules share a condition and an action with opposite modals, yes: that is a blocking logic/contradiction and the build is refused. It cannot tell that an upset customer and an expired code can arrive in the same message, so a goodwill rule scoped to upset customers passes clean. That overlap needs a test case.

Field note

Before a promotions block ships, run `typeglish review` on it and read the RULES count against the line count. Every line that is not in the table is something the agent will treat as background, and in a promotions block those lines are usually the ones about money. Give the live offer a `$REQUIRE`, give validity to a tool, and keep one test case that asks, nicely and then not so nicely, for a code that has expired.

∿ washed up Oct 18, 2026 ∿
