Compare codebases skill

Compare two folders function by function with jscpd --compare, in the same language or across languages, and explain the result.

by kucherenko·MIT license·★ 6,331 Stars on the repo·GitHub ↗

Use now

Files of Compare codebases

kucherenko/master1 file shown
SKILL.md
Show the full text149 lines

compare-codebases

jscpd --compare A B pairs every function of folder A with the function of folder B that does the same job, and lists the functions of each folder that have no counterpart. The two folders may be in one language or in two (Python and TypeScript, Kotlin and Swift, Java and Rust). This skill explains how the comparison works and how to run one well. For porting code with the comparison as the progress measure, use the code-migration skill; for the rest of jscpd, the jscpd skill.

How the comparison works

jscpd finds the functions of JavaScript, TypeScript, JSX, TSX, Vue, Svelte, Astro, Python, Rust, Go, Java, Kotlin, C#, C, C++, PHP, Ruby, Scala and Swift files in both folders. It turns the code of each function into a vector with a code embedding model that runs inside jscpd (CodeRankEmbed by default), so two functions that do the same job point the same way even when their languages, names and structure differ. Functions of one folder are never compared with each other.

Functions pair in two steps:

  1. By code. Two functions pair when each is the other's closest match in the other folder, their cosine similarity reaches the model's threshold (0.4125 across languages and 0.6375 within one language, with CodeRankEmbed), and the similarity stands out from the function's other matches. A function close to the best one also pairs when it reaches a higher bar, so a feature written twice on one side gets two pairs. Functions shorter than --min-tokens (30 with --compare) or --min-lines (5) stay out of this step, because a short function resembles too many others.
  2. By name. A function left over pairs with a function of the other folder under the same name, once case, underscores, spaces and punctuation are ignored (encodeBinary, encode_binary, _encode_binary; the test title rounds cents and rounds_cents), when their similarity reaches the medium level (0.5625 across languages with CodeRankEmbed). A name pair skips the closest-match and stand-out checks of the first step, so it needs more than that step's threshold; otherwise every load and init of two codebases would pair. Size does not matter here, so a short port is found. A name pair has to stay within modules the first step linked. A module is the folder right under the deepest folder all files of a side share (notification in android/notification/…), and a file that sits higher than the rest, such as a build script, does not move that folder up. Two modules link when one holds the most of the other's pairs, counted for code and for tests separately. So checkPermissions of one plugin does not pair with its namesake in another.

Each pair gets a level on the scale of the model, because a cosine that is high for one model is low for another:

Level With CodeRankEmbed, across languages Meaning
high 0.7125 and up almost always the same function
medium 0.5625 to 0.7125 usually the same function, restructured
low 0.4125 to 0.5625 read both: related code pairs here too

jscpd measures tests and code apart, in two blocks of the report, and pairs a test only with a test. It tells a test by the conventions of its language:

  • a test file such as *_test.go, test_*.py, *.test.ts, *.spec.js, *Tests.swift or *_spec.rb;
  • a test folder such as tests/, __tests__/, spec/, src/test/ (where Java, Kotlin and Scala keep their tests) or MyAppTests/, the compared folder's own name included;
  • a Rust function in a #[cfg(test)] module or under #[test];
  • a JavaScript or TypeScript test case such as it('rounds cents', () => …).

When neither side has a test that counts (see below), the report has one block and no headings.

Totals count the functions of at least --min-tokens tokens and --min-lines lines; smaller ones appear only as partners. Declarations without a body (TypeScript overloads, the functions of a .d.ts file, interface and abstract methods) take no part. Anonymous functions (callbacks, closures) take no part either, except JavaScript and TypeScript test cases. A test case such as it('rounds cents', () => …) goes by its title, and so do those written with test, specify, fit, xit, xtest or bench, with .only, .skip or .each(table) after them. Suites and hooks stay anonymous. jscpd does not compare types, constants, SQL or UI markup.

A way to compare two folders

