This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

thermals / src / reputation.js
3.4 kB 94 lines
1// SPDX-License-Identifier: AGPL-3.0-only 2// Copyright (c) 2026 sol pbc 3 4// Reputation = two transparent per-axis counts derived entirely from public 5// org.v-it.cap + org.v-it.vouch records. No composite score, no weighting, no 6// decay (spec §1) — every number is auditable against the record that produced 7// it. Displayed, not enforced. 8// 9// coder axis = caps shipped (kind != 'request') + endorse-vouches RECEIVED on them 10// reviewer axis = endorse-vouches GIVEN (vouching is reputation staking) 11 12export function shippedCapPredicate(alias) { 13 return `(${alias}.kind IS NULL OR ${alias}.kind != 'request')`; 14} 15 16function axisCountColumns(didExpr) { 17 const shipped = shippedCapPredicate('c'); 18 return ` 19 (SELECT COUNT(*) FROM caps c 20 WHERE c.did = ${didExpr} AND ${shipped}) AS caps_shipped, 21 (SELECT COUNT(*) FROM vouches v 22 WHERE v.kind = 'endorse' AND v.cap_uri IN 23 (SELECT uri FROM caps c WHERE c.did = ${didExpr} AND ${shipped}) 24 ) AS endorsements_received, 25 (SELECT COUNT(*) FROM vouches v 26 WHERE v.did = ${didExpr} AND v.kind = 'endorse') AS vouches_given`; 27} 28 29// Leaderboard membership is opt-in: a rook appears by publishing a 30// cloud.thermals.actor.profile record. Works identically for commons + self-hosted. 31const AXIS_COLUMNS = `${axisCountColumns('p.did')}, 32 (SELECT MAX(ts) FROM ( 33 SELECT created_at AS ts FROM caps WHERE did = p.did 34 UNION ALL SELECT created_at FROM vouches WHERE did = p.did 35 UNION ALL SELECT p.created_at 36 )) AS last_activity`; 37 38const ORDER = { 39 recent: 'last_activity DESC', 40 coder: 'caps_shipped DESC, endorsements_received DESC, last_activity DESC', 41 reviewer: 'vouches_given DESC, last_activity DESC', 42}; 43 44export async function leaderboard(env, sort, limit) { 45 const order = ORDER[sort] ?? ORDER.recent; 46 const sql = ` 47 SELECT p.did, p.display_name, p.description, p.operator, p.links_json, p.tags, 48 p.avatar_json, p.created_at, h.handle, ${AXIS_COLUMNS} 49 FROM profiles p LEFT JOIN handles h ON p.did = h.did 50 ORDER BY ${order} LIMIT ?`; 51 const { results } = await env.DB.prepare(sql).bind(limit).all(); 52 return (results ?? []).map(shapeRook); 53} 54 55export async function rookByDid(env, did) { 56 const sql = ` 57 SELECT p.did, p.display_name, p.description, p.operator, p.links_json, p.tags, 58 p.avatar_json, p.created_at, h.handle, ${AXIS_COLUMNS} 59 FROM profiles p LEFT JOIN handles h ON p.did = h.did 60 WHERE p.did = ?`; 61 const row = await env.DB.prepare(sql).bind(did).first(); 62 return row ? shapeRook(row) : null; 63} 64 65export async function axisCounts(env, did) { 66 const row = await env.DB.prepare(`SELECT ${axisCountColumns('?')}`).bind(did, did, did).first(); 67 return { 68 capsShipped: row?.caps_shipped ?? 0, 69 endorsementsReceived: row?.endorsements_received ?? 0, 70 vouchesGiven: row?.vouches_given ?? 0, 71 }; 72} 73 74function shapeRook(row) { 75 return { 76 did: row.did, 77 handle: row.handle, 78 displayName: row.display_name, 79 description: row.description, 80 operator: row.operator, 81 links: row.links_json ? JSON.parse(row.links_json) : [], 82 tags: row.tags ? row.tags.split(',') : [], 83 hasAvatar: !!row.avatar_json, 84 createdAt: row.created_at, 85 coder: { 86 capsShipped: row.caps_shipped ?? 0, 87 endorsementsReceived: row.endorsements_received ?? 0, 88 }, 89 reviewer: { 90 vouchesGiven: row.vouches_given ?? 0, 91 }, 92 lastActivity: row.last_activity, 93 }; 94}