Barazo AppView backend barazo.forum
at main 303 lines 9.0 kB view raw
1import { eq, and, sql, or, inArray } from 'drizzle-orm' 2import type { Database } from '../db/index.js' 3import type { Logger } from '../lib/logger.js' 4import { interactionGraph } from '../db/schema/interaction-graph.js' 5import { trustSeeds } from '../db/schema/trust-seeds.js' 6import { trustScores } from '../db/schema/trust-scores.js' 7import { users } from '../db/schema/users.js' 8 9// --------------------------------------------------------------------------- 10// Types 11// --------------------------------------------------------------------------- 12 13export interface TrustComputationResult { 14 totalNodes: number 15 totalEdges: number 16 iterations: number 17 converged: boolean 18 durationMs: number 19} 20 21export interface TrustGraphService { 22 computeTrustScores(communityId: string | null): Promise<TrustComputationResult> 23 getTrustScore(did: string, communityId: string | null): Promise<number> 24} 25 26// --------------------------------------------------------------------------- 27// Pure EigenTrust implementation (exported for simulation tests) 28// --------------------------------------------------------------------------- 29 30type Edge = { target: string; weight: number } 31 32interface EigenTrustResult { 33 scores: Map<string, number> 34 iterations: number 35 converged: boolean 36} 37 38/** 39 * Run the EigenTrust algorithm on an in-memory graph. 40 * 41 * @param edges - Adjacency list: source DID -> list of {target, weight} 42 * @param seedDids - Set of seed DIDs (initial trust = 1.0) 43 * @param maxIterations - Maximum number of iterations 44 * @param convergenceThreshold - Stop when max change < this value 45 * @returns Trust scores map and convergence metadata 46 */ 47export function runEigenTrust( 48 edges: Map<string, Edge[]>, 49 seedDids: Set<string>, 50 maxIterations: number, 51 convergenceThreshold: number 52): Map<string, number> 53export function runEigenTrust( 54 edges: Map<string, Edge[]>, 55 seedDids: Set<string>, 56 maxIterations: number, 57 convergenceThreshold: number, 58 returnMetadata: true 59): EigenTrustResult 60export function runEigenTrust( 61 edges: Map<string, Edge[]>, 62 seedDids: Set<string>, 63 maxIterations: number, 64 convergenceThreshold: number, 65 returnMetadata?: boolean 66): Map<string, number> | EigenTrustResult { 67 // Collect all nodes 68 const allNodes = new Set<string>() 69 for (const [source, targets] of edges) { 70 allNodes.add(source) 71 for (const { target } of targets) { 72 allNodes.add(target) 73 } 74 } 75 76 if (allNodes.size === 0) { 77 const empty = new Map<string, number>() 78 if (returnMetadata) { 79 return { scores: empty, iterations: 0, converged: true } 80 } 81 return empty 82 } 83 84 // Initialize trust: seeds = 1.0, others = 0.0 85 const trust = new Map<string, number>() 86 const seedTrust = new Map<string, number>() 87 for (const node of allNodes) { 88 const isSeed = seedDids.has(node) 89 trust.set(node, isSeed ? 1.0 : 0.0) 90 seedTrust.set(node, isSeed ? 1.0 : 0.0) 91 } 92 93 // If no seeds, all trust remains at 0 94 if (seedDids.size === 0) { 95 if (returnMetadata) { 96 return { scores: trust, iterations: 0, converged: true } 97 } 98 return trust 99 } 100 101 // Compute total outgoing weight per node 102 const totalOutgoing = new Map<string, number>() 103 for (const [source, targets] of edges) { 104 let total = 0 105 for (const { weight } of targets) { 106 total += weight 107 } 108 totalOutgoing.set(source, total) 109 } 110 111 // Build incoming edges: target -> [{source, weight}] 112 const incoming = new Map<string, { source: string; weight: number }[]>() 113 for (const [source, targets] of edges) { 114 for (const { target, weight } of targets) { 115 const existing = incoming.get(target) 116 if (existing) { 117 existing.push({ source, weight }) 118 } else { 119 incoming.set(target, [{ source, weight }]) 120 } 121 } 122 } 123 124 // Iterate with double-buffering: read from previous iteration, write to new map 125 let iterations = 0 126 let converged = false 127 128 for (let iter = 0; iter < maxIterations; iter++) { 129 iterations = iter + 1 130 let maxChange = 0 131 const nextTrust = new Map<string, number>() 132 133 for (const node of allNodes) { 134 const seed = seedTrust.get(node) ?? 0 135 let incomingTrust = 0 136 137 const inEdges = incoming.get(node) 138 if (inEdges) { 139 for (const { source, weight } of inEdges) { 140 const sourceTrust = trust.get(source) ?? 0 141 const sourceOutgoing = totalOutgoing.get(source) ?? 1 142 incomingTrust += sourceTrust * (weight / sourceOutgoing) 143 } 144 } 145 146 const newTrust = 0.5 * seed + 0.5 * incomingTrust 147 const oldTrust = trust.get(node) ?? 0 148 const change = Math.abs(newTrust - oldTrust) 149 if (change > maxChange) maxChange = change 150 151 nextTrust.set(node, newTrust) 152 } 153 154 // Swap: copy nextTrust into trust for next iteration 155 for (const [node, score] of nextTrust) { 156 trust.set(node, score) 157 } 158 159 if (maxChange < convergenceThreshold) { 160 converged = true 161 break 162 } 163 } 164 165 if (returnMetadata) { 166 return { scores: trust, iterations, converged } 167 } 168 return trust 169} 170 171// --------------------------------------------------------------------------- 172// Factory 173// --------------------------------------------------------------------------- 174 175const DEFAULT_TRUST_SCORE = 0.1 176const MAX_ITERATIONS = 20 177const CONVERGENCE_THRESHOLD = 0.001 178 179export function createTrustGraphService(db: Database, logger: Logger): TrustGraphService { 180 async function computeTrustScores(communityId: string | null): Promise<TrustComputationResult> { 181 const start = Date.now() 182 183 // 1. Load interaction graph edges 184 const communityFilter = communityId ? eq(interactionGraph.communityId, communityId) : sql`true` 185 186 const edgeRows = await db 187 .select({ 188 source_did: interactionGraph.sourceDid, 189 target_did: interactionGraph.targetDid, 190 weight: interactionGraph.weight, 191 }) 192 .from(interactionGraph) 193 .where(communityFilter) 194 195 if (edgeRows.length === 0) { 196 logger.info({ communityId }, 'No edges found, skipping trust computation') 197 return { 198 totalNodes: 0, 199 totalEdges: 0, 200 iterations: 0, 201 converged: true, 202 durationMs: Date.now() - start, 203 } 204 } 205 206 // Build adjacency list 207 const edges = new Map<string, Edge[]>() 208 const allNodes = new Set<string>() 209 210 for (const row of edgeRows) { 211 allNodes.add(row.source_did) 212 allNodes.add(row.target_did) 213 const existing = edges.get(row.source_did) 214 if (existing) { 215 existing.push({ target: row.target_did, weight: row.weight }) 216 } else { 217 edges.set(row.source_did, [{ target: row.target_did, weight: row.weight }]) 218 } 219 } 220 221 // 2. Get trust seeds (empty string = global scope) 222 const seedFilter = communityId 223 ? or(eq(trustSeeds.communityId, communityId), eq(trustSeeds.communityId, '')) 224 : eq(trustSeeds.communityId, '') 225 226 const seedRows = await db.select({ did: trustSeeds.did }).from(trustSeeds).where(seedFilter) 227 228 // Also include admins/moderators as seeds 229 const adminRows = await db 230 .select({ did: users.did }) 231 .from(users) 232 .where(inArray(users.role, ['admin', 'moderator'])) 233 234 const seedDids = new Set<string>() 235 for (const row of seedRows) { 236 seedDids.add(row.did) 237 } 238 for (const row of adminRows) { 239 seedDids.add(row.did) 240 } 241 242 // 3. Run EigenTrust 243 const result = runEigenTrust(edges, seedDids, MAX_ITERATIONS, CONVERGENCE_THRESHOLD, true) 244 245 // 4. Upsert results to trust_scores (empty string = global scope) 246 const effectiveCommunityId = communityId ?? '' 247 for (const [did, score] of result.scores) { 248 await db 249 .insert(trustScores) 250 .values({ 251 did, 252 communityId: effectiveCommunityId, 253 score, 254 computedAt: new Date(), 255 }) 256 .onConflictDoUpdate({ 257 target: [trustScores.did, trustScores.communityId], 258 set: { 259 score, 260 computedAt: new Date(), 261 }, 262 }) 263 } 264 265 const durationMs = Date.now() - start 266 267 logger.info( 268 { 269 communityId, 270 totalNodes: allNodes.size, 271 totalEdges: edgeRows.length, 272 iterations: result.iterations, 273 converged: result.converged, 274 durationMs, 275 }, 276 'Trust computation completed' 277 ) 278 279 return { 280 totalNodes: allNodes.size, 281 totalEdges: edgeRows.length, 282 iterations: result.iterations, 283 converged: result.converged, 284 durationMs, 285 } 286 } 287 288 async function getTrustScore(did: string, communityId: string | null): Promise<number> { 289 const effectiveCommunityId = communityId ?? '' 290 const filter = and(eq(trustScores.did, did), eq(trustScores.communityId, effectiveCommunityId)) 291 292 const rows = await db.select({ score: trustScores.score }).from(trustScores).where(filter) 293 294 const row = rows[0] 295 if (!row) { 296 return DEFAULT_TRUST_SCORE 297 } 298 299 return row.score 300 } 301 302 return { computeTrustScores, getTrustScore } 303}