1. Pick the folders
  • Point at the code, not the repositories: app/src/main/java and ios/Sources, not the two repository roots. Build output, vendored code, installed packages and generated files dilute the result, and a vendored dependency tree can turn a run of seconds into one of tens of minutes, since every function in it is embedded. Inside a git repository jscpd skips what .gitignore excludes; outside one, or for folders the .gitignore misses, pass them with --ignore (--ignore "**/vendor/**,**/target/**,**/node_modules/**"). When a side has no .gitignore entries for such folders, suggest them to the user.
  • Two folders are required, and they must not overlap: app/ and app/android/ is refused.
  • Keep parallel structures when you can (ios/<module> and android/<module>). Modules steer the name step, so matching folder names help.
  • For a port, put the source first and the target second, so the first line of the report is the port's progress. For two implementations that both live on, the order does not matter.
  • One run covers tests and code, since the report measures the tests in a block of their own. When the user asks about the code alone, leave the tests out with --ignore "**/__tests__/**,**/*.test.*,**/test/**". When they ask about the tests alone, pass the two test folders as the paths, or pick the test files with --pattern.
2. Get the model

The first run needs the model (548 MB). Ask the user before downloading it:

npx jscpd --semantic-download

jscpd caches the vectors per pair of folders, so later runs embed only the functions whose code changed.

3. Read the overview
npx jscpd --compare billing-py/ billing-ts/
 71% 5 of 7 functions in billing-py/ have a counterpart in billing-ts/
 80% 4 of 5 functions in billing-ts/ have a counterpart in billing-py/

billing-py/
  file         paired  similarity  counterpart
  billing.py   4 / 5   0.89        billing.ts
  shipping.py  1 / 2   0.91        shipping.ts

billing-ts/
  file         paired  similarity  counterpart
  billing.ts   3 / 4   0.89        billing.py
  shipping.ts  1 / 1   0.91        shipping.py

Paired under other names (1):
  billing-py/                   billing-ts/             similarity
  billing.py:28 tax_for_region  billing.ts:27 salesTax  0.87 high

Only in billing-py/ (2):
  billing.py (1)
    46  due_date                6 lines
  shipping.py (1)
    18  estimate_delivery_days  8 lines

Only in billing-ts/ (1):
  billing.ts (1)
    39  toCurrency  8 lines
  • When both folders hold tests, the report has a Code block and a Tests block, each with everything below. The two top lines of a block give the share of each folder's functions (or tests) that have a counterpart in the other.
  • The file tables give each file's paired functions, the mean similarity of its pairs (with the number of low pairs, as in 0.62, 1 low), and the file on the other side that holds most of its counterparts.
  • "Paired under other names" lists the pairs whose names differ even once case and underscores are ignored. A search by name never finds these.
  • "Only in" lists the functions with no counterpart, per folder, grouped by file with the number of each file's functions, then each function's first line, name and length.
  • In colour, levels are green (high), yellow (medium) and red (low), and the shares are green when complete, yellow when partial and red when nothing is paired. Pass --no-colors when you parse the console output, or read the JSON report instead.
  • If one folder has no functions, the report is one line of totals and <folder> has no functions yet. Check the path and the languages before concluding anything else.
4. Check the pairs
npx jscpd --compare billing-py/ billing-ts/ -r console-full

console-full adds every pair with its similarity and level, and marks the pairs found by name. Before reporting the comparison as reliable, read a sample:

  • two or three high pairs, to confirm the pairing works on this code;
  • every low pair, since related code pairs there too (a client call and the endpoint it calls, a function counting UTF-8 bytes and one converting a string to them);
  • the pairs marked by name, since a same-named function can do something else.

When a sample pair is wrong, say which one and why.

5. Check the functions with no counterpart

