rspace-online/modules/rcred/mod.ts

133 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 20182022) — 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: `<folk-cred-dashboard space="${space || 'demo'}"></folk-cred-dashboard>`,
scripts: '<script type="module" src="/modules/rcred/folk-cred-dashboard.js?v=1"></script>',
}),
);
});
// ── Cron: recompute every 6 hours ──
const CRON_INTERVAL = 6 * 60 * 60 * 1000; // 6h
let cronTimer: ReturnType<typeof setInterval> | 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<CredConfigDoc>(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',
},
],
};