doppeldiff 0.0.0-stage โ†’ 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 doppeldiff contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,285 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # ๐Ÿ”ฌ doppeldiff
4
+
5
+ **See what your PR actually changed.**
6
+
7
+ Built for refactors and "no behavior change" pull requests. doppeldiff runs the old and the new version of every
8
+ changed function on thousands of inputs your tests never tried, and shows every input where they disagree.
9
+
10
+ <img src="docs/assets/pr-comment.png" alt="A doppeldiff pull request comment listing unclaimed behavior changes with concrete inputs and old/new results" width="760">
11
+
12
+ ```bash
13
+ npx doppeldiff
14
+ ```
15
+
16
+ **No API key required. Free and fully local by default.**
17
+
18
+ </div>
19
+
20
+ ### What it looks like on a real pull request
21
+
22
+ [Pull request #1](https://github.com/7suheel/doppeldiff/pull/1) is an AI-style refactor of a small shop's cart
23
+ code. Its description says _"Pure refactor, no behavior change. All tests pass."_ They do. doppeldiff ran the old and new
24
+ `cartTotal` on 10,000 generated carts and
25
+ [commented](https://github.com/7suheel/doppeldiff/pull/1#issuecomment-6560143205):
26
+
27
+ > โš ๏ธ **Unclaimed behavior change**: **`cartTotal`**: return value changed ยท 3,212 of 10,000 inputs
28
+ >
29
+ > | Call | Old | New |
30
+ > | ----------------------------------------------------------------------- | ---- | --- |
31
+ > | `cartTotal([{ sku: "", unitPrice: -1, quantity: 1 }], undefined)` | `-1` | `0` |
32
+ > | `cartTotal([{ sku: "", unitPrice: -0.01, quantity: 0.01 }], undefined)` | `-0` | `0` |
33
+
34
+ The refactor clamps the total at zero, so a cart with a credit or refund line no longer adds up. No reviewer was
35
+ asked to look for that, and no test covered it. The same comment flags a second unclaimed change, in `slugify`.
36
+
37
+ ## What doppeldiff is good at, and what it is not good at yet
38
+
39
+ **Good at**
40
+
41
+ - **Refactors and "no behavior change" PRs**, AI-written or not. Either it ran thousands of inputs through both
42
+ versions and found no difference, or it shows the exact input where the refactor changed the result, like the
43
+ `cartTotal` example above. `--refactor` (CLI) and `verify_refactor` (MCP) turn that into a pass/fail check.
44
+ - **Finding inputs your tests never tried**: negative amounts, empty and blank strings, `-0`, values just past a
45
+ threshold in the code, strings in the formats the code's regexes parse, accents and lone surrogates. Each
46
+ finding comes with the old and the new result and can be written out as a regression test.
47
+ - **Plain TypeScript/JavaScript functions**: utilities, parsing, formatting, money and string handling in
48
+ libraries and app code.
49
+ - **Running on every PR**: a few seconds per PR, no API key, nothing leaves the machine.
50
+
51
+ **Not good at yet**
52
+
53
+ - **Finding arbitrary bugs.** doppeldiff only reports what differs between two versions, and random inputs rarely
54
+ hit a change that needs several specific conditions at once (an option combination plus a particular kind of
55
+ text). On the latest held-out set of real bug-fix commits it found **3 of 13**
56
+ ([evaluation](docs/EVALUATION.md)). Use it next to your tests and review, not instead of them.
57
+ - **Code with side effects** (network, filesystem, databases): the sandbox blocks it, and nothing is mocked.
58
+ - **Class methods, overloaded functions, UMD bundles and class-instance parameters**: reported as not verified.
59
+ - **Callback-heavy APIs**: generated callbacks return fixed values, so a custom sort comparator never reorders
60
+ anything.
61
+ - **"No behavior change" is evidence, not proof**: inputs are sampled, not exhaustive.
62
+
63
+ ## The problem
64
+
65
+ Writing code got cheap; verifying it did not. AI-written pull requests compile, pass CI and read well, yet quietly
66
+ change behavior on inputs nobody wrote a test for. AI review bots answer with opinions ("looks good to me"), so
67
+ you end up checking one model's code with another model's guess.
68
+
69
+ doppeldiff answers with evidence instead: **"for `cartTotal([{ unitPrice: -1, quantity: 1 }])` the old code
70
+ returned `-1` and the new code returns `0`"**. You can run that example yourself, and doppeldiff can write it into
71
+ a regression test for you.
72
+
73
+ ## How it works
74
+
75
+ ```
76
+ git diff โ”€โ”€โ–ถ changed exported functions (+ functions that call changed helpers)
77
+ โ”‚
78
+ โ–ผ
79
+ base version โ”€โ”€โ–ถ sandboxed Node process โ”€โ”
80
+ โ”œโ”€โ”€โ–ถ same generated inputs โ”€โ”€โ–ถ compare โ”€โ”€โ–ถ shrink โ”€โ”€โ–ถ group
81
+ head version โ”€โ”€โ–ถ sandboxed Node process โ”€โ”˜ (types + values mined returns, throws, minimal,
82
+ from the code + regex mutations, async, readable
83
+ formats + edge cases) timeouts examples
84
+ โ”‚
85
+ โ–ผ
86
+ report: terminal ยท PR comment ยท JSON ยท regression tests (optional LLM: claimed vs unclaimed)
87
+ ```
88
+
89
+ 1. **Find what changed.** It compares each module-level declaration with comments and formatting ignored, and
90
+ follows changed helpers, constants and imports to the exported functions that use them.
91
+ 2. **Load both versions side by side.** The base commit goes into a git worktree that reuses your
92
+ `node_modules`, and each version is bundled with esbuild (TypeScript, ESM/CJS, tsconfig `paths`, workspaces).
93
+ 3. **Generate inputs.** It derives input generators from the parameter types (via the TypeScript checker,
94
+ [fast-check](https://fast-check.dev) underneath), biased towards values mined from the code: comparison
95
+ thresholds ยฑ1, string literals, regex formats, property names, and boundaries such as `NaN`, `-0` and lone
96
+ surrogates.
97
+ 4. **Run both, compare everything.** It compares return values (`NaN` equals `NaN`, `-0` differs from `0`; Dates,
98
+ Maps, Sets and class instances structurally), thrown errors, argument mutations, async results, hangs and
99
+ crashes. The clock is frozen and randomness seeded, so the comparison is reproducible.
100
+ 5. **Explain it.** Each difference is shrunk to a minimal example, made readable (`2000` instead of
101
+ `9007199254740991`), re-run to confirm, and grouped into behaviors such as _"now returns NaN (previously a
102
+ number)"_.
103
+
104
+ ## Three scenarios
105
+
106
+ The [`examples/demo-shop`](examples/demo-shop) library ships with three pull requests. All three pass the existing
107
+ test suite.
108
+
109
+ | PR | What doppeldiff says |
110
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
111
+ | **Honest fix**: "Fix discount rounding for prices over $1000" | One behavior change, only for prices above 1000: `applyDiscount(2000, 0.07)` was `1859.99`, now `1860`. |
112
+ | **"AI refactor"**: "Pure refactor, no behavior change" | โš ๏ธ 3 unclaimed changes: negative totals are now clamped to `0`, `cartTotal` returns `NaN` for zero-quantity items with a non-finite price, and `slugify("รฉ")` was `"e"` and is now `""`. `itemCount` really is unchanged. |
113
+ | **Pure refactor**: "No behavior change intended" | โœ… No behavior change in 4 functions across 3,242 inputs. The report notes that `addBusinessDays` stopped early (242 inputs) because huge `days` values make both versions loop past the timeout. |
114
+
115
+ Try it: `node examples/demo-shop/try.mjs ai-refactor` creates a scenario repository and prints the command.
116
+
117
+ <img src="docs/assets/demo.gif" alt="Terminal recording of doppeldiff on the AI refactor scenario" width="760">
118
+
119
+ <sub>The image and recording above are rendered from real doppeldiff output on the demo scenario
120
+ (`scripts/render-assets.ts`); the PR comment is rendered with GitHub's Markdown stylesheet.</sub>
121
+
122
+ ## Results on real commits
123
+
124
+ We ran doppeldiff on 39 historical commits from 17 libraries (ufo, validator.js, es-toolkit, date-fns, qs, radash,
125
+ โ€ฆ). Most are bug fixes, which is a harder test than its main use: a bug fix changes behavior on purpose, often
126
+ only for a narrow set of inputs. **The full write-up, misses included, is in
127
+ [docs/EVALUATION.md](docs/EVALUATION.md).** In short:
128
+
129
+ - **Refactors:** defu's `isPlainObject` refactor was correctly reported as "no behavior change" across 6
130
+ functions. semver's "faster paths for compare" differs only for negative numbers, which semver never passes;
131
+ that difference is reported but collapsed as "unusual inputs". Real refactor commits are only 2 of the 39, so
132
+ most of the refactor evidence comes from the demo scenarios above.
133
+ - **Latest held-out set** (18 commits from 9 new libraries, picked by a fixed rule, code frozen before the run): it
134
+ found **3 of 13** verifiable bug fixes. The run also exposed tool bugs (a CommonJS export pattern, JSDoc
135
+ `{Object}`, cross-file impact). After fixing those, it finds 7 of 15; those numbers are no longer held out.
136
+ - On the first 21 commits it finds **11 of 13** fixes, with minimal examples. One is `withoutBase("///", "//")`, the
137
+ protocol-relative URL that a ufo security fix was about. Another is `isISO8601("1690360")`, an ordinal date
138
+ that a regex change started accepting.
139
+ - Most misses need two or three specific things at once (an option combination plus a particular shape of text),
140
+ or a real callback such as a sort comparator.
141
+ - It surfaced things the commit title did not say: validator's "restrict allowed characters" also started
142
+ accepting one- and two-character slugs.
143
+ - **Noise:** 91 % of the behaviors in the main list matter (79 % before the noise work). Differences that only
144
+ happen on inputs unlike the project's tests and call sites are collapsed and do not fail the check.
145
+ - It **could not verify 8 of 39 commits**: class methods, overloaded functions, UMD modules, class-instance
146
+ parameters and exported non-function values. Every report lists these explicitly.
147
+ - Each run took 0.5โ€“8 seconds.
148
+
149
+ ## Quick start
150
+
151
+ ### CLI
152
+
153
+ ```bash
154
+ npx doppeldiff # your uncommitted changes vs HEAD
155
+ npx doppeldiff --base main # this branch vs main (merge base, like a PR)
156
+ npx doppeldiff --base main --claim "Fix rounding for prices over $1000"
157
+ npx doppeldiff --refactor # assert: no behavior change (exit 1 otherwise)
158
+ npx doppeldiff --write-tests # pin the examples in tests/doppeldiff/*.test.ts
159
+ npx doppeldiff --json report.json --markdown report.md
160
+ ```
161
+
162
+ | Flag | Default | |
163
+ | ---------------------------------------- | --------------------- | ------------------------------------------------ |
164
+ | `--base` / `--head` | `HEAD` / working tree | Revisions to compare (via merge base) |
165
+ | `--claim`, `--claim-file`, `--refactor` | | What the change claims to do |
166
+ | `--runs` | `1000` | Max inputs per function |
167
+ | `--timeout` | `250` | Per-call timeout (ms) |
168
+ | `--budget` | | Total exploration time budget (s) |
169
+ | `--seed` | `1` | Every run is reproducible |
170
+ | `--fail-on` | `unclaimed` | Exit 1 on `unclaimed`, `any` or `never` |
171
+ | `--include` / `--exclude` / `--only` | | Limit files or functions |
172
+ | `--no-freeze` | | Do not freeze the clock / seed randomness |
173
+ | `--llm`, `--llm-model`, `--llm-base-url` | `off` | Optional LLM (below); `ollama` is free and local |
174
+
175
+ Node.js 22.13 or newer. Exit codes: `0` OK, `1` findings matching `--fail-on`, `2` tool error.
176
+
177
+ ### GitHub Action
178
+
179
+ Copy the two files in [`examples/workflows/`](examples/workflows) to `.github/workflows/` on your default branch.
180
+ No secrets are required. doppeldiff posts one sticky comment per PR and updates it on every push.
181
+
182
+ It is safe for pull requests from forks. `doppeldiff.yml` runs the PR's code without secrets and with a read-only
183
+ token. `doppeldiff-comment.yml` posts the comment and never runs PR code. These have to be two files because
184
+ GitHub does not let a workflow trigger itself with `workflow_run`. See [docs/ACTION.md](docs/ACTION.md).
185
+
186
+ ### MCP server for coding agents
187
+
188
+ ```bash
189
+ claude mcp add doppeldiff -- npx -y doppeldiff mcp
190
+ ```
191
+
192
+ Two tools: `verify_change` (what did my edit change?) and `verify_refactor` (PASS, or FAIL with
193
+ counterexamples). An agent can check its own refactor before telling you it is done. Setup for Cursor and Codex
194
+ is in [docs/MCP.md](docs/MCP.md).
195
+
196
+ ### Claim matching: free, no LLM
197
+
198
+ doppeldiff compares each behavior change with the PR title and description and labels it **claimed**,
199
+ **unclaimed** or neutral, using [documented deterministic rules](docs/CLAIMS.md):
200
+
201
+ - if the PR says it is a refactor, cleanup or performance change, or says "no behavior change", every difference
202
+ is unclaimed;
203
+ - if the PR names a function, that function's changes are claimed.
204
+
205
+ No LLM and no API key are involved.
206
+
207
+ Optionally, you can let an LLM read the description instead. A local model via
208
+ [Ollama](https://ollama.com) is free: `--llm ollama --llm-model qwen2.5:7b`. Any OpenAI-compatible server works,
209
+ and so do Anthropic or OpenAI with your own key. The LLM is never used unless you ask for it. It only labels
210
+ changes the engine actually observed, and its output is checked against the evidence. Details:
211
+ [docs/CLAIMS.md](docs/CLAIMS.md).
212
+
213
+ ### Configuration (optional)
214
+
215
+ ```ts
216
+ // doppeldiff.config.ts
217
+ import { defineConfig } from 'doppeldiff';
218
+
219
+ export default defineConfig({
220
+ exclude: ['src/legacy/**'],
221
+ compare: { epsilon: 1e-9 },
222
+ functions: {
223
+ // Declare the real input domain when inference from tests and call sites is not enough.
224
+ 'src/internal/identifiers.ts#compareIdentifiers': {
225
+ where: ([a, b]) => [a, b].every((x) => typeof x === 'string' || (Number.isInteger(x) && x >= 0)),
226
+ },
227
+ parseConfig: { skip: 'reads files; covered by integration tests' },
228
+ },
229
+ });
230
+ ```
231
+
232
+ ## Limitations
233
+
234
+ v1 is deliberately narrow. Known gaps:
235
+
236
+ - **Exported functions only.** It does not test class methods, overloaded functions, generators, or UMD/IIFE
237
+ modules. They are listed as "not verified" in every report. Class methods are the biggest gap in practice.
238
+ - **Pure-ish code.** Functions that read files, call the network or depend on global state either fail in the
239
+ sandbox or are reported as nondeterministic. They are not mocked.
240
+ - **Generated inputs are not a proof.** "No behavior change across 10,000 inputs" is strong evidence, not a
241
+ guarantee. Bugs that need a very specific input (a valid ISO ordinal date, prototype-chain objects, `DataView`)
242
+ can be missed; see the [misses in the evaluation](docs/EVALUATION.md#why-the-misses-happen). Optional LLM input
243
+ suggestions help with some of these.
244
+ - **Input domain.** Generators follow the types, not the documentation. A function that is only ever called with
245
+ non-negative numbers may report differences for negative ones. Declare the domain in the config (`args`).
246
+ - **Signature changes.** If the parameter types change incompatibly, the old and new versions cannot receive
247
+ the same inputs, so the function is reported as skipped.
248
+ - **Slow paths.** Functions that loop for a long time on large inputs hit the per-call timeout. Exploration stops
249
+ after 20 such inputs, and the report says how many inputs actually ran.
250
+ - **The sandbox is a guardrail, not a security boundary.** The code under test runs with the Node.js permission
251
+ model, no network and a scrubbed environment, but you should only run doppeldiff on code you would run tests
252
+ for. See [SECURITY.md](SECURITY.md).
253
+ - **JavaScript and TypeScript only.**
254
+
255
+ ## How it compares
256
+
257
+ | | What it does | How doppeldiff differs |
258
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
259
+ | **AI review bots** | Read the diff and give an opinion | doppeldiff runs the code and shows inputs and results |
260
+ | **[fast-check](https://fast-check.dev)** | Property-based testing library | doppeldiff is built on it; it adds change detection, loading both versions in isolation, generators derived from types and code, comparison semantics and reporting. You write no properties |
261
+ | **[RealDiff](https://github.com/issacnitin/RealDiff)** (formerly BehaviorDiff) | Runs your **existing tests** on both sides of a PR and diffs recorded values (6 languages) | doppeldiff **generates** inputs, so it finds differences on inputs no test covers. RealDiff sees real call sites; the two are complementary |
262
+ | **[receipts-cli](https://www.npmjs.com/package/receipts-cli)** | Enforces that an agent's fix comes with a test that is red on base and green on head, checks that the test has teeth, and adds agent guardrails | It verifies the test the author wrote for the claim; doppeldiff looks for changes nobody claimed or tested. They work well together: doppeldiff's `--write-tests` can produce such tests |
263
+ | **[ProofDiff](https://www.npmjs.com/package/proofdiff)** | Reports what changed, which related tests ran and what remains unverified (static by default) | ProofDiff tracks test evidence; doppeldiff creates behavioral evidence by running old and new code |
264
+ | **[Keploy](https://keploy.io)** | Records and replays API traffic as tests | API level with recorded traffic; doppeldiff works at function level on generated inputs |
265
+ | **[Testora](https://conf.researchr.org/details/icse-2026/icse-2026-research-track/10/Testora-Using-Natural-Language-Intent-to-Detect-Behavioral-Regressions)**, **DiffTestGen**, Mokav (research, Python) | LLM-generated tests that expose differences between versions; Testora classifies them against the PR text | The same idea, and evidence that it works. doppeldiff is a deterministic-first tool you can install (no key needed), for TypeScript, with an Action and an MCP server |
266
+ | **[CrossHair `diffbehavior`](https://crosshair.readthedocs.io/en/latest/diff_behavior.html)** (Python) | Symbolic execution finds inputs where two functions differ | Python only, and you target functions by hand; no git or PR integration |
267
+ | **BERT** (Jin, Orso, Xie 2010; Java) | The original "behavioral regression testing" | The academic origin of the technique |
268
+
269
+ ## Roadmap
270
+
271
+ - Class methods (construct instances from constructor parameters, compare state after each call)
272
+ - Coverage-guided input generation (V8 coverage of the changed lines)
273
+ - Overloaded functions and UMD modules
274
+ - Side-effect sandboxing: virtual filesystem and recorded `fetch` for less pure code
275
+ - HTTP-level diffs for request handlers
276
+ - Python
277
+
278
+ ## Contributing
279
+
280
+ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md). The design is documented in
281
+ [docs/DESIGN.md](docs/DESIGN.md).
282
+
283
+ ## License
284
+
285
+ [MIT](LICENSE)