Monitoring shows 200s. CI is green. Conversion is down 23%. Nobody knows why prod is silently routing checkouts to the retry queue.
This example reproduces that incident end-to-end, on your laptop, with no real backend.
A backend team renamed the field status → state on the checkout response in a
minor release. Staging wasn't redeployed; prod was. The client workflow asserts
body.status: { exists: true } before confirming the order — so in prod that
assertion silently fails and every checkout falls through to the retry_queued
edge. The HTTP status is still 200. Grafana shows nothing. Logs show nothing.
The rename was in a changelog nobody read.
./run-demo.shThe script spawns two local mock backends with the two schemas, runs
checkout.yaml against each, and diffs the traces. Traces
land in /tmp/ace-env-diff/{staging,prod}.json.
Requires ace on $PATH. If you're running from source: ACE=target/release/ace ./run-demo.sh.
════════════════════════════════════════════════════════════════
3/3 ace diff staging.json prod.json
════════════════════════════════════════════════════════════════
ACE diff: DRIFT — 3 change(s) across 3 step(s) · /tmp/ace-env-diff/staging.json vs /tmp/ace-env-diff/prod.json
User 1 / step "checkout"
↯ routing diverged
trace-a: matched checkout→poll_status
trace-b: matched checkout→retry_queued
rejected checkout→poll_status [assertions failed: body.status (expected exists: true, got <missing>)]
User 1 / step "poll_status"
⊘ step absent in trace-b
User 1 / step "retry_queued"
⊘ step absent in trace-a
ACE_SUMMARY: {"v":1,"command":"diff","verdict":"DRIFT","total_steps":4,"divergences":3,"affected_steps":3,"a":"/tmp/ace-env-diff/staging.json","b":"/tmp/ace-env-diff/prod.json"}
Four things the diff is telling you, in one screen:
- The fork is on
checkout, and the cause is right there. Staging matched the edge topoll_status; prod rejected that same edge becausebody.statusis missing from the response. The rejection reason carries the offending assertion — description, expected, actual — so you do not have to cross-reference the trace to know what changed. - The routing is self-describing. Edge labels show
from→tostate names so you can read the diff without cross-referencing the scenario file. TheACE_SUMMARY:line at the bottom is machine-readable JSON — CI scripts cangrep '^ACE_SUMMARY:'for a structured verdict without parsing the full diff. - The downstream is consequence, not cause.
poll_statusis absent in prod andretry_queuedis absent in staging because of the upstream routing divergence. ACE reports them so you know the scope, but the root cause is the first entry. - The exit code is
1. Non-zero on any divergence. Wireace diffinto CI against a last-known-good trace and a broken deploy fails the build.
$ curl -s staging/orders/checkout | jq
{ "id": "...", "status": "paid" }
$ curl -s prod/orders/checkout | jq
{ "id": "...", "state": "paid" }
Two 200 responses, both containing the word paid, differing by one field name.
A human has to notice the rename, understand that the client's workflow keys off
status, and trace the consequence through the state graph. That is the
30-minute debugging session ace diff collapses into one command.
checkout.yaml— the client workflow under testservers/backend-old.yaml— staging backend, emitsstatusservers/backend-new.yaml— prod backend, emitsstate(renamed)run-demo.sh— one-command orchestration
| Code | Meaning |
|---|---|
0 |
no divergences |
1 |
divergences found |
2 |
bad args, unreadable log, etc. |
ace diff staging.json prod.json --format json -o divergences.jsonfor programmatic consumption. Each divergence carries akinddiscriminator (routing_diverged,rejection_reason_changed,edge_only_in_a,edge_only_in_b,outcome_diverged,step_missing_in_a,step_missing_in_b) so downstream tooling can filter without string parsing.- Commit a known-good trace to your repo. Re-run on every deploy and diff against it. Any unexplained routing change now fails CI.
- Change
backend-new.yamlto return 503 instead of renaming the field — you'll see the status-code branch fire instead of the default fallthrough. Different divergence kind, same one-command repro.