← Tidelines/Deep dives

The typo that compiles is the one to worry about

An ablation over keystrokes rather than rules. Sixteen one-character edits to a clean care prompt, one per copy, and the split is ten blocking, three advisory, three completely silent. The report gets quieter as the risk goes up.

by TypeGlish team9 min read#deep-dives
Ten refused. Three shipped.

TL;DR Sixteen one-character edits to a clean A (92/100) agent prompt split three ways: ten refuse to compile, three warn and become errors under --strict, and three produce 0 error, 0 warning, 0 info with every facet unmoved. What decides the split is grammar rather than danger, because a keystroke that breaks the form of a declaration has no valid reading and a keystroke that leaves a grammatical sentence behind has one, so gate the quiet class on the build hash instead of the checker.

Every ablation on this blog has removed something a person wrote on purpose: a whole line, a numeric bound, a modal, the scope on a rule, a test. This one removes a character. It is the edit nobody plans and everybody makes: 6pm on a Thursday, one word changed in the file that runs the agent, no review, straight to the branch.

§1One clean file, sixteen keystrokes

The subject is a small care prompt with one of everything: a folded config block, a typed input, a named condition, a deterministic chain, four annotated rules, a literal zone and a test. Nothing exotic. It is roughly what a reviewed prompt looks like once somebody has moved the scaffolding out of the model's way.

care.tg - the baseline✓ A (92/100)
<$CONFIG>
  $IMPORT tool get_account
  $REQUIRE variable account_id: string
  $REQUIRE variable tier: one of gold, silver, bronze
  $DEFINE priority AS @{tier} is equal to gold
</$CONFIG>

<role>
  You are a care agent for Larkspur Broadband. You do not have access to a customer's payment card.
</role>

<constraints>
  @@ brevity: three sentences keeps a chat reply scannable
  - You MUST keep every reply to at most 3 sentences.
  @@ account: the account number unlocks the record
  - You MUST confirm an account number @{account_id} back to a customer.
  @@ lookup: read the record, never recall it
  - You MUST look up an account with @[get_account].
  @@ hours: quote the published window, never a guess
  - You MUST state the support hours as 9am - 5pm.
  $IF @{priority}:
    You MUST offer a callback within 2 hours.
  $ELSE:
    You MUST offer a callback within 24 hours.
</constraints>

<"escalation_card">
  L1 -> L2 -> duty manager
</"escalation_card">

$TEST callback
  - input:: My broadband is down again.
  - expect::
    - at most 3 sentences
Thirty-three lines, seven rules, one of every construct that carries a syntax. The literal zone is there because a contact centre always has one ASCII diagram somebody refuses to give up.
tg check + tg score care.tg - output
$ npx typeglish check care.tg
 1 file — 0 error, 0 warning, 0 info

$ npx typeglish score care.tg
care.tg — A (92/100)  proven errors: none  tiers: base+z3+spell
  planes  runtime 98 (what the model reads) · hygiene 75 (source only)
  facets  enforceability 91 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 50 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  lever   annotation 50/100 (up to +6 overall) — Put a "@@ why" note directly above each statement ("@@ name: why" also names it).
The base+z3+spell tier line matters for later. All three tiers are looking at this file, and one of them has a finding that check will not print.

Sixteen copies, one edit each. Every edit is a character dropped, added or replaced, and every one is something a real person does: a sigil that did not survive a paste, a hyphen where an underscore belongs, an autocorrected apostrophe, a dedent from a reformat, a digit.

typo/ - the sixteen edits
sigil          $IMPORT tool get_account        ->  IMPORT tool get_account
pointer_at     number @{account_id} back       ->  number {account_id} back
pointer_dash   @{account_id}                   ->  @{account-id}
oneof_colon    tier: one of gold, ...          ->  tier: one of: gold, ...
define_eq      $DEFINE priority AS @{tier} ... ->  $DEFINE priority = @{tier} ...
half_quote     <"escalation_card">             ->  <escalation_card">
switch_colon   $IF @{priority}:                ->  $IF @{priority}
test_colon     - input:: My broadband ...      ->  - input: My broadband ...
test_indent    (2-space indent on - input::)   ->  (dedented to column 0)
member_typo    is equal to gold                ->  is equal to glod
period         at most 3 sentences.            ->  at most 3 sentences
curly          a customer's payment card       ->  a customer’s payment card
endash         9am - 5pm                       ->  9am – 5pm
bound_digit    at most 3 sentences.            ->  at most 8 sentences.
modal_case     You MUST keep every reply       ->  You Must keep every reply
prose_typo     for Larkspur Broadband.         ->  for Larkspur Broadbnad.
Two of the sixteen are two characters (AS to =, and the dedent), because there is no one-character way to make those mistakes. Everything else is a single keystroke.

