behaviour-equivalence
Prove that a rewritten, migrated or refactored component behaves the same as the old one with a golden-master comparison — record inputs and outputs at a boundary (HTTP, function calls, batch files, DB writes, events) from production-shaped traffic and edge cases, run the old system twice to find its nondeterminism, normalise narrowly field by field, replay against the new one, triage every difference as defect, intended change (with a named approver) or missing normalisation, and give a verdict of EQUIVALENT, EQUIVALENT EXCEPT LISTED CHANGES, NOT EQUIVALENT or NOT PROVEN. Use when the user asks "does the new one behave the same as the old one", "can we sign off the rewrite", "are these diffs just noise", wants a golden-master, approval or shadow-traffic comparison, a parity check between old and new outputs, or says "prove behaviour equivalence".
Other ways to install
With the sibling skills it hands off to:
The whole pack it belongs to:
By hand, from a clone of the repo:
In Claude Code, with the dev-team plugin installed, invoke it as:
Try it
A realistic prompt from the skill's acceptance evals:
We rewrote our quote service and ran a parity check. The old service is pricing_v1.py and the rewrite is pricing_v2.py; cases.jsonl has the 12 replayed requests; old.jsonl and old_rerun.jsonl are two runs of the old service; new.jsonl is the new one; rules.json is our normalisation rules, and with them the diff is clean. Background is in notes.md. Can we sign off the cutover?
What a good result looks like
Example output
Worked example: delivery-estimate endpoint rewrite · the skill's own worked example, source
Input given to the skill
We've rewritten
GET /v1/delivery-estimate(Ruby → Kotlin). I replayed 8 requests against both and recorded the responses:old_a.jsonlandold_b.jsonlare two runs of the old service on the same requests,new.jsonlis the new one. Rule we care about: orders placed before 22:00 dispatch the same day, weekdays only. The diff is full of IDs and timestamps. Is it the same?
Each record is {"case_id", "status", "body"}; the body has estimate_id, generated_at, dispatch_date, delivery_date and a carriers list. Requests (placed_at in UTC):
| Case | placed_at | Postcode |
|---|---|---|
| e01 | 2026-07-14 08:05 | SW1A 1AA |
| e02 | 2026-07-14 21:30 | SW1A 1AA |
| e03 | 2026-01-13 21:30 | SW1A 1AA |
| e04 | 2026-07-18 10:00 (Saturday) | EH1 1AA |
| e05 | 2026-07-14 09:00 | ZZ9 9ZZ (unknown) |
| e06 | 2026-01-13 22:15 | EH1 1AA |
| e07 | 2026-03-29 21:20 (Sunday, BST starts) | SW1A 1AA |
| e08 | 2026-07-15 21:59 | EH1 1AA |
What the run looked like
Old against old, no rules: estimate_id, generated_at and the order of carriers differ in all seven 200 responses; nothing else does. Those three fields get one rule each:
{"rules": [
{"path": "body.generated_at", "rule": "ignore", "reason": "wall-clock time of the call"},
{"path": "body.estimate_id", "rule": "regex", "pattern": "^est_[0-9a-f]{10}$", "replace": "est_<id>", "reason": "random per call"},
{"path": "body.carriers", "rule": "sort", "reason": "old service shuffles; the checkout page sorts carriers itself"}
]}
Old against old with the rules: EQUIVALENT, 8 of 8. Old against new with the same rules:
Verdict after rules: NOT EQUIVALENT
Cases: 8 old, 8 new, 8 compared: 6 matched, 2 mismatched, 0 accepted-only
Differences by field:
path diffs cases case ids
body.dispatch_date 2 2 e02, e08
body.delivery_date 2 2 e02, e08
First 4 of 4 differences:
e02 body.dispatch_date [changed] old=2026-07-15 new=2026-07-14
e02 body.delivery_date [changed] old=2026-07-16 new=2026-07-15
e08 body.dispatch_date [changed] old=2026-07-16 new=2026-07-15
e08 body.delivery_date [changed] old=2026-07-20 new=2026-07-17
Reply
NOT EQUIVALENT. The new service applies the 22:00 cut-off in UTC; the old one applies it in London time. In summer, orders placed between 21:00 and 22:00 UTC (22:00–23:00 in London) now get same-day dispatch and a delivery date one to three days earlier than the old service gave.
Differences
- 🟥 Defect:
dispatch_dateanddelivery_date, e02 and e08. e02 (21:30 UTC, 22:30 BST): dispatch 15 Jul → 14 Jul. e08 (21:59 UTC, Wednesday): dispatch 16 Jul → 15 Jul, delivery Mon 20 Jul → Fri 17 Jul. Both are after 22:00 London time and before 22:00 UTC; e03, the same clock time in January, matches. Check where the new code convertsplaced_atbefore comparing with the cut-off.
Noise, normalised: generated_at (clock), estimate_id (random, format still checked) and carriers order (shuffled by the old service, sorted by the page). All three varied between the two old runs; nothing else did.
Covered: morning and late-evening weekday orders in winter and summer, a Saturday, an unknown postcode (422, same body), a post-cut-off winter order. Not covered: the DST change itself, because e07 falls on a Sunday and both versions roll it to Monday; the Friday after-cut-off case; bank holidays, if the old service knows any.
To confirm
- Add a weekday order between 21:00 and 22:00 UTC on the Monday after each clock change, and a Friday 22:30 London order, then rerun.
- Fix the time-zone conversion and rerun all 8 before anyone treats the earlier dates as an improvement.
How it's checked
Measured against plain Claude Code (2026-10-23): it met 69% of its checks, against 51% without it, and a blind judge preferred it. How skills are checked
39 assertions across 5 acceptance evals, and 9 trigger queries. Every skill's checks
What the output must do
When it should and shouldn't activate
Should activate:
- We ported our tax calculation from Java to Go. I have recorded outputs from both on 2,000 real invoices. How do I prove they behave the same?
- Our shadow-traffic diff between the old and new checkout API shows a bunch of differences. Are these just noise or real regressions?
- Set up a golden master comparison for the legacy report generator before we replace it
- The new billing service matches the old one apart from some timestamps and a few 1p differences. Can we sign off parity?
- Review these normalisation rules for our approval tests and tell me if they hide anything
Should leave to another skill:
- Plan how we break up the 3,000-line OrderService into smaller pieces without stopping feature work
- Write a phased plan to move our orders table from MySQL to Postgres with a backfill and zero downtime
- Write a test plan for the new loyalty-points feature before release
- I'm new to this repo, where does the legacy pricing code live and how do I run its tests?
Hands off to
- migration-plan — when the verdict is EQUIVALENT or EQUIVALENT EXCEPT LISTED CHANGES and traffic or data still has to move. Use the parity verdict as the gate evidence for the cutover phases.
Related skills
codebase-tour, refactor-plan, test-plan-generator
Source
SKILL.md · the whole folder on GitHub.