Measure whether the copies that should be identical still are — and never say which one is right.
2026-08-27. Two measurements on the same day, in the same estate:
One configuration file existed in 29 copies, in 4 distinct versions, under 3 different naming conventions. Nobody knew that until it was counted.
Separately, one of our own components — a plugin running on dozens of client sites we operate — was found in 6 distinct contents and declared only 3 version numbers — including one install that claimed the same version as the majority while differing from it, and one that carried the highest version number while having lost five files the others had.
Both findings share a shape: the declared version number does not identify the content. It is a declared value, not a measured one. Anything that trusts it is counting wrong, and no completeness check will notice, because the field is full.
For every copy it builds a manifest — sha256 per file, relative path — and
reduces it to one fingerprint: sha256 over the sorted digest-and-path lines.
Sorting is not cosmetic. os.walk does not guarantee an order, and without a stable
one two identical copies produce two different fingerprints; the tool would report
divergence everywhere and mean nothing by it.
Then it:
- groups by content, never by name and never by declared version;
- compares the declared version against the measured content, and names the contradiction when the same claimed version covers different contents. A tool that only groups by content sees those as two ordinary variants rather than as a lie;
- shows the three-set difference between groups: only in A, only in B, and in both but different. The third set is the one that distinguishes a superset from a divergent branch. Without it, a copy with two extra files looks like a newer version even when it has lost five others.
It will not tell you which copy is the correct one, and it will not align anything.
Choosing is a decision. A tool that decides quietly will one day overwrite the copy that had the thing nobody else had — the translation, the local fix, the field that was only ever set in one place.
Every run states how many copies were measured out of how many declared, how many files were read, which copies were missing, which files and directories could not be read, and what was excluded.
A directory that cannot be listed, and a symlink to a directory, are not entered. Both are named in the coverage, because every file inside them is left out of the comparison. A FIFO or device is not read and is counted as unreadable.
If an include filter was used, the run says so — divergence outside the filter was
not measured. Measured divergence is a lower bound, never an upper one. A partial
coverage can call two things identical when they are not, and it always errs in the
same direction.
A run that measured nothing prints NOTHING WAS MEASURED. This is not a pass. and
exits 2. That includes a run where the copies exist but no file was read in any of
them — an include that matches nothing, for example. Equal empty manifests are not
evidence that the copies are the same.
None needed. Python 3.9+, standard library only. The single file runs as it is:
curl -O https://raw.githubusercontent.com/langacorp/samecheck/main/samecheck.py
python3 samecheck.py --selftest
Or install it from git, which puts a samecheck command on the path. It is not
published on PyPI.
pipx install git+https://github.com/langacorp/samecheck
pip install git+https://github.com/langacorp/samecheck
samecheck --selftest
python3 samecheck.py --selftest
Five directions, all asserted, any one failing fails the test:
- must fire on copies that differ — one by a changed file, one by an added file
- must stay silent on identical copies, including when one has a
node_modulesthe other lacks - must name the contradiction — same declared version, different content
- must not look like a pass when nothing was measured
- the fingerprint must not depend on file order — the same manifest fed in reverse must produce the same fingerprint
The unit tests in tests/ check each part on its own, in both directions:
python3 -m unittest discover -s tests -v
python3 samecheck.py /srv/site-a/plugin /srv/site-b/plugin /srv/site-c/plugin
python3 samecheck.py -c copies.json --json
copies.example.json:
{
"copies": [
"/srv/site-a/wp-content/plugins/example",
"/srv/site-b/wp-content/plugins/example"
],
"exclude": ["node_modules", ".git", "cache"],
"include": "\\.php$",
"declared_version": {
"file": "example.php",
"pattern": "Version:\\s*([0-9.]+)"
}
}declared_version is optional; without it the tool groups by content only and
cannot name contradictions. Its pattern needs one capture group.
--exclude and --include on the command line also apply next to a config:
--exclude adds to its list, --include replaces its filter. An exclude with a
slash, such as vendor/bin, matches that run of directories at any depth.
A copy can be a single file. Its name is not part of the fingerprint, so the same
content kept as config.php and config-old.php is one content.
Exit codes: 0 one content · 1 divergence found · 2 nothing was measured, or
the config or a regex could not be used. The same codes apply with --json.
- It compares files on disk. Database rows, registered cron jobs and licence state are not files, are not measured, and can make two copies with the same fingerprint behave differently.
- Two files can be functionally equivalent and byte-different — a timestamp in a header, different line endings. Those are reported as divergence, because they are, and deciding they do not matter is a decision.
--includemakes a run faster and the answer smaller. It never makes it safer.
Each came out of a defect measured on our own estate. Each one is standalone and depends on none of the others.
- realroute — checks that a route really exists, by content and not by status code.
- leakform — finds secrets in a git repository by shape, across every ref.
- provenreal — compares what a system claims with what can be measured, from independent sources.
- countdrift — finds numbers written by hand that no longer match their source.
- kemproof — attests that an ML-KEM-768 key exchange really happened. It does not encrypt anything.
The set is kept on the organisation profile. It is not written here as a count, because a number typed by hand is the thing countdrift exists to find.
LANGA runs an ecosystem of digital services on its own infrastructure. This tool came out of a defect we hit while running them: the same component installed in many places, quietly no longer the same. Those places are:
- LANGA — the ecosystem
- Drive In — marketing packages for local business
- LANGA Tools — WordPress toolkit for developers
- eFruit — food marketplace for local producers
See How we work.
MIT. See LICENSE.
Built and maintained by LANGA.