Every diff gets exactly one label, and the label decides what happens next.
A bug is a diff where the incumbent is right and the candidate is wrong on the merits, or where the candidate crashed, timed out, or produced a verdict outside its allowed set. Bugs get a ticket, a fix, and a re-run on the same row. The re-run should be stored alongside the original so you can see the fix confirmed.
An improvement is a diff where the candidate is right and the incumbent was wrong or too coarse. Improvements get logged with a short justification and an owner who is willing to accept the behavior change. A candidate can be perfectly correct and still not cut over, because a deployment state is a decision about appetite as much as about correctness; the improvement diffs are where you record that appetite.
An environment artifact is a diff caused by something other than checker logic. Typical causes: the row changed between when the incumbent saw it and when the candidate did, a lookup service returned a different answer, a time-dependent rule fired differently because the two evaluations straddled a boundary, or a feature is present in one environment and not the other. Artifacts are not bugs, but they are not free either: a high artifact rate means your shadow window is not measuring what you think.
This third class is where most disagreements quietly go to die, and it is where the discipline pays. An audit trail that records who or what changed a value, and when, is what lets you distinguish a real behavior change from an artifact. If you cannot answer "did the input change between the two evaluations?" for a given row, your queue will drift toward blaming the candidate for the environment.
A worked example helps here. Suppose a week of shadowing produces 8,000 rows and 640 disagreements, an 8 percent rate. On review, 390 are artifacts (the row was modified midway through the day by a support fix), 180 are improvements (the incumbent was applying an outdated scope), and 70 are bugs, of which 55 are one parser defect. Fix the parser, re-run those 55 rows, and the true bug count falls to 15. Then fix the artifact source, which in this case is a race between two jobs, and the disagreement rate drops to about 3 percent on day five. That is the shape of a week that ends in a cutover decision, and the only way to see it is to record the four categories separately.
| Label | Definition | Action |
|---|
| Bug | Candidate wrong on the merits, or crashed, timed out or returned an invalid verdict | Ticket, fix, re-run the same rows |
| Improvement | Candidate right, incumbent wrong or too coarse | Log with justification and an owner who accepts it |
| Artifact | Input or environment differed between the two evaluations | Fix the pipeline, not the checker |
| Undecided | Neither side is clearly right | Time-box it, then treat as a bug until proven otherwise |
The "undecided" row matters. Ambiguous matches are a normal part of any matching problem, and recognizing them as a category rather than forcing them into pass or fail is how mature resolution systems are built. If undecided cases are more than a few percent of disagreements, your specification is underspecified, not your checker.