@molecule/api-rank-score
v1.0.1
Published
Pure-function ranking algorithms (HN decay, Reddit hot/best/controversial, recency, score).
Readme
@molecule/api-rank-score
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Pure-function ranking algorithms for link- and news-aggregator style apps: HN time-decay rank, Reddit hot/best/controversial, plain recency and score.
Every algorithm is a pure function — accepts { ups, downs, createdAt }
plus a { now, gravity? } context, returns a finite number. No I/O,
no global Date.now() reads, no provider wiring. Suitable for use
inside DataStore-driven sort handlers, cron rollup workers, or
client-side previews.
Quick Start
import { hnScore, rankScore } from '@molecule/api-rank-score'
const item = { ups: 42, downs: 3, createdAt: '2026-04-30T12:00:00Z' }
const ctx = { now: new Date() }
const directScore = hnScore(item, ctx)
const dispatched = rankScore('reddit-hot', item, ctx)Type
utility
Installation
npm install @molecule/api-rank-scoreAPI
Interfaces
RankContext
Common context for any ranking algorithm. now is injected so the
functions stay pure (no Date.now() reads inside).
interface RankContext {
/** Reference "now" for time-decay calculations. */
now: Date | string | number
/**
* Decay exponent for HN-style and recency algorithms. Higher = faster
* decay. Default `1.8` (HN's classic value).
*/
gravity?: number
}RankItem
Vote tally + creation timestamp for an item being ranked.
ups and downs are independent counts — Reddit-style. The score
(ups - downs) is computed by the algorithms that need it.
interface RankItem {
/** Number of upvotes / positive signals. Must be ≥ 0. */
ups: number
/** Number of downvotes / negative signals. Must be ≥ 0. */
downs: number
/**
* When the item was created. Accepts `Date`, ISO-8601 string, or epoch ms
* — normalised internally.
*/
createdAt: Date | string | number
}Types
RankAlgorithm
Identifier for the supported ranking algorithms.
type RankAlgorithm =
'hn' | 'reddit-hot' | 'reddit-best' | 'reddit-controversial' | 'recency' | 'score'Functions
hnScore(item, ctx)
Hacker-News-style decay rank.
Formula: (P - 1) / (T + 2)^G where P = ups - downs (clamped at 0),
T is item age in hours, and G is gravity (default 1.8). Newer items
with fewer points can outrank older items with many.
function hnScore(item: RankItem, ctx: RankContext): numberitem— Item being ranked.ctx— Reference time and optional gravity override.
Returns: Numeric score; higher = better. Always finite.
hoursBetween(then, now)
Hours elapsed between two timestamps. Result may be negative if then
is in the future relative to now.
function hoursBetween(then: number, now: number): numberthen— The earlier (item creation) timestamp, ms.now— The reference timestamp, ms.
Returns: Elapsed hours as a float.
pureScore(item)
Pure-score rank — ups - downs, no time decay.
function pureScore(item: RankItem): numberitem— Item being ranked.
Returns: Net score (may be negative).
rankScore(algorithm, item, ctx)
Compute a rank score using the named algorithm.
Convenience dispatcher — handlers/cron workers usually call the specific algorithm directly, but this is handy when the algorithm choice is configurable per project.
function rankScore(algorithm: RankAlgorithm, item: RankItem, ctx: RankContext): numberalgorithm— Algorithm identifier.item— Item being ranked.ctx— Reference time + optional gravity.
Returns: Numeric score; semantics depend on the chosen algorithm.
recencyScore(item, ctx)
Pure-recency rank — newer = higher. Uses gravity to decay.
Formula: 1 / (T + 2)^G where T is age in hours.
function recencyScore(item: RankItem, ctx: RankContext): numberitem— Item being ranked. Vote counts are unused.ctx— Reference time and optional gravity (default1.8).
Returns: Score in (0, 1]; higher = newer.
redditBestScore(item)
Reddit's "best" ranking — Wilson score lower-bound of the 95% confidence interval for the proportion of upvotes. Time-independent.
Edge cases:
ups + downs === 0→ returns0.- All upvotes (downs=0) → bounded above by
<1, but still grows with more votes (more confidence). - Tied votes → returns the lower bound of the 50% proportion at that sample size.
function redditBestScore(item: RankItem): numberitem— Item being ranked.createdAtis unused.
Returns: Score in [0, 1). Higher = better.
redditControversialScore(item)
Reddit's "controversial" ranking — favours items with high engagement AND a near-50/50 up/down ratio.
Formula: (ups + downs) * (min(ups, downs) / max(ups, downs)).
Items with very lopsided ratios collapse toward 0; perfectly-tied items with high vote totals score highest. Returns 0 if either side is 0 (no controversy without dissent).
function redditControversialScore(item: RankItem): numberitem— Item being ranked.createdAtis unused.
Returns: Numeric score ≥ 0; higher = more controversial.
redditHotScore(item, ctx)
Reddit's "hot" ranking — log10(|score|) + sign(score) * t / 45000,
where t is the item's age in seconds relative to a fixed epoch
(Reddit uses 2005-12-08T07:46:43Z). We approximate by using the
provided ctx.now as the epoch — the relative ordering between
items at the same now is what matters, which matches Reddit's
intent (newer items get a small bonus).
Symmetric: heavily-downvoted items receive an inverted ranking equal in magnitude to their upvoted mirror — useful for "show controversial at the bottom" rather than just hiding them.
function redditHotScore(item: RankItem, ctx: RankContext): numberitem— Item being ranked.ctx— Reference time (unused gravity).
Returns: Numeric score; higher = better.
sign(n)
Sign of n — -1, 0, or 1. Math.sign returns -0/0 ambiguously
across engines for some inputs; this helper coerces to 0.
function sign(n: number): 0 | 1 | -1n— Any finite number.
Returns: -1, 0, or 1.
toEpochMs(value)
Normalises a Date | string | number to epoch milliseconds.
function toEpochMs(value: string | number | Date): numbervalue— Date instance, ISO-8601 string, or epoch ms number.
Returns: Epoch milliseconds.
Injection Notes
Scoring is O(1) per item but sorting a large feed by a time-decay score is
O(n log n) per request and the scores change continuously. For
high-cardinality feeds, precompute scores on a schedule (cron/queue worker
writing a score column, then ORDER BY it) instead of ranking every row at
request time. hnScore, redditHotScore, and recencyScore read
ctx.now — pass the same now for every item in one ranking pass so
concurrent requests produce a stable order. redditBestScore and
redditControversialScore ignore time entirely.
