Specification mutations
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at README.md, and llms.txt lists every page.Each .mutation file here describes a deliberate defect in
../AtomicCommit.tla, paired with exactly one property
it must make TLC report as violated. Together they answer a question the base
specification cannot answer about itself: does this property actually fire?
Every property in ../AtomicCommit.cfg has one, and
SpecMutationCatalogueTests fails if that ever stops being true.
Why
The atomicity audit (epic #2299) found verification artefacts that were green because they could not reach the state they were named for. A Coyote model whose harness never scheduled the interleaving it claimed to explore. A refinement row asserting a correspondence its abstraction could not express. An invariant over a variable no action ever set to the value that would violate it. None of them failed. All of them read exactly like verification that works.
That is the whole problem: an artefact that asserts a property it is structurally unable to check is indistinguishable, from the outside, from one that checks it. Passing tells you nothing, because passing is what both cases do. The only thing that separates them is a demonstration that the artefact can go red, and the only honest form of that demonstration is a mutation that makes it do so.
Issue #2323 is the recommendation. This directory is it.
Each mutation is a controlled experiment, not a red run
A mutation is checked in two arms against the same generated single-property cfg:
| Arm | Module | Required outcome |
|---|---|---|
| Control | the unmutated base | clean |
| Treatment | the generated mutant | violates the named property |
Asserting only the second arm would repeat the mistake this directory exists to catch. A mutant can go red for reasons that have nothing to do with the property under test: a typo that makes the module unparseable, an accidental out-of-domain variable, a cfg naming the wrong thing. A one-armed test cannot distinguish "this property caught the defect" from "something went wrong". The control arm is what turns a red mutant into evidence, and it is also the standing proof that the harness is not vacuous, since the same property, the same cfg and the same machinery produce green on one input and red on the other.
Mutations are generated from the base, never checked in as copies
The obvious design is to check in a full mutated copy of the module beside the
base. That was the proof of concept, and it has a defect that only surfaces
months later: an edit to AtomicCommit.tla does not propagate to the copies,
nothing detects that it did not, and a mutant that has drifted far enough has
quietly stopped being evidence about the base while still passing. That is the
audit's own failure shape, reintroduced by the fix for it.
A test comparing mutant against base could detect that drift. Deriving the
mutant from the current base at run time makes it unexpressible, which is
strictly better, because there is no second copy to fall behind. So a
.mutation file stores only the difference, as anchored edits, and
SpecMutation.Apply requires each anchor to match the current base exactly
once:
- zero matches means the anchored region was edited, so the mutation has genuinely drifted and must be re-derived. The failure names the mutation, the edit and the anchor text, and arrives in milliseconds without running TLC.
- more than one match means the anchor is ambiguous and the edit could land somewhere unintended. Widen it with surrounding context.
- an edit to the base that does not touch an anchored region is absorbed silently and correctly, which is what stops this being a tax on every change to the specification.
The cfg is generated too, from the target property plus the base cfg's
CONSTANTS block. That removes a specific trap the audit hit: a cfg that
prepended a property to the base list instead of replacing it produced six
satisfiability branches rather than two, and reported two violations attributed
to the wrong properties. It read as a confident result.
File format
Deliberately dull. KEY: value metadata, then one or more
--- FIND / --- REPLACE / --- END blocks holding verbatim TLA+ text.
Before the first block, a line starting with # is a comment and a blank line
is skipped; every mutation here opens with such a comment, naming the property
it pairs with and the defect it stands for. Anything cleverer would make a
mutation harder to review than the specification it mutates, and a mutation
nobody can review is not evidence.
MODULE: TerminationNoFairness
TARGET: Termination
CLASS: Temporal
SUMMARY: drops the weak-fairness conjunct from Spec, so a saga may stall forever
--- FIND
Spec == Init /\ [][Next]_vars /\ \A t \in Txns : WF_vars(TxProgress(t))
--- REPLACE
Spec == Init /\ [][Next]_vars
--- END
MODULE must be a valid TLA+ module name. The harness writes the generated
mutant to <MODULE>.tla, so the filename follows from it rather than the other
way round; keeping the two the same is a convention for readability, not a
constraint TLA+ imposes. CLASS is Invariant, Action or Temporal,
and selects which violation banner the harness expects.
TypeOK rides along in every cfg
Every generated cfg checks TypeOK alongside the target. A mutation that
accidentally puts a variable outside its declared domain would otherwise make
the target fire for a reason unrelated to the property, and the run would look
like a successful pairing. With TypeOK present, TLC reports TypeOK instead,
the banner assertion fails, and the mistake surfaces as a mistake.
RevisionMonotonicRollback is the mutation where this matters most: it
decrements the revision counter but deliberately stays inside
0..(2 * Cardinality(Txns)), so TypeOK still holds and monotonicity is the
only property that can catch it.
TLC does not name the property for a liveness violation
Worth stating plainly, because it is a real gap rather than an oversight:
| Class | Banner | Names the property? |
|---|---|---|
| Invariant | Error: Invariant <Name> is violated. |
yes |
| Action property | Error: Action property <Name> is violated. |
yes |
| Liveness | Error: Temporal properties were violated. |
no |
So Termination and EveryCommittedKeyReadable cannot have their banner
checked against the property name. For those two the guard against a
misattributed violation is that the generated cfg names exactly one property,
backed by the harness asserting the violation count is one.
Inventory
All twelve properties are paired.
Every_property_the_base_model_checks_has_a_mutation reads the base cfg and
fails if a property is added without one, so this table cannot silently fall
behind.
| Mutation | Property it makes fire | Class | The defect it models |
|---|---|---|---|
TypeOkRevisionRunaway |
TypeOK |
Invariant | a revision bump with no decision behind it, leaving the declared domain |
AllOrNothingUngatedProjection |
AllOrNothing |
Invariant | reads bypass the gate, so a mid-broadcast saga is seen as a split view |
VisibilityMatchesDecisionTerminalPresence |
VisibilityMatchesDecision |
Invariant | any terminal counts as visible, so an aborted saga's writes surface |
StrictIsolationInflightSurfaces |
StrictIsolation |
Invariant | absence of a decision reads as permission, so in-flight writes are readable |
CommitIntegrityIgnoresVotes |
CommitIntegrity |
Invariant | the coordinator commits without consulting the prepare votes |
LinearizedTerminalsBroadcastBeforeDecision |
LinearizedTerminals |
Invariant | a leaf applies a terminal before the registry records the decision |
NoMixedTerminalsPerKeyVote |
NoMixedTerminals |
Invariant | each leaf takes its outcome from its own vote, so one saga does both |
DecisionDurabilityDecisionFlip |
DecisionDurability |
Action | a recorded terminal decision is overwritten after the fact |
MonotonicVisibilityOrphanShadows |
MonotonicVisibility |
Action | a late reshard orphan shadows the committed projection (the #1584 class) |
RevisionMonotonicRollback |
RevisionMonotonic |
Action | a stale registry write replays over a newer one |
TerminationNoFairness |
Termination |
Temporal | no fairness, so a saga may stall forever |
EveryCommittedKeyReadableStaleProjection |
EveryCommittedKeyReadable |
Temporal | the projection never switches to the post-saga value after a commit |
Some mutations break more than their target, and that is fine
The visibility properties are deliberately interrelated:
VisibilityMatchesDecision is the sharpened form of both AllOrNothing and
StrictIsolation, so a defect in the gate tends to trip several at once.
Because each generated cfg names only its target, that overlap cannot mislead
the harness. It does mean a mutation is evidence that its target catches the
defect, not that its target is the only property that would.
Two pairs are chosen specifically to avoid that ambiguity where it would matter.
RevisionMonotonicRollback stays inside the TypeOK domain, as above.
EveryCommittedKeyReadableStaleProjection breaks the leads-to while leaving
every saga terminating normally, rather than taking the easy route of dropping
fairness, which would fire Termination as well and so prove nothing about the
property it is paired with.
Nine mutations perturb the protocol; three add an action it does not have
This distinction matters, and reading past it would reproduce in miniature the overclaim this whole directory exists to prevent.
Nine mutations change something the protocol actually does: a guard, a gate definition, a projection, or the fairness assumption. For those, the pairing shows the property constrains the modelled protocol - weaken the protocol and the property notices.
Three do not. TypeOkRevisionRunaway, DecisionDurabilityDecisionFlip and
RevisionMonotonicRollback splice a brand-new action into Next
(RevisionRunaway, DecisionFlip, RevisionRollback) that models no step of
the protocol. They do this because the properties they target are
unfalsifiable by any behaviour of the base module: decision is written by
exactly one action, DecideTx, under a guard of phase[t] = "prepared" that
the same action immediately leaves, and no action ever re-enters "prepared",
so it is assigned at most once per saga; revision is only ever incremented,
once by DecideTx and at most once more by ForgetDecision, which sets the
forgotten[t] flag its own guard requires to be clear. So
DecisionDurability's two no-flip conjuncts, RevisionMonotonic and TypeOK's
0..(2 * Cardinality(Txns)) revision conjunct cannot fail however the base is
scheduled.
DecisionDurability's third conjunct is the exception, and its pairing does
not reach it. That conjunct forbids retiring a decision's row while a written
key has not yet applied its terminal, and ForgetDecision is a protocol action
whose drain guard is all that prevents it: weaken the guard and the conjunct
fires. DecisionDurabilityDecisionFlip exercises only the flip half, so the
retirement half is covered at the implementation level, by
AtomicCommitInvariantCoyoteTests.Forgetting_the_decision_before_every_leaf_drained_violates_decision_durability
(see ../Refinement.md), rather than by this directory.
For those three the two-arm experiment therefore establishes something weaker than it does for the other nine. It establishes that the property is well-formed: that it is not a tautology, that it says what its name says, and that TLC would report it if the state it forbids became reachable. It does not establish that the property currently constrains the protocol, because nothing in the protocol can violate it.
That is not a defect in the mutations, and it is not a reason to "fix" them by
hunting for a protocol-level perturbation instead - for the flip half and the
two revision properties there is none, which is precisely the point. It is a
limit on what may be concluded, and issue #2323's second supporting control
names it directly: such a result is a classifier,
never evidence of reachability. A future revision that lets a saga re-enter
"prepared" - a retry, a re-prepare, a recovery path - would make these three
properties load-bearing, and the mutations are the standing check that they
would be ready to fire on the day it does.
Every generated cfg names exactly one target, in one block
Issue #2323 records an authoring mistake made during the audit itself: a mutation
cfg was written by prepending a property to the existing PROPERTIES block
instead of replacing it. The result was six conjuncts where one was intended, the
same property named in two blocks, and two separate violations misattributed to
one property - a confidently wrong headline that took four independent routes to
overturn. The tell was in the log the whole time: the satisfiability report showed
six branches where the intended cfg shows two.
The issue asks for two things in response, and the harness does both. Each cfg is
written whole by SpecMutation.BuildConfig rather than edited, so the fault
has no way in; and Every_generated_config_names_its_target_once_in_a_single_block
asserts it anyway, for every mutation, without a JVM.
The assertion is not redundant with the construction. "Unreachable by construction" and "checked" are different states, and only the second survives somebody refactoring the generator back into an edit. Distrusting exactly that distinction is why this directory exists, so it would be incoherent to owe this one to a code-reading. The gate checks the block headers separately from the parsed names, because a repeated block header accumulates into a single list when parsed and is invisible there.
Running one by hand
The modules are generated, so there is nothing here to hand to TLC directly. Run the fixture, which writes each arm into a scratch directory (TLC emits state files beside the module it checks):
dotnet test test/lattice/Orleans.Lattice.Tests.csproj \
--filter "FullyQualifiedName~TerminationNoFairness"
It needs a JVM and tla2tools.jar; see
how the fixture finds them,
and note that it fails rather than skips under GITHUB_ACTIONS.