A function in an "Only in" list is either missing on the other side, or its counterpart was not recognized. Before calling it missing, search the other folder: in its counterpart file, under other names, as part of a larger function, or replaced by a library or platform call. The known gaps in pairing:

  • constructors across languages (a Java constructor and Rust's new, Kotlin's constructor, Swift's init) pair only when their code is similar enough;
  • a short function renamed on the other side (add_history for _finder_penalty_add_history);
  • one function split into several, or several merged into one: only the closest part may pair.

Functions that exist on one side only by design are expected: platform glue (an iOS delegate callback, an Android notification channel), helpers a language needs and another does not, features one side dropped.

6. Report

For the user, summarize in a few lines: the two percentages, the notable renamed pairs, the low pairs you checked and what you found, and the real gaps on each side, grouped by file or module. For a person who wants to explore the result, write the migration map, -r html: one offline page that draws both sides as dependency graphs with the pairs bridging them, and lists the same pairs as a table on a second tab. For a document or an issue, write the Markdown report and attach or paste it:

npx jscpd --compare billing-py/ billing-ts/ -r markdown -o .jscpd-compare

For your own processing, read the JSON report (-r json, written to jscpd-compare.json). It has a code and a tests section of the same shape. Each has sides[0] and sides[1], each with path, functions, matched, percentage, files (file, functions, matched, counterpart, similarity, lowPairs) unmatched (file, name, start, end) and readyToPort (the unmatched functions whose callees all have a counterpart, each with its number of callers), and pairs, each with a, b, similarity, level, renamed and matchedBy. Paths are relative to each folder. Write reports outside the repository or add the folder to .gitignore.

Options that change the result

  • --min-tokens and --min-lines set which functions count. Raise them to focus on substantial functions.
  • --ignore and --format narrow the files, for example to leave out tests or build scripts.
  • --semantic-model picks another model, and --semantic-url an OpenAI-compatible embeddings API; npx jscpd --semantic-models lists the models with calibrated thresholds. --semantic-threshold and --semantic-same-threshold move the thresholds; change them only after reading pairs on both sides of the new value.
  • --semantic-rebuild-cache embeds everything again, which is needed only when the cache is suspect.

Keep the folders, their order, the options and the model the same when you compare runs over time, or the numbers stop being comparable. The exit code is 0 whatever the result.

Limits

  • Similarity does not see small differences in behavior: two versions that drifted apart still pair, often at high. Only tests and reading the code catch drift.
  • The more one side is restructured, the fewer of its functions pair by code.
  • Only functions are compared.
  • The model runs on the CPU. The first run embedded about 25 functions a second on an Apple M1 (261 functions in 11 seconds) and can be several times slower on a small CI machine; later runs reuse the cache.
1---
2name: compare-codebases
3description: Compare two folders function by function with jscpd --compare, in the same language or across languages, and explain the result. Use when asked how two codebases relate, which functions one implementation has and the other lacks, how far a port has come, whether the iOS and Android versions of an app match, or which function in one folder corresponds to a function in the other.
4---
5 
6# compare-codebases
7 
8`jscpd --compare A B` pairs every function of folder `A` with the function of folder `B` that does the same job, and lists the functions of each folder that have no counterpart. The two folders may be in one language or in two (Python and TypeScript, Kotlin and Swift, Java and Rust). This skill explains how the comparison works and how to run one well. For porting code with the comparison as the progress measure, use the [code-migration](../code-migration/SKILL.md) skill; for the rest of jscpd, the [jscpd](../jscpd/SKILL.md) skill.
9 
10## How the comparison works
11 
12jscpd finds the functions of JavaScript, TypeScript, JSX, TSX, Vue, Svelte, Astro, Python, Rust, Go, Java, Kotlin, C#, C, C++, PHP, Ruby, Scala and Swift files in both folders. It turns the code of each function into a vector with a code embedding model that runs inside jscpd (CodeRankEmbed by default), so two functions that do the same job point the same way even when their languages, names and structure differ. Functions of one folder are never compared with each other.
13 
14Functions pair in two steps:
15 
161. By code. Two functions pair when each is the other's closest match in the other folder, their cosine similarity reaches the model's threshold (0.4125 across languages and 0.6375 within one language, with CodeRankEmbed), and the similarity stands out from the function's other matches. A function close to the best one also pairs when it reaches a higher bar, so a feature written twice on one side gets two pairs. Functions shorter than `--min-tokens` (30 with `--compare`) or `--min-lines` (5) stay out of this step, because a short function resembles too many others.
172. By name. A function left over pairs with a function of the other folder under the same name, once case, underscores, spaces and punctuation are ignored (`encodeBinary`, `encode_binary`, `_encode_binary`; the test title `rounds cents` and `rounds_cents`), when their similarity reaches the `medium` level (0.5625 across languages with CodeRankEmbed). A name pair skips the closest-match and stand-out checks of the first step, so it needs more than that step's threshold; otherwise every `load` and `init` of two codebases would pair. Size does not matter here, so a short port is found. A name pair has to stay within modules the first step linked. A module is the folder right under the deepest folder all files of a side share (`notification` in `android/notification/…`), and a file that sits higher than the rest, such as a build script, does not move that folder up. Two modules link when one holds the most of the other's pairs, counted for code and for tests separately. So `checkPermissions` of one plugin does not pair with its namesake in another.
18 
19Each pair gets a level on the scale of the model, because a cosine that is high for one model is low for another:
20 
21| Level | With CodeRankEmbed, across languages | Meaning |
22|---|---|---|
23| `high` | 0.7125 and up | almost always the same function |
24| `medium` | 0.5625 to 0.7125 | usually the same function, restructured |
25| `low` | 0.4125 to 0.5625 | read both: related code pairs here too |
26 
27jscpd measures tests and code apart, in two blocks of the report, and pairs a test only with a test. It tells a test by the conventions of its language:
28 
29- a test file such as `*_test.go`, `test_*.py`, `*.test.ts`, `*.spec.js`, `*Tests.swift` or `*_spec.rb`;
30- a test folder such as `tests/`, `__tests__/`, `spec/`, `src/test/` (where Java, Kotlin and Scala keep their tests) or `MyAppTests/`, the compared folder's own name included;
31- a Rust function in a `#[cfg(test)]` module or under `#[test]`;
32- a JavaScript or TypeScript test case such as `it('rounds cents', () => …)`.
33 
34When neither side has a test that counts (see below), the report has one block and no headings.
35 
36Totals count the functions of at least `--min-tokens` tokens and `--min-lines` lines; smaller ones appear only as partners. Declarations without a body (TypeScript overloads, the functions of a `.d.ts` file, interface and abstract methods) take no part. Anonymous functions (callbacks, closures) take no part either, except JavaScript and TypeScript test cases. A test case such as `it('rounds cents', () => …)` goes by its title, and so do those written with `test`, `specify`, `fit`, `xit`, `xtest` or `bench`, with `.only`, `.skip` or `.each(table)` after them. Suites and hooks stay anonymous. jscpd does not compare types, constants, SQL or UI markup.
37 
38## A way to compare two folders
39 
40### 1. Pick the folders
41 
42- Point at the code, not the repositories: `app/src/main/java` and `ios/Sources`, not the two repository roots. Build output, vendored code, installed packages and generated files dilute the result, and a vendored dependency tree can turn a run of seconds into one of tens of minutes, since every function in it is embedded. Inside a git repository jscpd skips what `.gitignore` excludes; outside one, or for folders the `.gitignore` misses, pass them with `--ignore` (`--ignore "**/vendor/**,**/target/**,**/node_modules/**"`). When a side has no `.gitignore` entries for such folders, suggest them to the user.
43- Two folders are required, and they must not overlap: `app/` and `app/android/` is refused.
44- Keep parallel structures when you can (`ios/<module>` and `android/<module>`). Modules steer the name step, so matching folder names help.
45- For a port, put the source first and the target second, so the first line of the report is the port's progress. For two implementations that both live on, the order does not matter.
46- One run covers tests and code, since the report measures the tests in a block of their own. When the user asks about the code alone, leave the tests out with `--ignore "**/__tests__/**,**/*.test.*,**/test/**"`. When they ask about the tests alone, pass the two test folders as the paths, or pick the test files with `--pattern`.
47 
48### 2. Get the model
49 
50The first run needs the model (548 MB). Ask the user before downloading it:
51 
52```bash
53npx jscpd --semantic-download
54```
55 
56jscpd caches the vectors per pair of folders, so later runs embed only the functions whose code changed.
57 
58### 3. Read the overview
59 
60```bash
61npx jscpd --compare billing-py/ billing-ts/
62```
63 
64```text
65 71% 5 of 7 functions in billing-py/ have a counterpart in billing-ts/
66 80% 4 of 5 functions in billing-ts/ have a counterpart in billing-py/
67 
68billing-py/
69 file paired similarity counterpart
70 billing.py 4 / 5 0.89 billing.ts
71 shipping.py 1 / 2 0.91 shipping.ts
72 
73billing-ts/
74 file paired similarity counterpart
75 billing.ts 3 / 4 0.89 billing.py
76 shipping.ts 1 / 1 0.91 shipping.py
77 
78Paired under other names (1):
79 billing-py/ billing-ts/ similarity
80 billing.py:28 tax_for_region billing.ts:27 salesTax 0.87 high
81 
82Only in billing-py/ (2):
83 billing.py (1)
84 46 due_date 6 lines
85 shipping.py (1)
86 18 estimate_delivery_days 8 lines
87 
88Only in billing-ts/ (1):
89 billing.ts (1)
90 39 toCurrency 8 lines
91```
92 
93- When both folders hold tests, the report has a `Code` block and a `Tests` block, each with everything below. The two top lines of a block give the share of each folder's functions (or tests) that have a counterpart in the other.
94- The file tables give each file's paired functions, the mean similarity of its pairs (with the number of `low` pairs, as in `0.62, 1 low`), and the file on the other side that holds most of its counterparts.
95- "Paired under other names" lists the pairs whose names differ even once case and underscores are ignored. A search by name never finds these.
96- "Only in" lists the functions with no counterpart, per folder, grouped by file with the number of each file's functions, then each function's first line, name and length.
97- In colour, levels are green (`high`), yellow (`medium`) and red (`low`), and the shares are green when complete, yellow when partial and red when nothing is paired. Pass `--no-colors` when you parse the console output, or read the JSON report instead.
98- If one folder has no functions, the report is one line of totals and `<folder> has no functions yet`. Check the path and the languages before concluding anything else.
99 
100### 4. Check the pairs
101 
102```bash
103npx jscpd --compare billing-py/ billing-ts/ -r console-full
104```
105 
106`console-full` adds every pair with its similarity and level, and marks the pairs found by name. Before reporting the comparison as reliable, read a sample:
107 
108- two or three `high` pairs, to confirm the pairing works on this code;
109- every `low` pair, since related code pairs there too (a client call and the endpoint it calls, a function counting UTF-8 bytes and one converting a string to them);
110- the pairs marked `by name`, since a same-named function can do something else.
111 
112When a sample pair is wrong, say which one and why.
113 
114### 5. Check the functions with no counterpart
115 
116A function in an "Only in" list is either missing on the other side, or its counterpart was not recognized. Before calling it missing, search the other folder: in its counterpart file, under other names, as part of a larger function, or replaced by a library or platform call. The known gaps in pairing:
117 
118- constructors across languages (a Java constructor and Rust's `new`, Kotlin's `constructor`, Swift's `init`) pair only when their code is similar enough;
119- a short function renamed on the other side (`add_history` for `_finder_penalty_add_history`);
120- one function split into several, or several merged into one: only the closest part may pair.
121 
122Functions that exist on one side only by design are expected: platform glue (an iOS delegate callback, an Android notification channel), helpers a language needs and another does not, features one side dropped.
123 
124### 6. Report
125 
126For the user, summarize in a few lines: the two percentages, the notable renamed pairs, the `low` pairs you checked and what you found, and the real gaps on each side, grouped by file or module. For a person who wants to explore the result, write the migration map, `-r html`: one offline page that draws both sides as dependency graphs with the pairs bridging them, and lists the same pairs as a table on a second tab. For a document or an issue, write the Markdown report and attach or paste it:
127 
128```bash
129npx jscpd --compare billing-py/ billing-ts/ -r markdown -o .jscpd-compare
130```
131 
132For your own processing, read the JSON report (`-r json`, written to `jscpd-compare.json`). It has a `code` and a `tests` section of the same shape. Each has `sides[0]` and `sides[1]`, each with `path`, `functions`, `matched`, `percentage`, `files` (`file`, `functions`, `matched`, `counterpart`, `similarity`, `lowPairs`) `unmatched` (`file`, `name`, `start`, `end`) and `readyToPort` (the unmatched functions whose callees all have a counterpart, each with its number of `callers`), and `pairs`, each with `a`, `b`, `similarity`, `level`, `renamed` and `matchedBy`. Paths are relative to each folder. Write reports outside the repository or add the folder to `.gitignore`.
133 
134## Options that change the result
135 
136- `--min-tokens` and `--min-lines` set which functions count. Raise them to focus on substantial functions.
137- `--ignore` and `--format` narrow the files, for example to leave out tests or build scripts.
138- `--semantic-model` picks another model, and `--semantic-url` an OpenAI-compatible embeddings API; `npx jscpd --semantic-models` lists the models with calibrated thresholds. `--semantic-threshold` and `--semantic-same-threshold` move the thresholds; change them only after reading pairs on both sides of the new value.
139- `--semantic-rebuild-cache` embeds everything again, which is needed only when the cache is suspect.
140 
141Keep the folders, their order, the options and the model the same when you compare runs over time, or the numbers stop being comparable. The exit code is 0 whatever the result.
142 
143## Limits
144 
145- Similarity does not see small differences in behavior: two versions that drifted apart still pair, often at `high`. Only tests and reading the code catch drift.
146- The more one side is restructured, the fewer of its functions pair by code.
147- Only functions are compared.
148- The model runs on the CPU. The first run embedded about 25 functions a second on an Apple M1 (261 functions in 11 seconds) and can be several times slower on a small CI machine; later runs reuse the cache.
149 

Discussion