Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions data/summary.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"lastUpdated": "2026-07-22T06:49:31.191Z",
"lastUpdated": "2026-07-22T20:21:16.853Z",
"total": {
"adoption": 285177,
"adoptionExCrypto": 264793,
"adoption": 150108,
"adoptionExCrypto": 132929,
"npm": 112222,
"pypi": 28487,
"cloneUniques": 1021,
"clones": 136090,
"docker": 8378,
"views": 28164,
Expand All @@ -27,6 +28,7 @@
"excludingCrypto": {
"npm": 110755,
"pypi": 12784,
"cloneUniques": 1012,
"clones": 132876,
"docker": 8378
}
Expand Down
71 changes: 71 additions & 0 deletions lib/adoption.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* The one definition of "adoption" — shared by the API (pages/api/overview.js,
* server) and the dashboard (pages/index.js, client) so the number can never
* drift between where it's computed and where it's shown.
*
* Adoption sums acquisition events: distinct code-pullers + package installs +
* image pulls + model downloads. The clone term is UNIQUE CLONERS, never the raw
* git-clone count.
*
* Why uniques and not raw clones: GitHub's clone `count` is dominated by
* automation — a single CI/mirror/scanner re-cloning a repo generates thousands
* of clones from a handful of actors (hackmyagent: ~2,000 clones/day from 5-10
* unique cloners, a 147x re-clone ratio). Raw count answers "how many git-clone
* operations happened", which is not adoption. Unique cloners answers "how many
* distinct people/systems pulled the code", which is. Raw clone count is retained
* on the GitHub-traffic surfaces as an operational metric, never in adoption.
*
* What "unique cloners" is HERE: GitHub's own 14-day DEDUPLICATED distinct-cloner
* count (`traffic_summary.clones_uniques`, surfaced as `recentUniqueCloners`).
* That is the only honest distinct-cloner figure GitHub exposes — you cannot
* deduplicate cloners across days you never captured identity for, so there is no
* true 30-day or all-time unique-cloner count to be had. It is therefore a
* ROLLING 14-DAY SNAPSHOT (like Chrome weekly-active users or the 14-day unique
* visitors), shown identically under every period rather than re-windowed.
*
* Deliberately NOT used: SUM(traffic_clones.uniques) over a window. That sums
* GitHub's per-day distinct-cloner counts, i.e. cloner-DAYS — a cloner active on
* N days counts N times — which re-inflates the exact signal this fix removes and
* would be a number labeled "unique cloners" that is not one.
*/

// Pick the value for the selected period from an object whose fields are named
// per-window (all / 30d / 7d / 24h / custom). Shared shape across github/npm/pypi.
function periodPick(o, allKey, k30, k7, k24, period, kCustom) {
if (!o) return 0;
switch (period) {
case '24h': return o[k24] || 0;
case '7d': return o[k7] || 0;
case '30d': return o[k30] || 0;
case 'custom': return o[kCustom] || 0;
default: return o[allKey] || 0;
}
}

// HuggingFace only reports all-time + rolling 30d, nothing finer.
function hfPeriod(hf, period) {
if (!hf) return null;
if (period === 'all') return hf.downloadsAllTime || 0;
if (period === '30d') return hf.downloads30d || 0;
return null; // 24h / 7d / custom -> not measured at this granularity
}

// The invariant: adoption is cloners + installs, never raw clone count. Every
// surface that reports adoption funnels through here.
function sumAdoption({ cloneUniques = 0, npm = 0, pypi = 0, docker = 0, hf = 0 }) {
return cloneUniques + npm + pypi + docker + hf;
}

// Period-aware adoption for one product row (used by the dashboard table + KPIs).
// The clone term is the 14-day deduplicated unique-cloner SNAPSHOT (period-
// independent — see the module header), NOT raw clones and NOT summed cloner-days.
function rowAdoption(p, period) {
const cloneUniques = p.github?.cloneUniques || 0; // 14-day deduped snapshot
const npm = periodPick(p.npm, 'allTimeDownloads', 'last30Downloads', 'last7Downloads', 'last24hDownloads', period, 'customDownloads');
const pypi = periodPick(p.pypi, 'allTimeDownloads', 'last30Downloads', 'last7Downloads', 'last24hDownloads', period, 'customDownloads');
const docker = period === 'all' ? (p.docker?.totalPulls || 0) : 0;
const hf = hfPeriod(p.hf, period) || 0;
return sumAdoption({ cloneUniques, npm, pypi, docker, hf });
}

module.exports = { periodPick, hfPeriod, sumAdoption, rowAdoption };
16 changes: 13 additions & 3 deletions lib/repos.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ function mergeByCanonical(repoStats) {
totalClones: sum('totalClones'),
clones24h: sum('clones24h'), clones7d: sum('clones7d'), clones30d: sum('clones30d'),
customClones: sum('customClones'),
// 14-day deduplicated uniques are a snapshot — take the canonical (live) one.
// 14-day deduplicated uniques are a snapshot (the adoption clone term) — take
// the canonical (live) row's value, never summed across twins.
recentUniqueVisitors: c.recentUniqueVisitors || 0,
recentUniqueCloners: c.recentUniqueCloners || 0,
// Snapshot metrics — from the canonical row, never summed.
Expand Down Expand Up @@ -117,21 +118,30 @@ function canonicalRepoTotals(db) {
const latestFork = db.prepare(
'SELECT total_forks FROM forks WHERE repo_id = ? ORDER BY date DESC LIMIT 1'
);
// Latest 14-day deduplicated distinct-cloner count per repo (a snapshot).
const latestUniqCloners = db.prepare(
'SELECT clones_uniques FROM traffic_summary WHERE repo_id = ? ORDER BY date DESC LIMIT 1'
);
const rows = repos.map(r => ({
full_name: r.full_name,
canonical_full_name: r.canonical_full_name,
stars: latestStar.get(r.id)?.total_stars || 0,
forks: latestFork.get(r.id)?.total_forks || 0,
uniqueCloners: latestUniqCloners.get(r.id)?.clones_uniques || 0,
}));

let stars = 0, forks = 0, repoCount = 0;
let stars = 0, forks = 0, repoCount = 0, uniqueCloners = 0;
for (const [canon, group] of groupByCanonical(rows)) {
const c = pickCanonical(group, canon);
stars += c.stars || 0;
forks += c.forks || 0;
// 14-day dedup snapshot from the canonical (live) row — NEVER summed across
// twins (you cannot add two rolling 14-day dedup counts). Stale slug's own
// clones_uniques is dropped, matching mergeByCanonical / the overview API.
uniqueCloners += c.uniqueCloners || 0;
repoCount += 1;
}
return { stars, forks, repoCount };
return { stars, forks, repoCount, uniqueCloners };
}

module.exports = {
Expand Down
3 changes: 3 additions & 0 deletions lib/standards.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ function mergeSpec(rows) {
github: {
views: sum('totalViews'), views24h: sum('views24h'), views7d: sum('views7d'), views30d: sum('views30d'), customViews: sum('customViews'),
clones: sum('totalClones'), clones24h: sum('clones24h'), clones7d: sum('clones7d'), clones30d: sum('clones30d'), customClones: sum('customClones'),
// 14-day deduplicated distinct cloners (snapshot) — from the canonical row, not summed.
cloneUniques: canonical.recentUniqueCloners || 0,
stars: canonical.stars || 0,
starsGrowth24h: canonical.starsGrowth24h || 0, starsGrowth7d: canonical.starsGrowth7d || 0, starsGrowth30d: canonical.starsGrowth30d || 0,
starsGrowthAll: canonical.starsGrowthAll || 0, starsGrowthCustom: canonical.starsGrowthCustom || 0,
Expand All @@ -73,6 +75,7 @@ function sumGithub(members) {
return {
views: acc('views'), views24h: acc('views24h'), views7d: acc('views7d'), views30d: acc('views30d'), customViews: acc('customViews'),
clones: acc('clones'), clones24h: acc('clones24h'), clones7d: acc('clones7d'), clones30d: acc('clones30d'), customClones: acc('customClones'),
cloneUniques: acc('cloneUniques'),
stars: acc('stars'),
starsGrowth24h: acc('starsGrowth24h'), starsGrowth7d: acc('starsGrowth7d'), starsGrowth30d: acc('starsGrowth30d'),
starsGrowthAll: acc('starsGrowthAll'), starsGrowthCustom: acc('starsGrowthCustom'),
Expand Down
45 changes: 35 additions & 10 deletions pages/api/overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const path = require('path');
const { computeWindowedStats, computeStarGrowth } = require('../../lib/windowing');
const { groupStandards } = require('../../lib/standards');
const { mergeByCanonical } = require('../../lib/repos');
const { sumAdoption } = require('../../lib/adoption');

/**
* Overview API: Returns combined GitHub + npm metrics suitable for
Expand Down Expand Up @@ -74,6 +75,12 @@ export default async function handler(req, res) {
FROM traffic_clones WHERE repo_id = ? AND date > date(${clonesAnchor}, '-30 days')
`).get(repo.id);

// Unique cloners — the adoption signal (distinct pullers) — is GitHub's own
// 14-day DEDUPLICATED count (traffic_summary.clones_uniques, read into
// recentUniqueCloners below), NOT a sum of per-day uniques. See lib/adoption.js:
// summing daily uniques double-counts a cloner across days and re-inflates the
// exact noise this replaces. GitHub only dedupes over 14 days, so this is a
// rolling snapshot, handled like recentUniqueVisitors / Chrome users.
const summary = db.prepare(`
SELECT views_uniques, clones_uniques
FROM traffic_summary WHERE repo_id = ?
Expand All @@ -88,7 +95,9 @@ export default async function handler(req, res) {
ORDER BY date DESC LIMIT 1
`).get(repo.id);

// Custom date range queries for GitHub traffic
// Custom date range queries for GitHub traffic. (Unique cloners has no
// custom-range form — GitHub only dedupes over a rolling 14 days, so the
// snapshot in recentUniqueCloners is used for every window.)
let customViews = 0, customClones = 0;
if (isCustomRange) {
const cv = db.prepare(`
Expand Down Expand Up @@ -118,6 +127,7 @@ export default async function handler(req, res) {
clones30d: clones30d.total,
customClones,
recentUniqueVisitors: summary?.views_uniques || 0,
// 14-day deduplicated distinct cloners — the adoption clone term. See lib/adoption.js.
recentUniqueCloners: summary?.clones_uniques || 0,
...starGrowth,
forks: forks?.forks || 0,
Expand Down Expand Up @@ -362,6 +372,8 @@ export default async function handler(req, res) {
const matchedChrome = chromeStats.filter(c => (config.chromeExtensions || []).includes(c.extensionId));

const githubClones = matchedRepos.reduce((s, r) => s + r.totalClones, 0);
// 14-day deduplicated distinct cloners across the tool's repos (snapshot).
const githubCloneUniques = matchedRepos.reduce((s, r) => s + (r.recentUniqueCloners || 0), 0);
const npmDownloads = matchedPackages.reduce((s, p) => s + p.allTimeDownloads, 0);
const pypiDownloads = matchedPypi.reduce((s, p) => s + p.allTimeDownloads, 0);
const dockerPulls = matchedDocker.reduce((s, d) => s + d.totalPulls, 0);
Expand All @@ -381,6 +393,9 @@ export default async function handler(req, res) {
clones7d: matchedRepos.reduce((s, r) => s + r.clones7d, 0),
clones30d: matchedRepos.reduce((s, r) => s + r.clones30d, 0),
customClones: matchedRepos.reduce((s, r) => s + (r.customClones || 0), 0),
// 14-day deduplicated distinct cloners (snapshot) — the clone term used
// in adoption. Period-independent; see lib/adoption.js.
cloneUniques: githubCloneUniques,
stars: matchedRepos.reduce((s, r) => s + r.stars, 0),
starsGrowth24h: matchedRepos.reduce((s, r) => s + (r.starsGrowth24h || 0), 0),
starsGrowth7d: matchedRepos.reduce((s, r) => s + (r.starsGrowth7d || 0), 0),
Expand Down Expand Up @@ -429,9 +444,11 @@ export default async function handler(req, res) {
ratingCount: matchedChrome[0]?.ratingCount ?? null,
extensionCount: matchedChrome.length,
},
// Combined adoption: clones + npm + pypi + docker pulls + HF model downloads.
// Chrome weekly-active users are intentionally excluded (snapshot, not installs).
totalAdoption: githubClones + npmDownloads + pypiDownloads + dockerPulls + hfDownloads,
// Combined adoption: UNIQUE CLONERS + npm + pypi + docker pulls + HF model
// downloads. Raw clone count is deliberately NOT used — it is CI/mirror
// re-clone noise (see lib/adoption.js). Chrome weekly-active users are
// excluded too (a snapshot, not installs).
totalAdoption: sumAdoption({ cloneUniques: githubCloneUniques, npm: npmDownloads, pypi: pypiDownloads, docker: dockerPulls, hf: hfDownloads }),
};
});

Expand Down Expand Up @@ -465,6 +482,7 @@ export default async function handler(req, res) {

if (ungroupedRepos.length > 0 || ungroupedNpm.length > 0 || ungroupedPypi.length > 0 || ungroupedDocker.length > 0 || ungroupedHf.length > 0) {
const ugClones = ungroupedRepos.reduce((s, r) => s + r.totalClones, 0);
const ugCloneUniques = ungroupedRepos.reduce((s, r) => s + (r.recentUniqueCloners || 0), 0);
const ugNpm = ungroupedNpm.reduce((s, p) => s + p.allTimeDownloads, 0);
const ugPypi = ungroupedPypi.reduce((s, p) => s + p.allTimeDownloads, 0);
const ugDocker = ungroupedDocker.reduce((s, d) => s + d.totalPulls, 0);
Expand All @@ -483,6 +501,7 @@ export default async function handler(req, res) {
clones7d: ungroupedRepos.reduce((s, r) => s + r.clones7d, 0),
clones30d: ungroupedRepos.reduce((s, r) => s + r.clones30d, 0),
customClones: ungroupedRepos.reduce((s, r) => s + (r.customClones || 0), 0),
cloneUniques: ugCloneUniques,
stars: ungroupedRepos.reduce((s, r) => s + r.stars, 0),
starsGrowth24h: ungroupedRepos.reduce((s, r) => s + (r.starsGrowth24h || 0), 0),
starsGrowth7d: ungroupedRepos.reduce((s, r) => s + (r.starsGrowth7d || 0), 0),
Expand Down Expand Up @@ -522,7 +541,7 @@ export default async function handler(req, res) {
usersGrowth7d: 0, usersGrowth30d: 0, rating: null, ratingCount: null,
extensionCount: ungroupedChrome.length,
},
totalAdoption: ugClones + ugNpm + ugPypi + ugDocker + ugHf,
totalAdoption: sumAdoption({ cloneUniques: ugCloneUniques, npm: ugNpm, pypi: ugPypi, docker: ugDocker, hf: ugHf }),
});
}

Expand All @@ -542,6 +561,8 @@ export default async function handler(req, res) {
clones24h: repoStats.reduce((s, r) => s + r.clones24h, 0),
clones7d: repoStats.reduce((s, r) => s + r.clones7d, 0),
clones30d: repoStats.reduce((s, r) => s + r.clones30d, 0),
// 14-day deduplicated distinct cloners across all repos (snapshot).
totalCloneUniques: repoStats.reduce((s, r) => s + (r.recentUniqueCloners || 0), 0),
totalStars: repoStats.reduce((s, r) => s + r.stars, 0),
starsGrowth24h: repoStats.reduce((s, r) => s + (r.starsGrowth24h || 0), 0),
starsGrowth7d: repoStats.reduce((s, r) => s + (r.starsGrowth7d || 0), 0),
Expand Down Expand Up @@ -583,11 +604,14 @@ export default async function handler(req, res) {
usersGrowth30d: chromeStats.reduce((s, c) => s + (c.usersGrowth30d || 0), 0),
},
combined: {
totalAdoption: repoStats.reduce((s, r) => s + r.totalClones, 0) +
npmStats.reduce((s, p) => s + p.allTimeDownloads, 0) +
totalPypiDownloads +
totalDockerPulls +
totalHfDownloads,
// 14-day deduplicated unique cloners, not raw clone count — see lib/adoption.js.
totalAdoption: sumAdoption({
cloneUniques: repoStats.reduce((s, r) => s + (r.recentUniqueCloners || 0), 0),
npm: npmStats.reduce((s, p) => s + p.allTimeDownloads, 0),
pypi: totalPypiDownloads,
docker: totalDockerPulls,
hf: totalHfDownloads,
}),
totalPageViews: repoStats.reduce((s, r) => s + r.totalViews, 0),
},
};
Expand Down Expand Up @@ -678,6 +702,7 @@ export default async function handler(req, res) {
stars: standards.reduce((s, f) => s + (f.github.stars || 0), 0),
views: standards.reduce((s, f) => s + (f.github.views || 0), 0),
clones: standards.reduce((s, f) => s + (f.github.clones || 0), 0),
cloneUniques: standards.reduce((s, f) => s + (f.github.cloneUniques || 0), 0),
forks: standards.reduce((s, f) => s + (f.github.forks || 0), 0),
};

Expand Down
Loading
Loading