@bunkarium/algorithm is a pure, stateless TypeScript package that implements culture-focused ranking algorithms. It processes snapshots of aggregated data and returns ranked recommendations without any database access or external I/O.
All exported APIs are deterministic functions of their inputs. Same input always produces same output.
// Pure function example
const result1 = rank(request)
const result2 = rank(request)
// result1 === result2 (always)The algorithm package does NOT:
- Store any persistent state
- Read from databases
- Make network requests
- Access file systems
All state is passed in via function parameters.
The algorithm works with aggregated snapshots provided by the application:
weightedLikeSum(not individual like records)qualifiedUniqueViewers(not raw view events)clusterWeights(not user-cluster memberships)
This separation allows the application to:
- Use any database (SQL, NoSQL, graph, etc.)
- Implement fraud detection independently
- Cache aggregates efficiently
Core functions can be used independently:
// Use just like decay
const likeWeight = calculateLikeWeight({ likeWindowCount: 10 })
// Use just public metrics
const metrics = calculatePublicMetrics(input, params)
// Use just diversity reranking
const diverse = mmrRerank(candidates, { lambda: 0.7 })
// Or use the full pipeline
const result = rank(request)Every request includes contractVersion to ensure compatibility:
const request: RankRequest = {
contractVersion: '1.0',
clusterVersion: 'v1.0',
// ...
}┌─────────────────────────────────────────────────┐
│ Application Layer │
│ (Database, Fraud Detection, Aggregation) │
└─────────────────┬───────────────────────────────┘
│
│ RankRequest (snapshot)
▼
┌─────────────────────────────────────────────────┐
│ @bunkarium/algorithm (Stateless) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌───────────┐ │
│ │ Like Decay │ │ CR/CP/CVV │ │ Metrics │ │
│ └────────────┘ └────────────┘ └───────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌───────────┐ │
│ │ Scoring │ │ Ranking │ │ Explain │ │
│ │ PRS/CVS │ │ │ │ Codes │ │
│ └────────────┘ └────────────┘ └───────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ Offline Evaluation │ │
│ │ (Gini, Coverage, Diversity Metrics) │ │
│ └─────────────────────────────────────────┘ │
└─────────────────┬───────────────────────────────┘
│
│ RankResponse (ranked items)
▼
┌─────────────────────────────────────────────────┐
│ Application Layer │
│ (Logging, Personalization, Rendering) │
└─────────────────────────────────────────────────┘
{
contractVersion: '1.0',
clusterVersion: 'v1.0',
requestId: 'uuid',
requestSeed: 'seed-for-determinism',
userState: {
userKey: 'user123',
likeWindowCount: 15, // From DB aggregation
recentLikeCount30s: 2, // From DB aggregation
recentClusterExposures: { // From exposure log
'cluster-a': 5,
'cluster-b': 2
},
curatorReputation: 1.5, // CR score
cpEarned90d: 120 // CP total
},
candidates: [
{
itemKey: 'post-1',
type: 'post',
clusterId: 'cluster-a',
createdAt: 1234567890000,
qualityFlags: {
moderated: true,
spamSuspect: false
},
features: {
cvsComponents: {
like: 0.8,
context: 0.3,
collection: 0.1,
bridge: 0.05,
sustain: 0.2
},
qualifiedUniqueViewers: 150,
publicMetricsHint: { ... },
prsSource: 'following'
}
},
// ...more candidates
],
context: {
surface: 'home_mix',
nowTs: Date.now()
},
params: {
weights: { prs: 0.70, cvs: 0.30 }
}
}Candidates (unranked)
│
├─> Quality Filtering (moderation requirements)
│
├─> Primary Scoring
│ ├─> PRS (Personal Relevance Score)
│ └─> CVS (Cultural Value Score)
│
├─> Mixed Score = PRS * wprs + CVS * wcvs
│
├─> Penalty Application (spam, low quality)
│
└─> Primary Ranking (by finalScore)
Final Ranked List
│
└─> Reason Code Generation
{
ranked: [
{
itemKey: 'post-42',
finalScore: 0.876543210,
reasonCodes: ['HIGH_CVS', 'BRIDGE_SIGNAL', 'NEW_CLUSTER'],
scoreBreakdown: {
prs: 0.6,
cvs: 0.9,
penalty: 1.0
}
},
// ...more items
],
metadata: {
requestId: 'uuid',
contractVersion: '2.0',
paramSetId: 'sha256-hash-of-params',
totalCandidates: 150,
totalRanked: 20
}
}- Input: User's recent like count
- Output: Weight multiplier (0.0-1.0)
- Purpose: Prevent spam/farming while keeping likes always pressable
- Input: Historical curation outcomes
- Output: CR score (0.1-10.0) and multiplier (0.5-2.0)
- Purpose: Reward quality curation, not just volume
- Input: Community contribution events
- Output: CP issuance amounts, stake outcomes
- Purpose: Non-transferable reputation currency
- Input: Viewer's CR, CP, view uniqueness
- Output: View weight
- Purpose: Value attention from engaged curators higher
- Input: Aggregated likes, views, clusters
- Output: Density, rate, breadth, persistence
- Purpose: Show cultural impact, not raw popularity
- PRS: Personal relevance (following, saved, liked sources)
- CVS: Cultural value (like/context/collection/bridge/sustain signals)
- Input: Candidate features and scores
- Output: Human-readable reason codes
- Purpose: Transparency ("Why am I seeing this?")
All random operations use Xorshift64 PRNG seeded with requestSeed:
const rng = new Xorshift64(hashSeed(requestSeed))
const explorationIndices = sampleExplorationSlots(rng, count, budget)Same requestSeed → same random choices → same output
All effective parameters are hashed to create an audit trail:
const paramSetId = sha256Hash(JSON.stringify({
contractVersion,
clusterVersion,
params,
weights,
// ...all config
}))This allows:
- A/B test analysis (compare paramSetId groups)
- Debugging (reproduce exact conditions)
- Compliance (prove parameters at request time)
All divisions include priors or clamping:
// Support density with prior
const supportDensity = (weightedLikeSum + priorLikes) /
(qualifiedUniqueViewers + priorViews)
// Breadth (effective cluster count)
const breadth = totalWeight > 0 ? sumSquares / totalWeight : 0finalScore: 9 decimal places- Public metrics: 6 decimal places
- All intermediate calculations use
Math.froundfor consistency
- Empty candidate array → empty ranked array
- No qualified viewers → prior-based metrics
- No cluster exposures → all clusters considered novel
- Primary scoring: O(n) where n = candidate count
- Full pipeline: O(n) typically
- O(n) for candidate storage
- O(1) for metrics calculation (streaming)
- Cache parameter hashes if params don't change often
- Batch requests when possible (same user state)
Extend determineReasonCodes:
const customReasons = (candidate: Candidate, ...args) => {
const baseCodes = determineReasonCodes(candidate, ...args)
// Add custom logic
if (candidate.features.customFlag) {
baseCodes.push('CUSTOM_REASON')
}
return baseCodes
}Each primitive function has isolated tests:
- Like decay edge cases
- Metric calculations
- Scoring formulas
- Diversity algorithms
Full pipeline tests with realistic data:
- End-to-end ranking
- Determinism verification
- Edge case handling
Verification against spec test vectors:
- Known inputs → expected outputs
- Numerical precision checks
- Contract validation
When contract changes:
- Increment
CONTRACT_VERSION - Add new optional fields (backward compatible)
- Deprecate old fields (support for N versions)
- Document breaking changes
ALGORITHM_VERSION changes when:
- Formulas change
- Default parameters change
- Behavior changes
Applications can pin to specific versions:
{
"dependencies": {
"@bunkarium/algorithm": "1.0.0"
}
}- Formulas - Detailed mathematical formulas
- Usage Guide - Examples and best practices
- Parameters - Tuning guide
- Main Spec - Normative specification