Engineering

Following one rule turned a passing gate into 16 failures. Both rules were right.

Two policies, each locally correct, sharing one assumption nobody owned.

MobileByteSensei 8 min read
architecturegovernancegates

TL;DR

Two rules in the same codebase. Each one sensible, each enforced by its own tooling, each written by someone who was right.

Obey the first and the second fails — naming a requirement that is demonstrably still satisfied. Not a bug in either rule. They shared an assumption that neither of them stated, and because no one owned the assumption, no one was wrong when it broke.

249 components were exposed. 32 were already failing. Nobody had made a mistake.

The case this post is about

Rule one — keep the executable spine short. A command's runtime file must stay under 200 lines. Past that, a cohesive block moves out to a shared sub-module and leaves a pointer:

> Load layers/<area>/commands/_shared/<name>.md

A pre-write hook enforces it. Try to save a 201-line runtime and the write is refused, with the remedy spelled out. Good rule: it keeps an executable file executable instead of letting it silt up into documentation.

Rule two — every command proves its own contract. Each command has a verify script asserting its runtime contains the clauses the contract requires — the halt classes, the account resolution, the prohibition on passing a secret on the command line. Also a good rule: it is the difference between a documented guarantee and an actual one.

Both have been in place for months. Both have their own enforcement. Neither mentions the other.

The relocation that the size rule prescribes simulated on a different command: every clause the gate requires moved out of the runtime leaving a pointer, zero occurrences remaining in the file the gate used to read, and the gate still passing because it follows the pointer

The symptom

I had a runtime at exactly 200 lines. Every edit I attempted was refused by the hook, correctly, so I did what the rule says: moved one cohesive block — a step with its commands, its halt and its own two > Load lines — verbatim into a sub-module, leaving the heading and a pointer.

Then I ran the command's verify script:

idea-medium-post: 16 failure(s)
  MP-9  no halt class for skipping the visual-asset step
  MP-9  runtime does not dispatch the Stitch producer
  MP-9b runtime does not require the art_source field
  MP-10 runtime does not pass the brief by file (--intent-file)
  MP-10 runtime does not pass a project palette
  …

Sixteen failures. Every one naming a clause that was still there, one file along, exactly where the other rule had just required me to put it.

I had complied with a policy and been punished by a gate, and neither the policy nor the gate was wrong.

Why the obvious fix failed

"Put the content back." The hook refuses the write. The two rules are not merely in tension; in that state they are mutually unsatisfiable — there is no arrangement of that file which passes both.

"The gate is too strict, relax it." The clauses it checks are load-bearing: a missing halt class means a step can be skipped silently, a missing --intent-file means a brief gets mangled by the shell. Deleting the assertion to clear the failure would remove a real guarantee to fix a reporting defect — the costliest trade in the catalogue.

"The slim rule is overreach." Also no. The runtime is read and executed; the 200-line ceiling is what stops it collecting prose until nobody can find the steps.

"Whoever broke it should fix it." Nobody broke it. Both rules shipped working. Both were tested — against their own concern. That is what makes this class hard: there is no commit to revert and no author to ask.

The actual mechanism

The gate asserted on a file. The slim rule moves content between files. Both are reasonable in isolation, and together they encode a shared premise that neither one writes down:

the runtime is one file.

Two rules that both use the word runtime to mean different things: the slim rule moves content between files, the contract gate asserts on one file, and the unstated shared premise between them is that a runtime is a single file

That premise was true when each rule was authored. It stopped being true the first time a runtime crossed 200 lines — and the rule that made it stop being true is the one enforced by a hook, so the breakage arrives automatically, as a consequence of compliance.

Three properties made it invisible for months:

It only fires on compliance. Ignore the slim rule and nothing fails. The gate goes red exactly when someone does the right thing, which inverts the usual signal — the people who trigger it are the ones following policy, and their natural conclusion is that they did the move wrong.

Each rule's tests pass. The slim hook has canaries proving it blocks an oversized file. The verify has canaries proving it catches a missing clause. Neither suite contains a case where a runtime is correctly split, because that scenario belongs to both rules and therefore to neither.

The failure is articulate and wrong. "Runtime does not pass a project palette" is a specific, actionable, confidently-stated claim about a file that does pass a project palette. A vague error invites investigation. A precise false one invites you to go and add the clause that is already there.

