kindred-drift
v0.1.0
Published
Template-less drift detection for a folder of sibling repos
Maintainers
Readme
kindred
Template-less drift detection for a folder of sibling repos.
You never declared a template, but your repos clearly share one. kindred finds it, and shows you where each repo has wandered off.
If you maintain a handful of small repos, you know the pattern. You copy a LICENSE, a .gitignore, a CI workflow from the last project into the new one. Six months later one repo has a fixed workflow, another has a stale .gitignore, a third never got CONTRIBUTING.md at all. There is a shared template in there somewhere. It just lives in your head.
kindred scans a folder of sibling git repos, clusters the files they share, and prints a drift matrix: which files are identical everywhere, which have drifted (and by how much), and which repos are missing them entirely. No template file, no manifest, no config. The repos themselves are the template.
It is strictly read-only. kindred never writes a single byte inside the repos it scans.
A real run
This is actual output from running kindred on the folder where kindred itself was built, alongside its sibling projects:
$ kindred scan "/Users/benmalaga/Github Projects"
4 repos under /Users/benmalaga/Github Projects:
claudemd-check, lockbisect, nopus, wastegate
file claudemd-check lockbisect nopus wastegate
-------------------------- -------------- ---------- ----- ---------
.github/workflows/test.yml = = -- =
.gitignore ~75% = = =
CONTRIBUTING.md -- = ~24% ~33%
LICENSE = = = =
README.md = ~30% ~27% ~33%
package.json = ~58% ~56% ~57%
src/cli.js = ~26% ~14% ~31%
legend: = identical to reference ~NN% drifted (similarity) -- missing
7 shared files across 4 repos: 1 identical everywhere, 5 drifted, 2 missing somewhereOne glance tells the whole story: the LICENSE is in sync everywhere, nopus never got a CI workflow, claudemd-check is missing CONTRIBUTING.md, and one repo's .gitignore has drifted from the other three. To see exactly how, ask for the diff:
$ kindred diff .gitignore "/Users/benmalaga/Github Projects"
--- lockbisect/.gitignore
+++ claudemd-check/.gitignore
@@ -1,4 +1,4 @@
node_modules/
*.log
.DS_Store
-*.tgz
+.claudemd-baseline.jsonInstall
No install needed:
npx kindred-drift scan ~/projectsOr install globally:
npm install -g kindred-drift
kindred scan ~/projectsOr run straight from a clone (there are zero dependencies, so there is no npm install step):
git clone https://github.com/BenMalaga/kindred.git
node kindred/bin/kindred.js scan ~/projectsWhy
kindred-drift? The npm nameskindred(a 2012 blogging engine) andkindred-cliwere both already taken, so the package is published askindred-drift. The binary is stillkindred.
Requires Node 18 or newer. Zero runtime dependencies, zero dev dependencies.
Usage
kindred scan [dir] [--json] scan sibling git repos under dir (default: cwd)
and print the drift matrix
kindred diff <relpath> [dir] unified diff between the variants of a shared file
kindred --help show help
kindred --version show versionscan looks at the immediate subdirectories of dir, keeps the ones that are git repos, and compares the files they share. --json emits the full result (statuses and similarity scores per repo) for scripting.
diff shows a unified diff between the reference variant of a shared file and every other variant. If all copies are identical it says so and exits 0. If the file is not shared, it exits 1.
How it works
Discover. Immediate subdirectories of the target folder that contain
.gitare treated as sibling repos. Everything else is ignored, including half-checked-out folders that are not repos yet.Collect. Each repo is walked, skipping
node_modules,.git,dist,build,coverage, and friends. Binary files, lockfiles (package-lock.json,yarn.lock, and other machine-generated files), and files over 1 MiB are excluded.Cluster. A file becomes a comparison cluster if its relative path appears in 2 or more repos, or if it is a well-known shareable file (LICENSE,
.gitignore, anything under.github/,tsconfig.json,.eslintrc*,CONTRIBUTING.md, and so on) present in at least one repo. Well-known files are reported even when only one repo has them, because absence is drift too.Normalize. Before comparison, content is normalized: CRLF becomes LF, trailing whitespace is stripped, and runs of blank lines collapse to one. Two files that differ only cosmetically count as identical.
Compare. Within each cluster, identical normalized content is grouped into variants. The variant held by the most repos becomes the reference (ties break toward the alphabetically first repo). Every other variant gets a similarity score against the reference: the normalized diff ratio
2 * LCS(a, b) / (|a| + |b|)over lines, the same family of metric as Python'sdifflib.ratio. It is order-sensitive on purpose: reordered config is changed config. For very large file pairs, kindred falls back to a linear-time multiset overlap ratio.Report. Each repo's cell in the matrix is one of:
=(holds the reference variant),~NN%(drifted, with similarity), or--(missing).
Why not cruft, copier, or repo-file-sync-action?
Those are all excellent tools, and they all share one assumption: a declared template that exists before your repos do. cruft and copier check projects against a cookiecutter-style template repo. repo-file-sync-action pushes files from a designated source repo to targets you enumerate in a config file.
kindred starts from the opposite end. Most people with five sibling repos never made a template; they made five repos that rhyme. kindred infers the implicit template you already have, by observation, and tells you where reality disagrees with it. There is nothing to set up, nothing to declare, and nothing to keep in sync about the syncing tool itself. Run one command in a folder you already have.
If kindred convinces you that you want a real template, great: graduate to copier. Until then, you do not need one to see your drift.
Roadmap
v1 is deliberately read-only: scan, matrix, diff. Planned next:
kindred bless <relpath> <repo>: mark one repo's variant as the canonical one for that file, recorded outside the scanned repos.kindred apply <relpath>: copy the blessed (or reference) variant into the repos that drifted or lack the file, with a dry-run by default and per-repo confirmation. This will be the first and only command that writes into scanned repos, and it will be loudly explicit about it.- Similarity-based clustering of files whose paths differ but whose content rhymes (the
ci.ymlin one repo that is really thetest.ymlfrom the others). --fail-on-driftexit codes for CI.
Contributing
See CONTRIBUTING.md. The short version: zero dependencies, Node 18+, node --test must stay green, and scanning must stay read-only.
License
MIT (c) 2026 Ben Malaga
