We moved eleven lines. Six shipped a different prompt.
Every ablation on this blog so far has taken something out. This one takes nothing out. Eleven elements of a clean support prompt get moved somewhere else legal, one at a time, and the question is which of those moves the compiler is entitled to have an opinion about.
TL;DR Position is an edit, and a checker can barely see it. Moving eleven elements of a clean A (97/100) support prompt one at a time, three moves refuse to build, two leave the deployed prompt byte-identical, and six ship different bytes to the model at the identical grade with nothing reported. The worst of the six swaps two arms of a $IF chain and turns a free courier collection into a drop-off label on a 600-pound order. Gate position-only edits on the built artifact, because that diff is the only place they exist.
Somebody reorganises the prompt. It happens for good reasons: the persona reads better at the bottom, the constraints block has grown and wants splitting, a note got orphaned when a rule landed between it and its line, ops asked for the survey step earlier. None of it is a policy change. Nobody writes a ticket for it. In review it looks like the diff equivalent of tidying a drawer, and it goes in under a green tick because the green tick is honest: nothing was added and nothing was removed.
So this run does the thing the file cannot tell you about. Eleven variants, each one base.tg with exactly one element moved to a different place in the file, the words untouched. Then check, score and build on all eleven, and a diff of every artifact against the baseline.
$REQUIRE variable order_total: number $TOOL check_stock - description:: Check whether an item is in stock at a depot. - input:: - sku:: string # Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Wren, the order support agent for Ellesmere Tools. # Constraints @@ brevity: three sentences keeps a chat reply scannable - You MUST keep every reply to at most 3 sentences. @@ stock_needs_tool: never guess whether a depot holds an item - WHEN a customer asks about availability THEN you MUST call @[check_stock]. @@ credit_default: goodwill is a supervisor decision - UNLESS an order is more than 5 days late THEN you MUST NOT offer a goodwill credit. @@ credit_exception: a late order is the one case the desk settles itself - WHEN an order is more than 5 days late THEN you MUST offer a goodwill credit. @@ claims: a damaged delivery is a claims decision - WHEN a customer reports a damaged delivery THEN you MUST transfer them to a claims handler. @@ courier: the higher band pays for a courier, the lower band does not $IF @{order_total} is at least 500: - You MUST offer a free courier collection. $ELSE IF @{order_total} is at least 100: - You MUST offer a free drop-off label. $ELSE: - You MUST quote a standard return postage. # Instructions @@ step_greet: a name in the first line tests better than a greeting alone 1. Greet a customer by name. @@ step_order: the order number unlocks every later lookup 2. Confirm an order number. @@ step_answer: answer before offering anything 3. Answer a question. @@ step_survey: the survey goes last or it reads as a brush-off 4. Offer a survey. $TEST damaged - input:: My drill arrived with a cracked case. - expect:: - transfers to a claims handler
$ npx typeglish check base.tg ✓ 1 file — 0 error, 0 warning, 0 info $ npx typeglish score base.tg base.tg — A (97/100) proven errors: none tiers: base+z3 planes runtime 99 (what the model reads) · hygiene 90 (source only) facets enforceability 100 x.21 · hardness 100 x.12 · directness 95 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 79 x.12 (hygiene) · style 100 x.08 · security 100 x.08 lever annotation 79/100 (up to +3 overall) — Put a "@@ why" note directly above each statement ("@@ name: why" also names it). $ npx typeglish build base.tg ✓ built .typeglish/dist/base.txt ← base.tg (e56aa317e9de, full)
e56aa317e9de is the number the rest of this post is about. It is the hash of the bytes the model reads, and it is the only thing in the pipeline that has an opinion about where a line sits.§1Eleven moves
Each variant lives in move/ and differs from the baseline by one relocation. No word changes. Four of the eleven move a control-plane declaration, four move a rule or a section, two move a piece of structure, and one reorders a chain.
# declarations and control plane require.tg the $REQUIRE line drops below the Role sentence tool.tg the $TOOL block moves to the end of the file test_top.tg the $TEST block moves above # Role note.tg one @@ note moves up one line, off its statement # rules and sections brevity.tg the brevity rule moves to the bottom of # Constraints credit_order.tg the exception moves above the default it excepts credit_far.tg the exception moves out of # Constraints into # Instructions role_last.tg the whole # Role section moves to the end of the file # structure and chains steps.tg the numbered procedure is reordered, renumbered 1 to 4 else_first.tg the $ELSE arm moves above the $IF arms.tg the two bands of the chain swap places
$ npx typeglish check move move/else_first.tg:23:1 error structure/bad-switch $ELSE without a deterministic $IF above it at the same column — a chain reads $IF @{x} …: / $ELSE IF …: / $ELSE:. move/else_first.tg:24:1 warn structure/bad-indent Indentation mirrors section nesting: expected column 0 (0 levels deep), found 2. The indent unit is 2 spaces. move/else_first.tg:25:1 error structure/non-exhaustive-switch This chain isn't exhaustive — no arm handles @{order_total}=0.0. Add an arm or an ELSE. move/note.tg:33:1 error structure/stacked-annotation Stacked annotations: a `@@` note documents the ONE statement directly below it, and the line below is another `@@`. Merge the notes into one line, or give each note its own statement. move/require.tg:9:1 error structure/misplaced-command A $-command must sit at the top of the file, outside any content section (it currently follows content on line 6). move/role_last.tg:9:3 info prompt/declared-later `you` is declared on line 28, after its first use here: declare the identity before the rules that use it (the model reads top-down). ✗ 11 files — 4 error, 1 warning, 1 info program: 11 independent files — no $IMPORT compositions
§2The three the language refuses
Take the good news first, because it is real. Three positions are not legal positions, and the compiler says so before anything is written.
require.tg moves the $REQUIRE line from the top of the file to just under the Role sentence. That is structure/misplaced-command, and the message names the content line it now follows. The manifest commands are the file's header, and a header below the body is not a header. This one matters more than it looks: a $REQUIRE that quietly stopped being read would leave @{order_total} dangling in a chain, which is the difference between a typed input and a hole.
note.tg moves one @@ note up a single line, which is the shape a file lands in whenever a note and its statement get separated by an edit. Now two notes sit stacked and the statement below them has no note at all. structure/stacked-annotation is blocking rather than advisory, which is the right call: an annotation is a claim about the line beneath it, and a claim pointing at the wrong line is worse than no claim.
else_first.tg lifts the $ELSE arm above its $IF. Three findings, and the third is the interesting one: structure/non-exhaustive-switch, with a counterexample, @{order_total}=0.0. Moving the default off the bottom of the chain did not just break the syntax, it left a real input with no arm, and the checker went and found that input.
move/arms.tg — A (97/100) proven errors: none tiers: base+z3 move/brevity.tg — A (97/100) proven errors: none tiers: base+z3 move/credit_far.tg — A (97/100) proven errors: none tiers: base+z3 move/credit_order.tg — A (97/100) proven errors: none tiers: base+z3 move/else_first.tg — F (89/100) proven errors — grade capped at F tiers: base+z3 move/note.tg — F (91/100) proven errors — grade capped at F tiers: base+z3 move/require.tg — F (92/100) proven errors — grade capped at F tiers: base+z3 move/role_last.tg — A (97/100) proven errors: none tiers: base+z3 move/steps.tg — A (97/100) proven errors: none tiers: base+z3 move/test_top.tg — A (97/100) proven errors: none tiers: base+z3 move/tool.tg — A (97/100) proven errors: none tiers: base+z3
A (97/100), which is the baseline grade to the point. The grade is a property of how the rules are phrased, and none of these edits rephrased anything.§3The two that really changed nothing
Two of the eight legal moves are worth separating out, because they are the only ones in the set where the silence is complete and correct. Build them and the artifact is not merely similar to the baseline, it is the baseline.
$ npx typeglish build move ✓ built .typeglish/dist/move/arms.txt ← move/arms.tg (d918a14e1d72, full) ✓ built .typeglish/dist/move/brevity.txt ← move/brevity.tg (3b3698dcd92f, full) ✓ built .typeglish/dist/move/credit_far.txt ← move/credit_far.tg (25de28446895, full) ✓ built .typeglish/dist/move/credit_order.txt ← move/credit_order.tg (ec54bfd78c8a, full) typeglish build: move/else_first.tg refused — nothing written typeglish build: move/note.tg refused — nothing written typeglish build: move/require.tg refused — nothing written ✓ built .typeglish/dist/move/role_last.txt ← move/role_last.tg (a40889ecec2a, full) ✓ built .typeglish/dist/move/steps.txt ← move/steps.tg (ae7633f3a649, full) ✓ built .typeglish/dist/move/test_top.txt ← move/test_top.tg (e56aa317e9de, full) ✓ built .typeglish/dist/move/tool.txt ← move/tool.tg (e56aa317e9de, full) ✗ built 8/11 — 3 refused (each named above, nothing written for them)
test_top.tg and tool.tg both come out at e56aa317e9de, which is the baseline's artifact hash, not a near miss. Nothing about the deployed prompt changed, because a $TOOL declaration and a $TEST block are control plane: the tool's schema ships in the build --bundle sidecar, the test suite is graded out of band, and neither contributes a byte to the prompt the model reads. Move them wherever the file is easiest to read. That is genuinely a free edit, and it is the only free edit in this list.
Two moves out of eleven change nothing. Ten reviewers out of ten would have guessed eight.
§4The six that shipped a different prompt
Which leaves six files that check with nothing reported, hold A (97/100), and hand the model different bytes. Here is the whole of what changed, in the only place it exists.
=== brevity === 5d4 < - You MUST keep every reply to at most 3 sentences. 15a15 > - You MUST keep every reply to at most 3 sentences. === credit_order === 7d6 < - UNLESS an order is more than 5 days late THEN you MUST NOT offer a goodwill credit. 8a8 > - UNLESS an order is more than 5 days late THEN you MUST NOT offer a goodwill credit. === credit_far === 8d7 < - WHEN an order is more than 5 days late THEN you MUST offer a goodwill credit. 21a21 > - WHEN an order is more than 5 days late THEN you MUST offer a goodwill credit. === steps === 18,21c18,21 < 1. Greet a customer by name. < 2. Confirm an order number. < 3. Answer a question. < 4. Offer a survey. --- > 1. Offer a survey. > 2. Greet a customer by name. > 3. Confirm an order number. > 4. Answer a question.
credit_far as a support agent rather than as an engineer. The prohibition on goodwill credit now sits in the constraints and the one exception to it sits thirteen lines away, at the bottom of a numbered procedure, next to the survey step. Both rules still ship. The prover is still satisfied, because a guard holds them apart no matter how far apart they are on the page. The model reading it top-down has no such guarantee.steps is the one an ops lead would ask for by name, and it is the clearest case of the compiler having nothing to say because the thing it would need to know is not in the language. A numbered list is prose to this checker. It reads 1. Greet a customer by name. as a bare imperative heading a line, proves what it can about it, and never asks whether 1 comes before 4 for a reason. Offering the survey first is a procedure that will annoy every customer who uses it, and it is 0 error, 0 warning, 0 info at A (97/100). If the order of a procedure is load-bearing, the order has to be stated as a rule the checker can read, not implied by a number. That is the same lesson as how to write a procedure your agent follows in order, arriving from the other direction.
role_last.tg is the only one of the six that gets any signal at all, and it is worth looking at because the signal is exactly right and it is not blocking. Moving the whole # Role section to the end of the file is a real practice, argued for on the grounds that the persona sits closest to the response. The compiler notices something narrower and more checkable.
$ npx typeglish check move/role_last.tg move/role_last.tg:9:3 info prompt/declared-later `you` is declared on line 28, after its first use here: declare the identity before the rules that use it (the model reads top-down). ✓ 1 file — 0 error, 0 warning, 1 info $ diff .typeglish/dist/base.txt .typeglish/dist/move/role_last.txt 1,3d0 < # Role < You are Wren, the order support agent for Ellesmere Tools. < 21a19,21 > > # Role > You are Wren, the order support agent for Ellesmere Tools.
you before the file says who you is. The finding is an info, not an error, and that is the correct severity: the identity does bind, the artifact is valid, and the objection is about reading order rather than logic. It is also the only member of the six with any trace in the report, which makes it the exception that proves the rule.§5The move that changes the answer
And then arms.tg, which is not like the other five. The other five move text the model weighs. This one moves text the compiler evaluates, and a $IF chain is first-true-wins.
@@ courier: the higher band pays for a courier, the lower band does not $IF @{order_total} is at least 100: - You MUST offer a free drop-off label. $ELSE IF @{order_total} is at least 500: - You MUST offer a free courier collection. $ELSE: - You MUST quote a standard return postage.
$ npx typeglish check move/arms.tg ✓ 1 file — 0 error, 0 warning, 0 info $ npx typeglish build base.tg --vars '{"order_total": 600}' --out-dir out/base ✓ built out/base/base.txt ← base.tg (aab9c453f4fc, full) $ npx typeglish build move/arms.tg --vars '{"order_total": 600}' --out-dir out/arms ✓ built out/arms/move/arms.txt ← move/arms.tg (ac538ff92c3b, full) $ diff out/base/base.txt out/arms/move/arms.txt 10c10 < - You MUST offer a free courier collection. --- > - You MUST offer a free drop-off label.
--vars build is where a chain stops being prose and starts being a decision, which is also the whole argument of your system prompt has a compile time.The natural next question is whether some flag catches this. It does not, and it is worth being explicit about that rather than leaving a reader to hope.
$ npx typeglish check move/arms.tg move/brevity.tg move/credit_far.tg \
move/credit_order.tg move/steps.tg move/role_last.tg --strict
move/role_last.tg:9:3 info prompt/declared-later `you` is declared on line 28, after its first use here: declare the identity before the rules that use it (the model reads top-down).
✓ 6 files — 0 error, 0 warning, 1 info
What you can do instead is remove the ordering from the design, so that no future tidy-up can move it. A $IF chain takes ordered predicates that may overlap. A $SWITCH ON a typed input takes members of a closed domain, which are unordered by construction, so shuffling the arms is not a semantic edit and cannot become one.
$REQUIRE variable return_band: one of courier, label, postage # Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Wren, the order support agent for Ellesmere Tools. # Constraints @@ courier: one arm per band, so no arm can shadow the arm below it $SWITCH ON @{return_band} - courier:: - You MUST offer a free courier collection. - label:: - You MUST offer a free drop-off label. - postage:: - You MUST quote a standard return postage.
return_band, and the grade drops to B (87/100) because three conditional arms are less enforceable than three flat rules. What you buy is that the arms cannot shadow each other, and a member added to the domain later fails loudly at every switch instead of silently taking the first arm that fits.# bands_moved.tg is bands.tg with the postage arm moved to the top $ npx typeglish build bands.tg --vars '{"return_band":"courier"}' --out-dir a ✓ built a/bands.txt ← bands.tg (bc7327cd6c1d, full) $ npx typeglish build bands_moved.tg --vars '{"return_band":"courier"}' --out-dir b ✓ built b/bands_moved.txt ← bands_moved.tg (bc7327cd6c1d, full) # and for the other two members label bands.tg 9082444d696c bands_moved.tg 9082444d696c postage bands.tg 8a4e2f9b0d3f bands_moved.tg 8a4e2f9b0d3f
aab9c453f4fc against ac538ff92c3b. That is what "order does not matter here" looks like when it is a property of the construct rather than a hope about the reviewer.§6Common questions
- Does the order of instructions in a system prompt matter?
- To the model, yes, and to a compiler, almost never. Moving eleven elements of a clean
A (97/100)support prompt one at a time, three moves refuse to build and eight pass. Of the eight, two leave the deployed prompt byte-identical because the thing moved was control plane, and six change the bytes the model reads while holding the identical grade and reporting nothing at all. So a checker is the wrong instrument for this question. The instrument is the built artifact: diff it, because that diff is the only place a position-only edit exists. - Will typeglish check catch a rule I moved to the wrong place?
- Only when the position itself is illegal or breaks a structure the language owns. Three moves out of eleven do that: a
$REQUIREdropped below content isstructure/misplaced-command, a@@note pushed off its statement isstructure/stacked-annotation, and an$ELSElifted above its$IFisstructure/bad-switchplus a now non-exhaustive chain. All three refuse to build and cap the grade at F. The other eight are legal positions, and legal is all a checker is asked about. Moving an exception thirteen lines away from the rule it excepts, or putting the survey step first in a numbered procedure, is a legal position and a different prompt. - Can reordering the arms of a conditional change what my agent does?
- Yes, and this is the one move in the set that is genuinely dangerous. A
$IFchain is first-true-wins, so overlapping predicates make the order load-bearing: swap the at least 500 arm below the at least 100 arm and the 500 arm can never be reached. Both files check with nothing reported and both scoreA (97/100). Bind the input and the difference is one line, a free courier collection becoming a drop-off label on a 600-pound order. The structural fix is a$SWITCH ONa typed input, whose arms are members of a closed domain rather than ordered predicates: shuffle those arms and every bound build is byte-identical. - Is there a strict flag that catches a position-only prompt edit?
- No. Running
check --strictover the six moves that changed the deployed prompt reports the same thing the ordinary check does, one non-blocking info on one file, because strict raises the exit code on warnings rather than inventing new findings. There is no flag for this and there should not be one: the compiler cannot know which of two legal orders you meant. Gate on the artifact instead. Build the prompt, hash it, and require a review on any change to the hash, which is the one number that moves every time a position-only edit moves a byte the model reads.
Every ablation on this blog has been looking for the same boundary from a different side, and this one lands on it from the least expected direction. Deleting a line is caught when something else points at it. Stripping a guard is caught when it was holding two rules apart. Replacing a bound with the word it stood for is caught by nothing you run today. Moving a line is caught when the position was part of the language and missed when it was part of your intent, and the ratio here, three loud to eight quiet, is about what the others found too. The useful conclusion is not that the checker is weak. It is that the checker and the artifact answer different questions, and a prompt review that reads only the source diff is reading the wrong document: a position-only edit is invisible in check, invisible in score, and completely legible in a five-line artifact diff. If you are setting that gate up, the prompt is frozen, the artifact is not is the mechanics, and your headings decide which rules get checked is the sibling result for the structure the lines sit inside.