Adversarially Validating Game Repairs
Purpose
A fix that makes the symptom disappear has proven one thing: the symptom disappears under the one
condition that was tried. That is compatible with the fix being correct, and equally compatible with
it being a guard at the reader, a clamp on the output, a special case that happens to cover the
reported input, or a change that works only at 60 fps on seed 2001.
This procedure attacks the fix with conditions its author did not have in mind, and produces a
record an independent reader can check without re-deriving the author's reasoning.
When to Use
- A patch exists, the reported symptom is gone, and confidence in why is low.
- The fix touches lifecycle, identity, timing, ordering, persistence, or a shared/pooled resource —
the areas where a locally correct change is most often globally wrong.
- Before shipping a repair to a mechanic that players can grind, repeat, or abuse.
When Not to Use
- No patch yet. There is nothing to attack.
- The build crashes or throws — establish runtime health first; adversarial conditions on a build
that does not run produce noise.
- The question is whether a mechanic matches its written spec, or whether the game is well
balanced. Both are different questions with different instruments.
- The change is cosmetic and cannot reach game state.
Required Inputs
The patch, as a diff.
The ReproCase the patch was written against — minimal contract, inlined so this skill needs
nothing else:
id: # stable name for this defect
seed: # RNG seed, or "none"
build: # commit / build identifier
setup: # starting state or save slot
inputs: # ordered input events with tick indices — the input tape
observe: # the wrong observable, as a value
expected: # the healthy value
determinism: # always | intermittent(n/m runs)
The invariant the patch claims to restore, stated as a checkable predicate.
Whatever passing checks existed before the patch, so pass-to-pass has a baseline.
Procedure
Restate the claim without the author's explanation. Write, from the diff alone, what the
patch changes and what would have to be true for that to fix the reported symptom. Do not read
the author's rationale first, and do not treat it as evidence when you do. An explanation is a
hypothesis with a motive; the diff is the artifact.
Establish fail-to-pass and relevant pass-to-pass. Run the ReproCase on the pre-patch build (must
fail) and the post-patch build (must pass). A repro that does not fail before the patch
invalidates everything downstream — stop and fix the repro. Then run the existing healthy checks
whose paths the patch can reach and confirm they still pass. State what those checks exercise;
an unrelated green suite is not evidence. Record counts, not adjectives.
Select adversarial cases by reachability and failure cost. Use
references/adversarial-conditions.md as a routing catalog, not a universal checklist. Map the
diff and restored invariant to the state surfaces they can reach, then run the cheapest cases
that cover those surfaces:
- vary seeds only when the patched path consumes or depends on RNG;
- vary frame/tick rate only when it reaches elapsed time, frame counts, deadlines, or ordering;
- exercise pause/resume, scene transitions, or save/load only when it reaches that lifecycle;
- exercise pooling, identity reuse, same-tick events, or deferred completions only when it
reaches identity, ordering, or lifetime;
- measure performance, memory, or build time only when a prior budget exists and the patch can
affect it.
Do not enumerate catalog rows the patch cannot reach. Record a reachable, high-cost condition in
untested_scope when it matters but cannot be run.
Attack the fix's shape, not only its inputs. For each of these, decide yes/no with evidence:
- Does the patch change the first divergent write, or add a check downstream of it? A guard
at the reader leaves the bad write in place and fails the moment a second reader appears.
- Does it clamp an output (
max(0, hp), min(cap, score)) rather than prevent the bad
value? Clamping converts a visible defect into a silent one.
- Does it special-case the reported input — a condition mentioning the exact entity, level,
seed, or count from the bug report?
- Does it change only a constant where the report describes wrong logic? Retuning a number is
not a logic repair, and the two are routinely confused because both make the symptom go away.
- Does it introduce state that a pool, restore, or scene reload can carry across a lifetime
boundary?
Test the inverse. Construct a case where the patched code path should not trigger and
confirm it does not. A fix that suppresses the symptom by suppressing the whole mechanic passes
every test aimed at the bug.
Judge by outcome, never by diff similarity. If a reference or "correct" patch exists, do not
compare against it. A different change that restores the invariant is correct; an identical
change that does not is not.
Do not let the fix's author supply the verdict. The evidence must be reconstructable from the
diff, the ReproCase, and the recorded runs alone. If a claim in the record can only be checked
by asking the author what they meant, it is not evidence — rewrite it or drop it.
Stop Conditions
- The repro does not fail pre-patch. Stop at step 2. Nothing after it means anything.
- Any adversarial case reproduces the original symptom. Stop and report; the patch is
incomplete. Do not extend the patch and re-run in the same pass — a fix shaped around the case
that caught it is a bigger overfit than the one you started with.
- The invariant cannot be written as a predicate. Report that the patch is unvalidatable by this
method and say what a human has to judge instead.
- Behavior is indistinguishable with and without the patch across every case. Report
undecidable. Do not resolve it by preferring the patched version.
Output
A PatchEvidence record. Field list and a worked example are in references/patch-evidence.md;
the required fields are:
patch_id:
root_cause_hypothesis:
violated_invariant:
fail_to_pass:
pass_to_pass:
adversarial_cases:
cross_configuration:
performance_impact:
fix_shape:
inverse_test:
untested_scope:
confidence: # high | medium | low | undecidable
human_decisions_required:
human_decisions_required may not be empty. untested_scope contains important conditions the
patch plausibly reaches but this pass did not run; use none identified from patch reachability
when there are none. That phrase is scoped to the patch, not a claim of exhaustive game-state
coverage.
Validation
fail_to_pass names a run that actually failed before the patch.
pass_to_pass reaches the patched subsystem, and an inverse case confirms the patched path does
not fire when it should not.
- The restored invariant is evaluated directly on the post-patch repro.
- Every selected adversarial case names the patch surface that made it relevant.
confidence is justified by the recorded runs and downgraded to low when untested_scope
covers a condition the patch plausibly reaches.
- No field cites the author's explanation as its support.
- Steps 4 and 5 are answered explicitly, not implied by the other results.
Common Failure Modes
- Grading the explanation. The rationale is coherent, so the patch is accepted. Coherent
rationales accompany wrong patches at roughly the rate they accompany right ones.
- Reporting the pre-existing suite as pass-to-pass. A suite that never covered the mechanic
passes before and after and proves nothing. Say what the passing checks actually reach.
- Skipping a reached source of variation. If the patched path consumes RNG, crosses a lifetime
boundary, or depends on event timing, the corresponding condition is relevant even when the diff
looks locally deterministic.
- Confusing "symptom gone" with "invariant restored". These come apart exactly when the patch
is wrong, which is the case worth detecting.
- Extending the patch mid-run. Each extension invalidates the evidence collected so far, and the
record ends up describing a patch that no longer exists.
References
references/adversarial-conditions.md — the condition catalog, with the setup and the observable
break for each.
references/patch-evidence.md — full PatchEvidence schema and a worked example.
1---2name: adversarially-validating-game-repairs-23description: Stress-tests an already-written game bug fix with adversarial cases selected from the state, timing, identity, lifecycle, persistence, and input surfaces the patch can actually reach, then returns a PatchEvidence record covering fail-to-pass, relevant pass-to-pass, inverse-case, invariant, and untested-scope evidence. Use when a fix makes the reported symptom go away and it is still unknown whether the fix is the real one or an overfit, coincidental, or plausible-but-wrong one. Not for finding unknown defects in a build (that is runtime smoke testing), checking one mechanic against its written spec, or judging difficulty, fun, or balance.4---56# Adversarially Validating Game Repairs78## Purpose910A fix that makes the symptom disappear has proven one thing: the symptom disappears under the one11condition that was tried. That is compatible with the fix being correct, and equally compatible with12it being a guard at the reader, a clamp on the output, a special case that happens to cover the13reported input, or a change that works only at 60 fps on seed 2001.1415This procedure attacks the fix with conditions its author did not have in mind, and produces a16record an independent reader can check without re-deriving the author's reasoning.1718## When to Use1920- A patch exists, the reported symptom is gone, and confidence in *why* is low.21- The fix touches lifecycle, identity, timing, ordering, persistence, or a shared/pooled resource —22 the areas where a locally correct change is most often globally wrong.23- Before shipping a repair to a mechanic that players can grind, repeat, or abuse.2425## When Not to Use2627- No patch yet. There is nothing to attack.28- The build crashes or throws — establish runtime health first; adversarial conditions on a build29 that does not run produce noise.30- The question is whether a mechanic matches its written spec, or whether the game is well31 balanced. Both are different questions with different instruments.32- The change is cosmetic and cannot reach game state.3334## Required Inputs3536- The patch, as a diff.37- The `ReproCase` the patch was written against — minimal contract, inlined so this skill needs38 nothing else:3940 ```yaml41 id: # stable name for this defect42 seed: # RNG seed, or "none"43 build: # commit / build identifier44 setup: # starting state or save slot45 inputs: # ordered input events with tick indices — the input tape46 observe: # the wrong observable, as a value47 expected: # the healthy value48 determinism: # always | intermittent(n/m runs)49 ```5051- The invariant the patch claims to restore, stated as a checkable predicate.52- Whatever passing checks existed before the patch, so pass-to-pass has a baseline.5354## Procedure55561. **Restate the claim without the author's explanation.** Write, from the diff alone, what the57 patch changes and what would have to be true for that to fix the reported symptom. Do not read58 the author's rationale first, and do not treat it as evidence when you do. An explanation is a59 hypothesis with a motive; the diff is the artifact.60612. **Establish fail-to-pass and relevant pass-to-pass.** Run the `ReproCase` on the pre-patch build (must62 fail) and the post-patch build (must pass). A repro that does not fail before the patch63 invalidates everything downstream — stop and fix the repro. Then run the existing healthy checks64 whose paths the patch can reach and confirm they still pass. State what those checks exercise;65 an unrelated green suite is not evidence. Record counts, not adjectives.66673. **Select adversarial cases by reachability and failure cost.** Use68 `references/adversarial-conditions.md` as a routing catalog, not a universal checklist. Map the69 diff and restored invariant to the state surfaces they can reach, then run the cheapest cases70 that cover those surfaces:71 - vary seeds only when the patched path consumes or depends on RNG;72 - vary frame/tick rate only when it reaches elapsed time, frame counts, deadlines, or ordering;73 - exercise pause/resume, scene transitions, or save/load only when it reaches that lifecycle;74 - exercise pooling, identity reuse, same-tick events, or deferred completions only when it75 reaches identity, ordering, or lifetime;76 - measure performance, memory, or build time only when a prior budget exists and the patch can77 affect it.7879 Do not enumerate catalog rows the patch cannot reach. Record a reachable, high-cost condition in80 `untested_scope` when it matters but cannot be run.81824. **Attack the fix's shape, not only its inputs.** For each of these, decide yes/no with evidence:83 - Does the patch change the **first divergent write**, or add a check downstream of it? A guard84 at the reader leaves the bad write in place and fails the moment a second reader appears.85 - Does it **clamp an output** (`max(0, hp)`, `min(cap, score)`) rather than prevent the bad86 value? Clamping converts a visible defect into a silent one.87 - Does it **special-case the reported input** — a condition mentioning the exact entity, level,88 seed, or count from the bug report?89 - Does it change **only a constant** where the report describes wrong logic? Retuning a number is90 not a logic repair, and the two are routinely confused because both make the symptom go away.91 - Does it introduce state that a **pool, restore, or scene reload** can carry across a lifetime92 boundary?93945. **Test the inverse.** Construct a case where the patched code path *should not* trigger and95 confirm it does not. A fix that suppresses the symptom by suppressing the whole mechanic passes96 every test aimed at the bug.97986. **Judge by outcome, never by diff similarity.** If a reference or "correct" patch exists, do not99 compare against it. A different change that restores the invariant is correct; an identical100 change that does not is not.1011027. **Do not let the fix's author supply the verdict.** The evidence must be reconstructable from the103 diff, the `ReproCase`, and the recorded runs alone. If a claim in the record can only be checked104 by asking the author what they meant, it is not evidence — rewrite it or drop it.105106## Stop Conditions107108- **The repro does not fail pre-patch.** Stop at step 2. Nothing after it means anything.109- **Any adversarial case reproduces the original symptom.** Stop and report; the patch is110 incomplete. Do not extend the patch and re-run in the same pass — a fix shaped around the case111 that caught it is a bigger overfit than the one you started with.112- **The invariant cannot be written as a predicate.** Report that the patch is unvalidatable by this113 method and say what a human has to judge instead.114- **Behavior is indistinguishable with and without the patch across every case.** Report115 `undecidable`. Do not resolve it by preferring the patched version.116117## Output118119A `PatchEvidence` record. Field list and a worked example are in `references/patch-evidence.md`;120the required fields are:121122```yaml123patch_id:124root_cause_hypothesis:125violated_invariant:126fail_to_pass:127pass_to_pass:128adversarial_cases:129cross_configuration:130performance_impact:131fix_shape:132inverse_test:133untested_scope:134confidence: # high | medium | low | undecidable135human_decisions_required:136```137138`human_decisions_required` may not be empty. `untested_scope` contains important conditions the139patch plausibly reaches but this pass did not run; use `none identified from patch reachability`140when there are none. That phrase is scoped to the patch, not a claim of exhaustive game-state141coverage.142143## Validation144145- `fail_to_pass` names a run that actually failed before the patch.146- `pass_to_pass` reaches the patched subsystem, and an inverse case confirms the patched path does147 not fire when it should not.148- The restored invariant is evaluated directly on the post-patch repro.149- Every selected adversarial case names the patch surface that made it relevant.150- `confidence` is justified by the recorded runs and downgraded to `low` when `untested_scope`151 covers a condition the patch plausibly reaches.152- No field cites the author's explanation as its support.153- Steps 4 and 5 are answered explicitly, not implied by the other results.154155## Common Failure Modes156157- **Grading the explanation.** The rationale is coherent, so the patch is accepted. Coherent158 rationales accompany wrong patches at roughly the rate they accompany right ones.159- **Reporting the pre-existing suite as pass-to-pass.** A suite that never covered the mechanic160 passes before and after and proves nothing. Say what the passing checks actually reach.161- **Skipping a reached source of variation.** If the patched path consumes RNG, crosses a lifetime162 boundary, or depends on event timing, the corresponding condition is relevant even when the diff163 looks locally deterministic.164- **Confusing "symptom gone" with "invariant restored".** These come apart exactly when the patch165 is wrong, which is the case worth detecting.166- **Extending the patch mid-run.** Each extension invalidates the evidence collected so far, and the167 record ends up describing a patch that no longer exists.168169## References170171- `references/adversarial-conditions.md` — the condition catalog, with the setup and the observable172 break for each.173- `references/patch-evidence.md` — full `PatchEvidence` schema and a worked example.