Comparing Two Codebases
jscpd --compare A B pairs every function of folder A with the function of folder B that does the same job, in one language or across two. One report answers two questions:
- During a port, such as a library moving to another language or an iOS app moving to Android, which functions of the source already have a version in the target, and which are still to port?
- For two implementations of one app, such as the Android and the iOS one, what do both have, and what does only one of them have?
This page runs --compare on a small port and goes through every report it writes. To follow along in a browser, open the demo's migration map, or a larger one that compares ten Tauri plugins across Android and iOS.
--compare pairs functions with the code embedding model of --semantic, which runs inside jscpd after a one-time download.Quick start
jscpd --semantic-download # once: fetches the model, 548 MB
jscpd --compare python/ typescript/ # the source first, the target second
jscpd --compare python/ typescript/ -r html # writes the migration map to report/
--compare takes exactly two paths. In a port the first is the source, the code you port from, and the second is the target. For two implementations of one app the order only decides which side the reports list first and which side the map draws on the left.
| Reporter | Writes | Use it for |
|---|---|---|
console (default) | the summary, in the terminal | reading the result |
console-full | the summary plus every pair | checking each pair |
json | jscpd-compare.json | scripts, CI and agents |
markdown | jscpd-compare.md | a pull request, an issue or a wiki page |
html | jscpd-compare.html | the migration map and the table of pairs |
-r takes several reporters at once, as in -r console,json,html, and -o names the folder for the files, report/ by default. jscpd ignores other reporters with a warning. The exit code is 0 unless the run fails.
The example
fixtures/compare-demo in the jscpd repository holds a billing module halfway through its port from Python to TypeScript:
| File | Functions | What happened in the port |
|---|---|---|
python/billing.py | line_total, apply_discount | ported under the same names, in camelCase |
tax_for_region | ported as salesTax | |
format_invoice_number | ported as a one-line arrow function | |
due_date | not ported yet | |
python/shipping.py | shipping_cost | ported |
estimate_delivery_days | not ported yet | |
typescript/billing.ts | toCurrency | new in TypeScript, with no Python original |
python/test_billing.py | three pytest tests | ported to typescript/billing.test.ts as it('…', () => …) cases |
test_due_date_skips_the_weekend | not ported yet, like due_date itself |
Every report below comes from this folder. Run the commands from inside it, since from the repository root jscpd would also load the repository's .jscpd.json:
cd fixtures/compare-demo
jscpd --compare python typescript
The console report
Code
71% 5 of 7 functions in python have a counterpart in typescript
80% 4 of 5 functions in typescript have a counterpart in python
python
file paired similarity counterpart
billing.py 4 / 5 0.89 billing.ts
shipping.py 1 / 2 0.91 shipping.ts
typescript
file paired similarity counterpart
billing.ts 3 / 4 0.90 billing.py
shipping.ts 1 / 1 0.91 shipping.py
Paired under other names (1):
python typescript similarity
billing.py:28 tax_for_region billing.ts:27 salesTax 0.87 high
Only in python (2):
billing.py (1)
46 due_date 6 lines
shipping.py (1)
18 estimate_delivery_days 8 lines
Only in typescript (1):
billing.ts (1)
39 toCurrency 8 lines
Tests
75% 3 of 4 tests in python have a counterpart in typescript
100% 3 of 3 tests in typescript have a counterpart in python
python
file paired similarity counterpart
test_billing.py 3 / 4 0.87 billing.test.ts
typescript
file paired similarity counterpart
billing.test.ts 3 / 3 0.87 test_billing.py
Only in python (1):
test_billing.py (1)
30 test_due_date_skips_the_weekend 7 lines
The report has a block for the code and a block for the tests, and both read the same way from the top. In a terminal it is in color: levels are green for high, yellow for medium and red for low, and shares and paired counts are green when every function has a counterpart, yellow when some do and red when none do. --no-colors prints the plain text shown here.
The two share lines
The first line of a block counts the functions of the source that have a counterpart in the target. Here 5 of the 7 Python functions have a TypeScript version, and during a port that line is your progress. The second line counts the other way: 4 of the 5 TypeScript functions have a Python original, and the fifth, toCurrency, is new. A parity check reads both lines, since each says how much of one side the other covers.
The file tables
Each side gets a table with a row per file:
pairedis how many of the file's functions have a counterpart, out of the functions that count.similarityis the mean similarity of those pairs. When some of them arelow, the column says how many, as in0.62, 1 low.counterpartis the file on the other side that holds most of the counterparts.
So billing.py has 4 of its 5 functions in billing.ts, at a mean similarity of 0.89. A file with no pairs leaves the last two columns empty.
Paired under other names
This section lists the pairs whose names differ even once case and underscores are ignored, here tax_for_region, ported as salesTax. Nobody finds these pairs by searching for a name, so the default report shows them. Renamed ports, constructors (QrCode and __init__) and platform names (startWatch and watchPosition) end up here. Each pair has its similarity and a level.
Only in python, only in typescript
These lists hold the functions without a counterpart, grouped by file, each with its first line, name and length. In a port, "Only in python" is the work left: due_date and estimate_delivery_days. "Only in typescript" is code that exists only in the target, here toCurrency. A function in these lists is either missing on the other side or has a counterpart that jscpd did not recognize, so check it before you call it missing.
The tests block
jscpd measures tests apart from code and pairs a test only with a test, so the tests of line_total never count as line_total itself. It tells a test by the conventions of its language: test_billing.py is a pytest file and billing.test.ts a Vitest one. It also knows names such as *_test.go, *.spec.js and *Tests.swift, folders such as tests/, __tests__/ and src/test/, and Rust's #[test] functions and #[cfg(test)] modules. The full list is in the reference. When neither side has tests, the report shows the code alone, without the two headings.
The TypeScript tests are it('line total sums quantity times price', () => …) callbacks. jscpd names such a test case after its title and compares test names without case, underscores, spaces, punctuation and a leading test_. The title meets the pytest function test_line_total_sums_quantity_times_price, and the report does not list the pair as renamed.
What counts as a function
The billing.ts row says 3 / 4, though the file holds five functions. The fifth, formatInvoiceNumber, is a one-line arrow function, shorter than the bar for counting. A side's totals count the functions of at least --min-tokens tokens and --min-lines lines, the first and the last line included. With --compare, --min-tokens defaults to 30 instead of 50, because a function worth porting is often shorter than a clone worth reporting, and --min-lines stays at 5. A shorter function still shows up as the partner of one that counts, as formatInvoiceNumber does in the next section.
Anonymous functions, such as callbacks and closures, take no part. JavaScript and TypeScript test cases are the exception, since they go by their titles.
Every pair: console-full
jscpd --compare python typescript -r console-full
console-full prints the console report and ends each block with the list of every pair:
Code
...
Pairs (5):
python typescript similarity
billing.py:8 line_total billing.ts:10 lineTotal 0.89 high
billing.py:18 apply_discount billing.ts:19 applyDiscount 0.93 high
billing.py:28 tax_for_region billing.ts:27 salesTax 0.87 high
billing.py:38 format_invoice_number billing.ts:36 formatInvoiceNumber 0.89 high by name
shipping.py:6 shipping_cost shipping.ts:5 shippingCost 0.91 high
Tests
...
Pairs (3):
python typescript similarity
test_billing.py:8 test_line_total_sums_quantity_times_price billing.test.ts:6 line total sums quantity times price 0.84 high
test_billing.py:15 test_line_total_rejects_a_zero_quantity billing.test.ts:13 line total rejects a zero quantity 0.91 high
test_billing.py:23 test_discount_is_capped billing.test.ts:19 discount is capped 0.84 high
jscpd finds pairs in two steps. The first pairs functions by their code with the rule of --semantic: each function is the other's closest match or close to it, and their similarity reaches the threshold and stands out from the function's similarity to the rest of the other side. tax_for_region and salesTax paired in this step, on their code alone.
The second step takes the functions left over and pairs two whose names match once case and underscores are ignored (encodeBinary, encode_binary, _encode_binary), when their similarity reaches the medium level. That bar keeps namesakes such as load and init from pairing whatever they do. The step takes functions of any size, since a port often makes a function shorter, so formatInvoiceNumber pairs with format_invoice_number here and the list marks the pair by name. A name pair also stays within the modules that the code pairs link, such as one plugin's Android and iOS folders.
Similarity levels
Every pair has its cosine similarity and a level. The level is on the scale of the model, since a cosine that is high for one model is low for another. For the default model, CodeRankEmbed:
| Level | Across languages | Within one language | What it means |
|---|---|---|---|
high | 0.7125 and up | 0.7125 and up | almost always the same function |
medium | 0.5625 to 0.7125 | 0.675 to 0.7125 | usually the same function, restructured |
low | 0.4125 to 0.5625 | 0.6375 to 0.675 | read both: related code pairs here too |
low starts at the pair threshold, the lowest similarity at which jscpd pairs two functions. high starts at the group floor of the --semantic rules, or at the threshold when a model's threshold is higher, and medium starts halfway between the two. Another model has other numbers. Semantic Clones explains the thresholds, and jscpd --semantic-models lists them for each model.
The JSON report
jscpd --compare python typescript -r json # writes report/jscpd-compare.json
The JSON report holds everything console-full shows, plus the functions ready to port, in two sections of the same shape, code and tests. Each section has the two sides, the source first, and the pairs. Here is the code section of the demo, with two of its five pairs:
{
"code": {
"sides": [
{
"path": "python",
"functions": 7,
"matched": 5,
"percentage": 71.43,
"files": [
{ "file": "billing.py", "functions": 5, "matched": 4, "counterpart": "billing.ts", "similarity": 0.89, "lowPairs": 0 },
{ "file": "shipping.py", "functions": 2, "matched": 1, "counterpart": "shipping.ts", "similarity": 0.91, "lowPairs": 0 }
],
"unmatched": [
{ "file": "billing.py", "name": "due_date", "start": 46, "end": 51 },
{ "file": "shipping.py", "name": "estimate_delivery_days", "start": 18, "end": 25 }
],
"readyToPort": [
{ "file": "billing.py", "name": "due_date", "start": 46, "end": 51, "callers": 1 },
{ "file": "shipping.py", "name": "estimate_delivery_days", "start": 18, "end": 25, "callers": 0 }
]
},
{
"path": "typescript",
"functions": 5,
"matched": 4,
"percentage": 80.0,
"files": [
{ "file": "billing.ts", "functions": 4, "matched": 3, "counterpart": "billing.py", "similarity": 0.9, "lowPairs": 0 },
{ "file": "shipping.ts", "functions": 1, "matched": 1, "counterpart": "shipping.py", "similarity": 0.91, "lowPairs": 0 }
],
"unmatched": [
{ "file": "billing.ts", "name": "toCurrency", "start": 39, "end": 46 }
],
"readyToPort": [
{ "file": "billing.ts", "name": "toCurrency", "start": 39, "end": 46, "callers": 0 }
]
}
],
"pairs": [
{
"a": { "file": "billing.py", "name": "tax_for_region", "start": 28, "end": 35 },
"b": { "file": "billing.ts", "name": "salesTax", "start": 27, "end": 34 },
"similarity": 0.868,
"level": "high",
"renamed": true,
"matchedBy": "code"
},
{
"a": { "file": "billing.py", "name": "format_invoice_number", "start": 38, "end": 43 },
"b": { "file": "billing.ts", "name": "formatInvoiceNumber", "start": 36, "end": 37 },
"similarity": 0.889,
"level": "high",
"renamed": false,
"matchedBy": "name"
}
]
}
}
| Field | What it holds |
|---|---|
path | the path as given on the command line |
functions, matched, percentage | the share line: the functions that count, those with a counterpart, and the share in percent |
files | the file table; a file with no pairs has no counterpart and no similarity |
unmatched | the "Only in" list, with the first and the last line of each function |
readyToPort | the functions with no counterpart whose callees all have one, most called first |
pairs[].a, pairs[].b | the function on the first side and its counterpart on the second |
similarity, level | the cosine similarity and its level |
renamed | whether the names differ once case and underscores are ignored |
matchedBy | code for a pair from the first step, name for one from the second |
Ready to port
A function is ready to port when it has no counterpart yet and everything it calls has one, so porting it waits for nothing. jscpd finds calls by name: a name followed by ( in a function's code calls the functions of the same side with that name, in a language that can call it. A function in the caller's own file wins, and jscpd does not follow a name that more than three functions carry. Two unported functions that call each other wait for each other, so neither is ready. callers counts the functions and tests that call it: due_date has one, its test.
Each side has its own list. A port works through the source's list. In a parity check, each list holds what could move to the other side next.
Reading it from a script
The JSON is what a CI job or an agent reads between two steps of a port. Three examples with jq:
# the port's progress, in percent
jq '.code.sides[0].percentage' report/jscpd-compare.json
# what to port next, one function per line
jq -r '.code.sides[0].readyToPort[] | "\(.file):\(.start) \(.name)"' report/jscpd-compare.json
# the pairs under other names, to check by hand
jq -r '.code.pairs[] | select(.renamed) | "\(.a.name) -> \(.b.name) \(.similarity) \(.level)"' report/jscpd-compare.json
71.43
billing.py:46 due_date
shipping.py:18 estimate_delivery_days
tax_for_region -> salesTax 0.868 high
The code-migration skill runs this loop for an agent. It ports the tests first, then the code one function at a time, starting with readyToPort, and reruns --compare after each step to see the function leave the unmatched list and pair with the right counterpart.
The Markdown report
jscpd --compare python typescript -r markdown # writes report/jscpd-compare.md
The Markdown report has the tables of console-full for a pull request description, an issue or a wiki page that tracks a port. The demo's report starts like this:
# `python` compared with `typescript`
## Code
| Side | Functions | With a counterpart | % |
|---|--:|--:|--:|
| `python` | 7 | 5 | 71.43 |
| `typescript` | 5 | 4 | 80 |
### `python`
| File | With a counterpart | Similarity | Counterpart file |
|---|--:|--:|---|
| `billing.py` | 4 / 5 | 0.89 | `billing.ts` |
| `shipping.py` | 1 / 2 | 0.91 | `shipping.ts` |
It goes on with the table of the other side, then "Only in" tables with each function's place and length, the pairs under other names, and every pair with its similarity, level and the step that matched it. The ## Tests section repeats the same tables for the tests.
The HTML report: the migration map
jscpd --compare python typescript -r html # writes report/jscpd-compare.html
jscpd-compare.html is one file with its styles, script and data inside, so it opens offline and can travel as a CI artifact or a ticket attachment. It shows the comparison as a map and as a table, with one set of filters for both. Open the demo's report and click around while you read.


The header gives both shares, the tests below them and the number of pairs in the middle. Under it, "One mark per" picks folders, files or functions, and the search box finds a file or a function. When the comparison has tests, "Show" picks code, tests or both, and the page opens with both.
The map
The map draws the two sides as dependency graphs facing each other across a channel, the source in orange on the left and the target in green on the right. The labels across the top name the four bands: source functions not ported yet, ported ones lining the channel, target functions with a counterpart, and functions only in the target.
- A mark is a function, a file or a folder. The page picks the level by the size of the comparison, and past 1,500 marks it asks for bigger marks or the table.
- Code is a circle and a test is a square. With both shown, thin lines tie each test to the code it calls.
- A mark fills from the bottom as its functions find counterparts. It is empty when none has one and full when all have.
- A dark ring marks the source functions that are ready to port, here
due_dateandestimate_delivery_days. - A dotted bridge stands for the pairs between two marks. Its color is their mean similarity, light blue at 0.40 and dark blue at 1.00, and it is thicker for more pairs. The dark theme turns the blue scale around, so the closest pairs stay the easiest to see.
- Thin gray lines are calls within a side.
Hover a mark to light up what it calls and what it pairs with. Click it to list its functions with their counterparts, calls and callers: the panel for due_date says it is ready to port and that test_due_date_skips_the_weekend calls it. Drag to move the map, and zoom with the − and + buttons or with Ctrl or ⌘ held while you scroll.
The table


The Table tab lists the same bridges as rows: the source mark on the left, the similarity in the middle, the target mark on the right, and a status of ported, partly ported, ready to port, not ported or only in the target. A mark with no bridge gets a row of its own. A click on a column header sorts the table, and a click on a row opens the pairs behind it with what its source mark still lacks, or, for a function, its pairs, calls and callers. The address keeps the tab, so a link that ends in #table, like this one, opens the table.
Below the map and the table
Three more sections follow, for whatever the filters select. "Ready to port" lists those functions with the number of callers of each. "How alike the pairs are" charts the pairs by similarity in steps of 0.05, colored by level, with a switch to show the counts as a table. "Progress by folder" gives the share of each folder's functions that have a counterpart, on both sides.
A larger map: Android and iOS
tauri-apps/plugins-workspace has ten plugins written for both Android, in Kotlin, and iOS, in Swift. This report compares the two platforms as a parity check, built from commit d4835d0 of 2026-09-30 with jscpd 5.4.0:
git clone https://github.com/tauri-apps/plugins-workspace
git -C plugins-workspace checkout d4835d0
mkdir android ios
for p in barcode-scanner biometric clipboard-manager dialog geolocation haptics nfc notification opener shell; do
cp -R plugins-workspace/plugins/$p/android android/$p
cp -R plugins-workspace/plugins/$p/ios ios/$p
done
jscpd --compare android ios -r html


60 of the 147 Android functions and 58 of the 106 iOS functions have a counterpart, in 63 pairs: 38 high, 16 medium and 9 low. 17 pairs join functions that the two platforms name differently, such as startWatch and watchPosition in geolocation or writeTag and writeNDEFTag in nfc, and all 9 low pairs are among them. One pair crosses plugins: permissionState of notification on Android with getPermissionState of barcode-scanner on iOS, at 0.485, marked low. The "Only in" lists include the six functions of barcode-scanner's GraphicOverlay.kt on Android, the three of its CameraView.swift on iOS, and createChannel, which builds an Android notification channel for the notification plugin.
The map labels the first path "Source" and the second "Target" whatever the comparison is. In a parity check that only sets which platform the map draws on the left.
Before you trust the numbers
Point --compare at the code, not at the repository roots. It walks every file under both paths that jscpd finds functions in, so the functions of build scripts, examples and benchmarks count too. Keep vendored dependencies and build output out of both paths as well. Inside a git repository jscpd reads .gitignore. Elsewhere, or for folders that the repository keeps, pass --ignore, as in --ignore "**/vendor/**,**/target/**". A walk into node_modules or target/ also slows the run down, since jscpd embeds the functions it finds there.
Then read a sample of the pairs with -r console-full: two or three high pairs, to confirm that the pairing works on this code, every low pair, and the pairs marked by name, since a function with the same name can do something else.
Before you call a function in an "Only in" list missing, search the other side for it: in its counterpart file, under another name, as part of a larger function, or replaced by a library or platform call. jscpd misses some pairs:
- constructors across languages, such as a Java constructor and Rust's
new, Kotlin'sconstructoror Swift'sinit, pair only when their code is similar enough; - a short function renamed on the other side, such as
add_historyfor_finder_penalty_add_history; - when one function was split into several, or several merged into one, only the closest part may pair.
Some functions exist on one side by design: platform glue, such as an iOS delegate callback or an Android notification channel, helpers that one language needs and the other does not, and features that one side dropped.
An empty side is fine. At the start of a port the target has nothing yet, and jscpd prints the source's totals with a note:
Code
0% 0 of 7 functions in python have a counterpart in typescript
typescript has no functions yet
Tests
0% 0 of 4 tests in python have a counterpart in typescript
typescript has no tests yet
The JSON and Markdown reports still list every function of the source as unmatched, so they track a port from its first day.
The comparison has limits. It compares functions, not types, constants or UI markup. Similarity does not see small differences in behavior, so two versions that drifted apart still pair. The more the target is restructured, the fewer of its functions pair by code. Related code pairs too, such as a function that counts UTF-8 bytes and one that converts a string to them, which is what the low level warns about.
More information
- Semantic Clones: the model behind
--compare, the other models and their thresholds. Every--semantic-*option applies to--compareexcept--semantic-scope. - Agent skills:
compare-codebasesruns and explains a comparison, andcode-migrationports a codebase tests first with--compareas the progress measure. - Configuration:
--comparein the table of options. - The reference in docs/rust.md: how the pairing works in detail, and how jscpd tells a test from code in each language.