score design
This page derives the properties of the scoring model in
src/score/impact_factor.mbt and
explains why it has this shape. The score API lists the
functions; the architecture guide shows where the index
builder gets the signals from.
Design goal
The score must order the packages of a registry snapshot by how much the ecosystem relies on them, from signals that a local registry index can provide. It must be cheap, deterministic and explainable: a reader of the web page should be able to see why one package outranks another, and the same signals must give the same score in MoonBit, in the index builder and in the browser.
Mathematical background
The score
Write , and for the number of dependents, recent dependents and downloads, and for the days since the latest release. The implementation computes
where is the base score and is the step function
with negative treated as . Nothing else enters the score: there is no normalisation against the rest of the registry, so a package’s score does not change when other packages are added.
A logarithmic index
Because , the base score is the logarithm of a weighted product:
The product is a Cobb–Douglas index11 The Cobb–Douglas form comes from production economics (Cobb and Douglas, 1928). Its logarithm is linear in , which is why ranking by is the same as ranking by the weighted geometric mean of the shifted counts. of the shifted counts, and the weights are its elasticities: . Two consequences follow directly.
Doubling adds a constant. Since , doubling adds points to , whatever was. The same step is worth points for recent dependents and for downloads. A package with 1000 dependents gains as much from the next 1001 as a package with 10 gains from the next 11.
Diminishing returns. One more dependent adds
using with . The marginal value of a dependent falls like , so no single signal can dominate the ranking.
Rank thresholds as counts
The rank buckets are thresholds on : S from , A from , B
from , C from . Inverting shows what they mean in counts.
With and a single non-zero signal of weight , the score reaches a
threshold when
so the smallest integer count is :
| Threshold | Dependents only () | Recent dependents only () | Downloads only () |
|---|---|---|---|
C () | 3 | 6 | 9 |
B () | 18 | 58 | 148 |
A () | 114 | 785 | 3575 |
S () | 936 | 15208 | 135697 |
For example while , so
936 dependents are the first count to reach S on their own. In practice the
signals combine: 20 dependents, 4 recent dependents and 300 downloads already
give .
Growth and momentum
A snapshot evaluates the score twice, on the current signals and on the signals of 30 days ago, and defines
Since , exactly when every historical count is : the package had no dependents and no downloads 30 days ago. For such a package the relative growth is undefined, and the implementation uses (that is, 100 %) instead of so that stays finite and can be stored and sorted.
The momentum label tests three conditions at two levels:
The Rising conditions imply the Hot conditions, so the classes are nested
levels of one scale rather than independent tags. When ,
is the same as , so Rising asks for a
score at least times the old one and an absolute gain of
points. The absolute bound stops tiny packages from rising by going from
to points; the relative bound stops large packages from rising
through the noise of a big base.
Design decisions
Logarithms of counts
Problem. Dependent and download counts are heavy-tailed: a few packages have thousands, most have none. A linear score would make the ranking a leaderboard of the largest package in each signal.
Options. Raw counts; ranks or percentiles within the registry; square roots; logarithms.
Choice. . The shift by one keeps finite and makes exactly when a package has no signal at all. Percentiles would need the whole registry and change a package’s score when other packages appear, which breaks the CLI’s one-package-at-a-time contract. Square roots still grow without the scale invariance derived above.
Additive weights
Problem. The three signals must be combined into one number.
Choice. A weighted sum of logarithms with weights . Total dependents capture established adoption and weigh most. Downloads are an external popularity hint that is missing for packages the builder could not look up, so they weigh least. Recent dependents are counted on top of total dependents, so a dependent from the recent window contributes to both terms: the recent term is a bonus for current adoption, not a separate population. The weights are editorial choices of this project, not fitted parameters.
A recency multiplier, not a recency term
Problem. Old, unmaintained packages should not hold their rank forever, but age must not outweigh adoption.
Choice. A multiplicative step function between and . Because
it multiplies , it changes the score by at most , and the ratio
between the freshest and the oldest package with the same signals is
. That can move a package across one rank boundary
(for example gives S at and A at ) but never turns
an unused package into a ranked one: stays . An additive age term
would give unused but freshly released packages a positive score.
Fixed thresholds for labels
Problem. The web pages need short, stable labels.
Choice. Constant thresholds on and . Labels therefore mean the same in every snapshot and need no registry-wide statistics. Quantile buckets (“top 5 %”) would need the whole registry, like percentile scores.
Integers in, Double out
Problem. The signals are counts, but the score is real-valued.
Choice. All inputs are Int, and negative values are clamped instead of
rejected, so every function is total and can be called on raw database
values. The scoring functions never abort and return no Result; the only
non-finite output is the overflow described under
numerical accuracy.
Correctness / invariants
Monotonicity
For counts in , is non-decreasing in , and , and strictly increasing as long as the count stays below : is strictly increasing, the weights are positive and , so
Above the conversion through Float (below) can map neighbouring
counts to the same value, so strict growth degrades to non-decreasing.
is non-increasing in because is. The blackbox tests in
impact_factor_test.mbt check instances of this for dependents, downloads
and release age.
Range
, with equality exactly when . For counts up to , , so
Labels are total
rank_label and compute_momentum_label return one of their labels for every
input. Every comparison with NaN is false, so a NaN score is ranked D
and has Stable momentum.
Numerical accuracy
log_signal converts to Float before taking the logarithm in
Double. Every integer up to is exact in Float; above it the
conversion rounds to nearest with relative error .
Then
and the error in from this conversion is at most
, on top of the
ordinary Double rounding of a few operations. The index builder still
contains an unused Python copy of the formula (compute_score in
scripts/build_index.py) that calls math.log1p without the Float step;
it agrees with the MoonBit result to this bound plus a few units in the last
place. The database itself is filled through the MoonBit CLI.
The addition is done in Int. For it wraps to
, the logarithm of a negative number is NaN, and the score is
NaN.
Snapshot consistency
compute_score_snapshot computes every field from the same two calls of
compute_score, so score_growth_30d == score - score_30d_ago holds exactly
(it is the same floating-point subtraction), and rank_label and
momentum_label are always the labels of the stored numbers.
How scores are ranked
The package only computes scores; consumers sort them. Every ordering in the
repository breaks ties deterministically: the ranked feeds and the default
search order use score descending, then full_name ascending, and the
Hot and Rising feeds use score_growth_30d descending, then score,
then full_name. The static_search design gives the
orderings of the browser search.
Alternatives rejected
- PageRank-style centrality on the dependency graph would reward being depended on by important packages, but it needs the whole graph, an iterative solver and a damping parameter, and it cannot be explained on a package page. The direct dependent count is the first step of that iteration and is enough for a registry of this size.
- Transitive dependents were not used: they count the same downstream package many times through every path and favour low-level packages even more than the logarithm can correct.
- Learning the weights from a labelled ranking would need labels that do not exist; the fixed weights are stated in the code and in this page.
- Returning
Resultfor negative inputs would push error handling into every caller for a condition that has an obvious meaning (no signal).
Boundaries
- The score measures adoption inside one registry snapshot. It does not measure code quality, correctness, security or maintenance effort.
- The package takes the signals as given. Collecting them, deciding which dependents are recent and which downloads are trusted is the index builder’s job, described in the architecture guide.
- The builder currently passes
0as the historical download count, soscore_growth_30dcontains the whole download term of the current score. Read growth together with the dependent counts, which the momentum label does by requiring recent dependents. - There is no normalisation across packages, no time decay inside a window and no confidence interval: a score is a deterministic function of four integers.
- Counts of
2147483647are not supported (the score becomesNaN).
Footnotes
-
The Cobb–Douglas form comes from production economics (Cobb and Douglas, 1928). Its logarithm is linear in , which is why ranking by is the same as ranking by the weighted geometric mean of the shifted counts. ↩