133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
/**
|
||
* 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: `<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',
|
||
},
|
||
],
|
||
};
|