Same-Code-Path Preview
Same-code-path preview
A preview that doesn't share its code path with execution will eventually lie. The only durable preview is one where --dry-run and the real run produce the same selection through the same query.
The rule
The selection logic that previews what would change is the same code path as the real mutation. No parallel "preview" implementation, no "approximate" preview, no read-only mirror of the write path. One function decides "what does this operation touch?"; preview and execute both call it; execute additionally writes.
Why this is non-negotiable
A bulk-archive bug in early mxr: preview said "1,200 messages." Execution archived 12,000. The preview query and the execute query had diverged — different joins, different limits, different ordering. The preview was correct for its own query; the query was wrong. The user saw a number, trusted it, ran the operation, lost a decade of mail.
After that the rule went in the decision log: same code, same selection, no exceptions. Any deviation is the architecture being wrong, not the rule.
What this looks like in practice
A Selection type that captures "what set of rows are we operating on." A function compute_selection(...) -> Selection. Both --dry-run and --yes call it. --dry-run renders the selection; --yes renders it and then mutates. The mutation function takes a Selection, not a query — so the act of mutating can't expand the set.
fn run_mutation(args: &Args) -> Result<()> {
let selection = compute_selection(args)?;
if args.dry_run {
print_preview(&selection);
return Ok(());
}
confirm(&selection)?;
mutate(&selection)
}
The shape is what matters. Compute once; render or execute; never recompute under execute.
Why this generalizes beyond mutations
Any operation with a preview mode is vulnerable to drift between preview and execute. Schema migrations, mass tag updates, billing recalculations, A/B test rollouts. If preview and execute take different paths to "what will change," eventually they disagree under load or edge cases, and the disagreement is invisible until it isn't.
The frame: preview is execute minus the side effect. Not "execute, parallel-but-similar." Not "approximate." Minus the side effect, full stop.
Where this connects
- Mutations Documented Dry-Run First is the documentation side of the same discipline: every doc example pairs dry-run with the destructive form.
- Optimistic Mutation Funnel is the client-side restatement: snapshot then apply, restore on error. Preview is on the daemon side; optimistic restore is on the client side; both come from the same instinct that the "before" state matters and must be recoverable.
- Mxr and Lazydap both encode this rule. It's one of the few non-negotiables shared between them.
See also
- Mutations Documented Dry-Run First — the doc-discipline mirror
- Mxr — the project where the rule was learned the painful way
- Optimistic Mutation Funnel — the client-side analogue