§2The ten that refuse

Run the whole directory and the headline is 16 files, 15 error, 16 warning, 1 info. Split out the ten with a blocking error and the report reads like this.

tg check blocking/ - ten variants, one keystroke each✗ 15 error
$ npx typeglish check blocking
blocking/define_eq.tg:5:3  error  structure/define-binder  The $DEFINE binder is AS, never "=". Replace this line with: "  $DEFINE priority AS @{tier} is equal to gold".
blocking/half_quote.tg:27:1  error  structure/undeclared-tool  Tool "escalation_card" is used but never imported (add it to a $IMPORT tool line).
blocking/half_quote.tg:27:1  warn   structure/missing-period  Unterminated statement — end it with a period (or ! ?; a lead-in may end with ":"). Statement boundaries are a compile contract.
blocking/half_quote.tg:27:1  warn   structure/loose-content  This content is outside any section — put it inside a <tag> (a heading ends where a tag or the next heading begins).
blocking/half_quote.tg:27: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.
blocking/half_quote.tg:28:3  warn   structure/missing-period  Unterminated statement — end it with a period (or ! ?; a lead-in may end with ":"). Statement boundaries are a compile contract.
blocking/half_quote.tg:28:1  warn   structure/bad-indent  Indentation mirrors section nesting: expected column 0 (0 levels deep), found 2. The indent unit is 2 spaces.
blocking/half_quote.tg:28:3  warn   structure/loose-content  This content is outside any section — put it inside a <tag> (a heading ends where a tag or the next heading begins).
blocking/half_quote.tg:29:1  error  structure/stray-close-tag  </"escalation_card"> has no matching <"escalation_card"> to close.
blocking/member_typo.tg:5:3  error  structure/impossible-value  $DEFINE priority: @{tier} is never glod (it's one of gold, silver, bronze).
blocking/oneof_colon.tg:4:3  error  structure/bad-input-type  Input "tier": "one of: gold, silver, bronze" is not a domain. The forms: boolean, number, integer, string, or "one of a, b".
blocking/pointer_at.tg:3:3  warn   structure/unused-import  Required variable "account_id" is never used.
blocking/pointer_at.tg:3:21  warn   clarity/unused-variable  $REQUIRE variable "account_id" is never used — no @{account_id} reference fills it. Remove it, or reference it in the prompt.
blocking/pointer_at.tg:16:40  error  structure/retired-brace-ref  Bare braces are prose — they no longer reference. "account_id" is a declared name, so point at it: replace {account_id} with @{account_id}.
blocking/pointer_dash.tg:3:3  warn   structure/unused-import  Required variable "account_id" is never used.
blocking/pointer_dash.tg:3:21  warn   clarity/unused-variable  $REQUIRE variable "account_id" is never used — no @{account_id} reference fills it. Remove it, or reference it in the prompt.
blocking/pointer_dash.tg:16:40  error  structure/bad-pointer-name  @{account-id} is not a name the compiler can read — a pointer name begins with a LETTER, then letters, digits, and underscore (a dot namespaces the runner's environment: @{env.KEY}). The renderer cannot read it either, so the model receives this line with the compile-time @ still on it. Rename the symbol — @{account_id} is the mechanical rewrite.
blocking/sigil.tg:2:3  error  structure/config-section-content  Content inside <$CONFIG> never reaches the model — only $-commands ($IMPORT, $CONFIG, $TOOL, $DEFINE, $TEST) belong here. Move prompt content outside the section.
blocking/sigil.tg:2:3  error  structure/needs-sigil  Commands must start with "$" — did you mean "$IMPORT"?
blocking/sigil.tg:18:38  error  structure/undefined-tool-inline  @[get_account] — no $TOOL or $IMPORT tool named "get_account" in this file. Define it ("$TOOL get_account") or import it ("$IMPORT tool get_account") so the pointer has a target.
blocking/switch_colon.tg:22:5  error  logic/time-strength  Conflicts with line 18. One bound per slot — "within 2 hours" already entails "within 24 hours"; the looser rule on "you offer a · callback" is dead weight. Keep one.
blocking/switch_colon.tg:22:1  warn   structure/bad-indent  Indentation mirrors section nesting: expected column 2 (1 level deep), found 4. The indent unit is 2 spaces.
blocking/switch_colon.tg:23:3  error  structure/bad-switch  $ELSE without a deterministic $IF above it at the same column — a chain reads $IF @{x} …: / $ELSE IF …: / $ELSE:.
blocking/switch_colon.tg:24:5  error  logic/time-strength  Conflicts with line 16. One bound per slot — "within 2 hours" already entails "within 24 hours"; the looser rule on "you offer a · callback" is dead weight. Keep one.
blocking/switch_colon.tg:24:1  warn   structure/bad-indent  Indentation mirrors section nesting: expected column 2 (1 level deep), found 4. The indent unit is 2 spaces.
blocking/test_colon.tg:31:1  error  structure/bad-test  only "- input::" and "- expect::" fields belong in $TEST callback — "- input: My broadband is down again." is not one.
blocking/test_colon.tg:32:3  warn   structure/loose-content  This content is outside any section — put it inside a <tag> (a heading ends where a tag or the next heading begins).
blocking/test_indent.tg:31:1  error  structure/bad-test  $TEST callback needs an "- input:: …" line.
blocking/test_indent.tg:34:5  warn   structure/missing-period  Unterminated statement — end it with a period (or ! ?; a lead-in may end with ":"). Statement boundaries are a compile contract.

 10 files — 15 error, 13 warning, 1 info
program: 10 independent files — no $IMPORT compositions
Ten distinct codes for ten keystrokes, and every one names the character. Eight of the ten carry the rewrite in the message, which is the part that makes this class cheap: you do not debug a dropped sigil, you paste the suggestion back.

Two things in that report are worth more than the count. The first is blast radius. The sigil variant is one dollar sign, and it produces three errors in two places: the line itself, and a tool pointer sixteen lines further down that no longer has anything to point at.

sigil.tg - the dollar sign, minimised✗ 3 error
<$CONFIG>
  IMPORT tool get_account
</$CONFIG>

<role>
  You are a care agent for Larkspur Broadband.
</role>

<constraints>
  - You MUST look up an account with @[get_account].
</constraints>
One missing character, three errors: structure/needs-sigil on the line, structure/config-section-content because a line with no sigil is content and content is not allowed in a folded config block, and structure/undefined-tool-inline on the pointer that lost its declaration. Declaring a thing and pointing at it are one contract, so a break at either end is reported at both.

The second is that half the class is the reference family: a pointer is the one construct in the language whose name has to survive a keystroke exactly, and the compiler treats a name it cannot read differently from a name that reads fine and resolves to nothing.

pointer_at.tg - the at sign, minimised✗ 1 error
<$CONFIG>
  $REQUIRE variable account_id: string
</$CONFIG>

<role>
  You are a care agent for Larkspur Broadband.
</role>

<constraints>
  - You MUST confirm an account number {account_id} back to a customer.
</constraints>
structure/retired-brace-ref, plus two warnings saying the declared input is now unused. Change the underscore to a hyphen instead and the code is structure/bad-pointer-name, whose message spells out the consequence the other one avoids: the renderer cannot read the name either, so the line would reach the model with its compile-time @ still attached.

§3The three that only warn

Three of the sixteen are not errors. All three are lookalikes: a character that renders like the ASCII one and parses like nothing.

tg check warn/ then --strict
$ npx typeglish check warn
warn/curly.tg:9:84  warn   structure/curly-quote  A curly apostrophe is a lookalike: the parsers read ASCII ', so possession and contractions written with it are invisible to the consistency checker. Straighten it.
warn/endash.tg:20:44  warn   structure/em-dash  A typographic dash hides its break from the ASCII parsers, and models mirror the tic (the canonical AI-tell). Its plain form is the spaced hyphen: " - ".
warn/period.tg:14:3  warn   structure/missing-period  Unterminated statement — end it with a period (or ! ?; a lead-in may end with ":"). Statement boundaries are a compile contract.

 3 files — 0 error, 3 warning, 0 info
program: 3 independent files — no $IMPORT compositions

$ npx typeglish check warn --strict
warn/curly.tg:9:84  error  structure/curly-quote  A curly apostrophe is a lookalike: the parsers read ASCII ', so possession and contractions written with it are invisible to the consistency checker. Straighten it.
warn/endash.tg:20:44  error  structure/em-dash  A typographic dash hides its break from the ASCII parsers, and models mirror the tic (the canonical AI-tell). Its plain form is the spaced hyphen: " - ".
warn/period.tg:14:3  error  structure/missing-period  Unterminated statement — end it with a period (or ! ?; a lead-in may end with ":"). Statement boundaries are a compile contract.

 3 files — 3 error, 0 warning, 0 info
program: 3 independent files — no $IMPORT compositions
All three at B (88/100), four points off the baseline, and all three blocking once --strict is on. If you only take one CI change out of this post, take that flag: this is the class where the default severity is genuinely arguable and the escalation is free.

Two of the three arrive without anybody typing them. A curly apostrophe is what a word processor, a ticketing system or a chat window gives you when you type a straight one, and a typographic dash is what half the writing tools in a marketing team produce from two hyphens. That is the whole argument for the severity: the character is not a mistake in the sense of being unintended, it is a mistake in the sense that a parser reading ASCII now sees a different sentence than the one on your screen.

What does not help here is the formatter. fmt joins wrapped fragments, splits compound rules and normalises whitespace, and it leaves every one of these alone.

tg fmt warn/ --check
$ npx typeglish fmt warn --check
 3 files already formatted
Correct, and worth knowing before you build a pipeline on it. These are lexical decisions, not layout, so the fix rides on the diagnostic rather than on a formatting pass.

§4The three that ship

Here is the part of the run that made it a post.

tg check silent/ - three variants, with and without --strict✓ 0 error
$ npx typeglish check silent
 3 files — 0 error, 0 warning, 0 info
program: 3 independent files — no $IMPORT compositions

$ npx typeglish check silent --strict
 3 files — 0 error, 0 warning, 0 info
program: 3 independent files — no $IMPORT compositions

$ npx typeglish score care.tg
care.tg — A (92/100)  proven errors: none  tiers: base+z3+spell
$ npx typeglish score silent/bound_digit.tg
silent/bound_digit.tg — A (92/100)  proven errors: none  tiers: base+z3+spell
$ npx typeglish score silent/modal_case.tg
silent/modal_case.tg — A (92/100)  proven errors: none  tiers: base+z3+spell
$ npx typeglish score silent/prose_typo.tg
silent/prose_typo.tg — A (91/100)  proven errors: none  tiers: base+z3+spell
Nothing to fix, nothing to escalate, and two of the three land on the baseline grade to the point. There is no severity dial that reaches this class, because there is no finding to escalate.

One of the three is correct and should be silent. MUST and Must are the same operator, because every operator in this language is case-insensitive, so modal_case is not a defect at all: the file means exactly what it meant, and the only trace is that the artifact carries the capitalisation you typed.

The other two are the interesting ones, and they share one property. Each leaves a grammatical English sentence behind.

too.tg - a preposition became an adverb✓ 0 error
<role>
  You are a care agent for Larkspur Broadband.
</role>

<constraints>
  - You MUST keep every reply too at most 3 sentences.
</constraints>
0 error, 0 warning, 0 info at A (92/100). Not one of the sixteen originally, just the cheapest demonstration of the rule: too is a word, so no tier has anything to object to, and the bound is still parsed and still provable. The sentence is wrong in the way only a reader notices.

bound_digit is the same shape with worse consequences: at most 3 sentences becomes at most 8 sentences, and 8 is exactly as well-formed a bound as 3. Every facet is identical, because there is nothing in the file for the new number to argue with. A checker that flagged this would be flagging the possibility that you changed your mind, which is most of what a prompt repository does.

And prose_typo is where the two commands come apart. A misspelling in a rule is not in the check report at all, and it is in the score ledger.

tg score silent/prose_typo.tg - the row check never printed
$ npx typeglish check silent/prose_typo.tg
 1 file — 0 error, 0 warning, 0 info

$ npx typeglish score silent/prose_typo.tg
silent/prose_typo.tg — A (91/100)  proven errors: none  tiers: base+z3+spell
  planes  runtime 97 (what the model reads) · hygiene 75 (source only)
  facets  enforceability 91 x.21 · hardness 100 x.12 · directness 100 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 50 x.12 (hygiene) · style 93 x.08 · security 100 x.08
  lever   annotation 50/100 (up to +6 overall) — Put a "@@ why" note directly above each statement ("@@ name: why" also names it).
  L9  spelling/unknown-word        −0.25  Unrecognized word — "Broadbnad" isn't in the dictionary. Fix the spelling, or
One point of style, one row in the ledger, and no diagnostic. If your gate is check and nothing else, spelling is not in your pipeline at all. Two more surfaces we checked while we were there: the same misspelling inside an @@ annotation moves nothing, because annotations never compile, and a typo that lands on a real word moves nothing anywhere.
The compiler defends the form of your prompt. Nothing defends the meaning of a sentence that is still a sentence.

Which is the whole asymmetry, and it is not a gap so much as the definition of the job. Ten of the sixteen edits produced text with no valid reading: a command with no sigil, a domain that is not a domain, a chain arm with no chain, a pointer name outside the grammar. There is exactly one thing to say about text like that, and the compiler says it, usually with the rewrite attached. The other three produced text with a valid reading that happens not to be yours, and telling those apart requires knowing what you meant.

§5A gate for the quiet three

The quiet class has one property you can gate on: all three change what the model reads. That is not true of most edits to a .tg file, where annotations, tests, config and scaffolding never reach the artifact at all, and it is what makes the build hash a sharper instrument here than the checker.

tg build - four files, four hashes
$ npx typeglish build care.tg silent/*.tg
 built .typeglish/dist/care.txt ← care.tg (82cbd42c61b8, full)
 built .typeglish/dist/silent/bound_digit.txt ← silent/bound_digit.tg (a69be5a420d9, full)
 built .typeglish/dist/silent/modal_case.txt ← silent/modal_case.tg (46832016db8f, full)
 built .typeglish/dist/silent/prose_typo.txt ← silent/prose_typo.tg (114f9cb464b7, full)
 built 4/4

$ diff dist/care.txt dist/silent/bound_digit.txt
6c6
< - You MUST keep every reply to at most 3 sentences.
---
> - You MUST keep every reply to at most 8 sentences.

$ diff dist/care.txt dist/silent/prose_typo.txt
2c2
< You are a care agent for Larkspur Broadband. You do not have access to a customer's payment card.
---
> You are a care agent for Larkspur Broadbnad. You do not have access to a customer's payment card.
Four builds, four different hashes, three one-line diffs. The checker cannot see these edits and the build cannot miss them, because the artifact is a byte string and a byte string either matches or does not.

So the gate is nine lines: build the file, read the twelve-character hash out of the manifest, and compare it to the one somebody approved.

hash-gate.mjs - nine lines
// A keystroke that changes what the model reads changes the artifact hash. Nothing else has to notice.
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
const [, , file] = process.argv;
execFileSync('npx', ['typeglish', 'build', file], { stdio: 'pipe' });
const { builds } = JSON.parse(readFileSync('.typeglish/build-manifest.json', 'utf8'));
const built = builds.find((b) => b.source === file).artifactSha256.slice(0, 12);
const approved = readFileSync('approved.txt', 'utf8').trim();
console.log(built === approved ? `approved  ${built}` : `CHANGED   ${approved} -> ${built}`);
process.exit(built === approved ? 0 : 1);
The twelve characters the build prints are the first twelve of artifactSha256 in .typeglish/build-manifest.json, so you can read them out of the manifest rather than scraping stdout.
node hash-gate.mjs - the baseline, then the three silent variants
$ node hash-gate.mjs care.tg
approved  82cbd42c61b8
exit 0

$ node hash-gate.mjs silent/bound_digit.tg
CHANGED   82cbd42c61b8 -> a69be5a420d9
exit 1

$ node hash-gate.mjs silent/modal_case.tg
CHANGED   82cbd42c61b8 -> 46832016db8f
exit 1

$ node hash-gate.mjs silent/prose_typo.tg
CHANGED   82cbd42c61b8 -> 114f9cb464b7
exit 1
Exit 0 on the baseline, exit 1 on all three. It catches every quiet typo in this run and it also catches every deliberate policy change, which is the honest description of what it is: not a defect detector, a re-approval trigger.

Be clear-eyed about that trade before you wire it in. This gate has a false-positive rate of one hundred percent on intended edits, so it is only tolerable where the artifact changes rarely and the approval is cheap: one reviewer, one line in the pull request, the twelve characters and the diff. On a prompt that changes twice a day it is noise. On the file that runs your contact centre, which is the case this blog keeps writing about in the artifact you actually deployed, it is the difference between reading a diff and trusting one.

Two smaller habits from the same run, both free. Keep --strict on in CI, which converts the three lookalikes from a warning nobody reads into a failure. And run score alongside check rather than instead of it, because the spelling row lives in one and not the other, and a misspelled brand name in the role line is the single most embarrassing typo on this list.

§6Common questions

Will TypeGlish catch a typo in my agent system prompt?
Most of them, and it tells you which kind you have by how loud it is. Across sixteen one-character edits to a clean care prompt, ten were blocking errors, three were warnings that become errors under --strict, and three produced 0 error, 0 warning, 0 info. The dividing line is not severity, it is grammar: a keystroke that breaks the FORM of a declaration, a pointer, a domain, a block or a chain has no valid reading, so the compiler names it. A keystroke that leaves a grammatical English sentence behind has a valid reading, and a checker cannot tell a valid reading from the one you meant.
Why did my prompt still pass after I mistyped a number?
Because a number typo is indistinguishable from a policy change. Changing at most 3 sentences to at most 8 sentences leaves the file at 0 error, 0 warning, 0 info and at A (92/100), the identical grade, with every facet unmoved, because 8 is as well-formed a bound as 3 and nothing else in the file argues with it. The one thing that does move is the build: the artifact hash goes from 82cbd42c61b8 to a69be5a420d9, so a gate on the approved hash catches it and the checker never will.
Does typeglish check report spelling mistakes in a prompt?
No, and typeglish score does. A misspelled word in a rule (custmer for customer) is 0 error, 0 warning, 0 info under check, and the same file scores A (91/100) with a spelling/unknown-word row in the ledger worth -0.25. So if your CI only runs check, you never see spelling at all. Two more surfaces are worth knowing: a misspelling inside an @@ annotation is invisible to both, because annotations never compile, and a typo that lands on a real word (too for to) draws nothing anywhere.
Should I run typeglish check with --strict on an agent prompt?
Yes, for the lookalike class specifically. A curly apostrophe, a typographic dash and a missing period are all warnings by default and all three become blocking errors under --strict, which matters because each of them hides something from a parser rather than breaking it: a curly apostrophe conceals a possession, a dash splices two statements onto one line, and a missing period leaves a statement boundary undeclared. Note that --strict changes nothing about the quiet class. The three silent edits are still 0 error, 0 warning, 0 info under --strict.
Field note

Run this on your own file and the number that matters is not ten out of sixteen, it is which of your constructs are load-bearing. A prompt made of pointers, typed domains, tool declarations and chains has most of its surface defended by the grammar, because every one of those constructs is a name that has to match something else in the file. A prompt made of flat prose rules has almost none, and it is the same lesson as deleting a line at a time read one character down: a defect is visible exactly when the text participates in a reference. The practical version is short. Every construct you replace with plain English buys you a sentence the model reads more easily and costs you a keystroke the compiler can no longer catch, and that trade is worth making deliberately rather than by habit.

∿ washed up Sep 1, 2026 ∿