rspace-online/modules/rvote/schemas.ts

137 lines
2.9 KiB
TypeScript

/**
* rVote Automerge document schemas.
*
* Granularity: one Automerge document per proposal.
* DocId format: {space}:vote:proposals:{proposalId}
*
* Vote tallying uses the Intent/Claim pattern:
* clients submit vote intents, server validates and writes claims.
*/
import type { DocSchema } from '../../shared/local-first/document';
// ── Document types ──
export interface VoteItem {
id: string;
userId: string;
proposalId: string;
weight: number;
creditCost: number;
createdAt: number;
decaysAt: number;
}
export interface FinalVoteItem {
id: string;
userId: string;
proposalId: string;
vote: 'YES' | 'NO' | 'ABSTAIN';
createdAt: number;
}
export interface SpaceConfig {
slug: string;
name: string;
description: string;
ownerDid: string;
visibility: string;
promotionThreshold: number | null;
votingPeriodDays: number | null;
creditsPerDay: number | null;
maxCredits: number | null;
startingCredits: number | null;
createdAt: number;
updatedAt: number;
}
export interface ProposalMeta {
id: string;
spaceSlug: string;
authorId: string;
title: string;
description: string;
status: string;
score: number;
votingEndsAt: number;
finalYes: number;
finalNo: number;
finalAbstain: number;
createdAt: number;
updatedAt: number;
}
export interface PairwiseData {
elo: number; // Current Elo rating (default 1500)
comparisons: number; // Total times shown in a pair
wins: number; // Times chosen as preferred
}
export interface ProposalDoc {
meta: {
module: string;
collection: string;
version: number;
spaceSlug: string;
createdAt: number;
};
spaceConfig: SpaceConfig | null;
proposal: ProposalMeta;
votes: Record<string, VoteItem>;
finalVotes: Record<string, FinalVoteItem>;
pairwise: PairwiseData;
}
// ── Elo rating helpers ──
export const ELO_K = 32;
export const ELO_DEFAULT = 1500;
export function computeElo(winnerElo: number, loserElo: number): { winner: number; loser: number } {
const expected = 1 / (1 + Math.pow(10, (loserElo - winnerElo) / 400));
const delta = Math.round(ELO_K * (1 - expected));
return { winner: winnerElo + delta, loser: loserElo - delta };
}
// ── Schema registration ──
export const proposalSchema: DocSchema<ProposalDoc> = {
module: 'vote',
collection: 'proposals',
version: 1,
init: (): ProposalDoc => ({
meta: {
module: 'vote',
collection: 'proposals',
version: 1,
spaceSlug: '',
createdAt: Date.now(),
},
spaceConfig: null,
proposal: {
id: '',
spaceSlug: '',
authorId: '',
title: '',
description: '',
status: 'RANKING',
score: 0,
votingEndsAt: 0,
finalYes: 0,
finalNo: 0,
finalAbstain: 0,
createdAt: Date.now(),
updatedAt: Date.now(),
},
votes: {},
finalVotes: {},
pairwise: { elo: ELO_DEFAULT, comparisons: 0, wins: 0 },
}),
};
// ── Helpers ──
export function proposalDocId(space: string, proposalId: string) {
return `${space}:vote:proposals:${proposalId}` as const;
}