Barazo AppView backend
barazo.forum
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}