/** * rCred module — Contribution Recognition via CredRank. * * Builds a per-space contribution graph from all module activity, * runs CredRank (modified PageRank on weighted contribution edges), * and distributes Grain tokens proportional to contribution. * * Inspired by SourceCred (RIP 2018–2022) — algorithm lives on. */ import { Hono } from 'hono'; import { renderShell } from '../../server/shell'; import { getModuleInfoList } from '../../shared/module'; import type { RSpaceModule } from '../../shared/module'; import type { SyncServer } from '../../server/local-first/sync-server'; import { graphSchema, scoresSchema, configSchema, configDocId, scoresDocId } from './schemas'; import type { CredConfigDoc } from './schemas'; import { createCredRoutes } from './routes'; import { recomputeSpace, ensureConfigDoc } from './grain-engine'; import { renderLanding } from './landing'; let _syncServer: SyncServer | null = null; const routes = new Hono(); // Mount API routes const apiRoutes = createCredRoutes(() => _syncServer); routes.route('/', apiRoutes); // ── Landing page ── routes.get('/', (c) => { const space = c.req.param('space'); return c.html( renderShell({ title: 'rCred — Contribution Recognition', spaceSlug: space || 'demo', moduleId: 'rcred', modules: getModuleInfoList(), body: ``, scripts: '', }), ); }); // ── Cron: recompute every 6 hours ── const CRON_INTERVAL = 6 * 60 * 60 * 1000; // 6h let cronTimer: ReturnType | null = null; function startCredCron() { if (cronTimer) return; cronTimer = setInterval(() => { if (!_syncServer) return; const allDocs = _syncServer.listDocs(); const configDocs = allDocs.filter(id => id.endsWith(':rcred:config')); for (const docId of configDocs) { const space = docId.split(':')[0]; if (!space) continue; const config = _syncServer.getDoc(docId); if (!config?.enabled) continue; // Check epoch timing const epochMs = (config.epochLengthDays || 7) * 86_400_000; if (Date.now() - (config.lastEpochAt || 0) < epochMs) continue; console.log(`[rCred cron] Recomputing ${space}...`); recomputeSpace(space, _syncServer); } }, CRON_INTERVAL); } export const credModule: RSpaceModule = { id: 'rcred', name: 'rCred', icon: '⭐', description: 'Contribution recognition — Cred scores + Grain distribution via CredRank', routes, scoping: { defaultScope: 'space', userConfigurable: false }, docSchemas: [graphSchema, scoresSchema, configSchema], onInit: async ({ syncServer }) => { _syncServer = syncServer; startCredCron(); console.log('[rCred] Module initialized, cron started (6h interval)'); // Auto-seed demo space on startup (delayed to let docs load) setTimeout(() => { if (!_syncServer) return; const scores = _syncServer.getDoc(scoresDocId('demo')); if (!scores) { console.log('[rCred] Seeding demo space scores...'); recomputeSpace('demo', _syncServer); } }, 10_000); }, feeds: [ { id: 'rcred-scores', name: 'Cred Scores', description: 'CredRank contribution scores per space member', kind: 'trust', }, ], landingPage: renderLanding, settingsSchema: [ { key: 'enabled', label: 'Enable CredRank', type: 'boolean', description: 'Enable automatic contribution scoring and grain distribution', default: true, }, { key: 'grainPerEpoch', label: 'Grain per Epoch', type: 'string', description: 'Amount of GRAIN tokens distributed each epoch (default: 1000)', default: '1000', }, { key: 'lookbackDays', label: 'Lookback Window (days)', type: 'string', description: 'How far back to look for contributions (default: 90)', default: '90', }, ], };