Then the scale. One grep across the repository: 249 components had already-split runtimes with gates that read only the runtime file, and 32 of those were red right now — reporting requirements that were sitting one file along, in some cases for weeks. One of them wanted a term that lives in two separate sub-modules the runtime loads.

Nobody had written a bad rule. The number was a property of the pair.

The fix

Make the gate read what the system reads. When the platform executes the command it follows the pointers, so the verification does too:

# The runtime is the runtime PLUS what it loads — one level, deliberately.
RUNTIME_RAW="$REPO_ROOT/layers/<area>/commands/<name>.md"   # preflight-ignore: a path, not a secret
RUNTIME="$(bash "$REPO_ROOT/core/scripts/capsule-runtime-resolve.sh" "$RUNTIME_RAW" "$REPO_ROOT")"
[ "$RUNTIME" != "$RUNTIME_RAW" ] && trap 'rm -f "$RUNTIME"' EXIT

Three details decided whether this was a fix or a second bug.

RUNTIME_RAW survives. Some gates also assert the runtime is under 200 lines. Point that check at the resolved text and it measures the resolution, which is deliberately larger — and the fix would break the very rule it exists to serve. The size check reads the file; the clause checks read the file plus its loads.

One level deep, not recursive. A recursive resolve would let a sub-module pull in half the codebase and still satisfy every assertion, which is the same false green in a longer coat.

A dangling pointer is not swallowed. If a > Load names a file that does not exist, the resolver contributes nothing and the clause check fails honestly. Reporting it separately would count one defect twice.

The deeper fix is the one that is not code: the shared assumption now has a name and an owner. The resolver's header states it — a runtime is a file plus what it loads — so the next rule written against runtimes inherits the corrected premise instead of the old one.

How we proved it

Not by asserting the gate now passes, which it would do if the resolver silently returned nothing.

I simulated the exact relocation the slim rule prescribes on a different command: moved every clause its gate requires out of the runtime and into a sub-module, leaving a > Load.

clauses remaining in the runtime file: 0
gate verdict:                          PASS

Zero occurrences in the file the gate used to read, and it still passes — because it follows the pointer. Before the fix, that same move produced 16 failures. Runtime restored byte-for-byte afterwards.

Then the population, measured rather than estimated:

32 gates targeted  ·  16 now pass  ·  16 still fail

And the 16 survivors are the point. They name clauses that genuinely exist nowhere — one wants a library term absent from the runtime and every sub-module it loads; another sits in a sub-module the runtime never actually loads. The fix removed the failures caused by the rule conflict and left the ones caused by real defects, which is the only outcome that distinguishes a fix from a mute button.

My first attempt at the sweep, incidentally, broke 233 files — but that is its own post.

Two independently good rules with their own enforcement mechanisms, neither referring to the other, so the conflict lives entirely in the gap and each rule's own tests pass

What this generalizes to

The useful question is not "which rule is wrong" — often neither is. It is what do these two rules both believe, that neither of them says?

Look for shared nouns. Both of mine used the word "runtime" and meant different things: one meant a file on disk, the other meant the instructions a command executes. The conflict lived entirely in that gap, and it was invisible because the word was the same.

Suspect any failure that appears on compliance. If doing the right thing turns something red, you are almost certainly looking at a policy pair rather than a defect. It is a distinctive signal and it is routinely misread as "I must have done the move wrong" — which is what I thought for the first ten minutes.

Test the interaction, not just the rule. Each rule had canaries. Neither suite had a case for correctly split and correctly asserted, because that case belongs to both and so was owned by neither. The scenarios that fall between two owners are the ones that reach production.

Count the exposure before you decide it is minor. My instinct was "edge case, one command". One grep said 249 exposed, 32 already failing. The instinct was not slightly wrong; it was wrong by two orders of magnitude, and the correction cost four seconds.

Give the assumption an owner. The code fix stops the current bleeding. What stops the next one is that the premise is now written down somewhere a future rule-author will read — because the real defect was never in either rule. It was in the space between them, where nobody was looking because nobody had been assigned to.

Keep reading

This is part of Architecture decisions & code review — the judgement calls that outlast the code they were made about. Follow MobileByteSensei for the rest.

One post a week, no digest

New writing when it lands. No roundups, no course pitch, no "10 tips".

Follow by RSS