Sprint 3 : protocoles de vote et boite a outils
Backend: - Sessions de vote : list, close, tally, threshold details, auto-expiration - Protocoles : update, simulate, meta-gouvernance, formulas CRUD - Service vote enrichi : close_session, get_threshold_details, nuanced breakdown - Schemas : ThresholdDetailOut, VoteResultOut, FormulaSimulationRequest/Result - WebSocket broadcast sur chaque vote + fermeture session - 25 nouveaux tests (threshold details, close, nuanced, simulation) Frontend: - 5 composants vote : VoteBinary, VoteNuanced, ThresholdGauge, FormulaDisplay, VoteHistory - 3 composants protocoles : ProtocolPicker, FormulaEditor, ModeParamsDisplay - Simulateur de formules interactif (page /protocols/formulas) - Page detail protocole (/protocols/[id]) - Composable useWebSocket (live updates) - Composable useVoteFormula (calcul client-side reactif) - Integration KaTeX pour rendu LaTeX des formules Documentation: - API reference : 8 nouveaux endpoints documentes - Formules : tables d'inertie, parametres detailles, simulation API - Guide vote : vote binaire/nuance, jauge, historique, simulateur, meta-gouvernance 55 tests passes (+ 1 skipped), 126 fichiers total. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
98
frontend/app/composables/useVoteFormula.ts
Normal file
98
frontend/app/composables/useVoteFormula.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Composable for real-time vote formula computation.
|
||||
*
|
||||
* Re-exports and wraps the threshold utility functions for reactive use
|
||||
* in Vue components. Provides convenient methods for threshold calculations,
|
||||
* inertia factor, required ratio, and adoption checks.
|
||||
*/
|
||||
import { wotThreshold, smithThreshold, techcommThreshold } from '~/utils/threshold'
|
||||
|
||||
export interface FormulaParams {
|
||||
majority_pct: number
|
||||
base_exponent: number
|
||||
gradient_exponent: number
|
||||
constant_base: number
|
||||
}
|
||||
|
||||
export function useVoteFormula() {
|
||||
/**
|
||||
* Compute the WoT threshold for a given set of parameters.
|
||||
*/
|
||||
function computeThreshold(wotSize: number, totalVotes: number, params: FormulaParams): number {
|
||||
return wotThreshold(
|
||||
wotSize,
|
||||
totalVotes,
|
||||
params.majority_pct,
|
||||
params.base_exponent,
|
||||
params.gradient_exponent,
|
||||
params.constant_base,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the inertia factor: 1 - (T/W)^G
|
||||
* Ranges from ~1 (low participation) to ~0 (full participation).
|
||||
*/
|
||||
function computeInertiaFactor(
|
||||
totalVotes: number,
|
||||
wotSize: number,
|
||||
gradientExponent: number,
|
||||
): number {
|
||||
if (wotSize <= 0 || totalVotes <= 0) return 1.0
|
||||
const participationRatio = totalVotes / wotSize
|
||||
return 1.0 - Math.pow(participationRatio, gradientExponent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the required ratio of "pour" votes at a given participation level.
|
||||
* requiredRatio = M + (1 - M) * inertiaFactor
|
||||
*/
|
||||
function computeRequiredRatio(
|
||||
totalVotes: number,
|
||||
wotSize: number,
|
||||
majorityPct: number,
|
||||
gradientExponent: number,
|
||||
): number {
|
||||
const M = majorityPct / 100
|
||||
const inertia = computeInertiaFactor(totalVotes, wotSize, gradientExponent)
|
||||
return M + (1.0 - M) * inertia
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a vote is adopted given all criteria.
|
||||
* A vote is adopted when it passes the WoT threshold AND
|
||||
* any applicable Smith/TechComm criteria.
|
||||
*/
|
||||
function isAdopted(
|
||||
votesFor: number,
|
||||
threshold: number,
|
||||
smithVotesFor?: number,
|
||||
smithThresholdVal?: number,
|
||||
techcommVotesFor?: number,
|
||||
techcommThresholdVal?: number,
|
||||
): boolean {
|
||||
// Main WoT criterion
|
||||
if (votesFor < threshold) return false
|
||||
|
||||
// Smith criterion (if applicable)
|
||||
if (smithThresholdVal !== undefined && smithThresholdVal > 0) {
|
||||
if ((smithVotesFor ?? 0) < smithThresholdVal) return false
|
||||
}
|
||||
|
||||
// TechComm criterion (if applicable)
|
||||
if (techcommThresholdVal !== undefined && techcommThresholdVal > 0) {
|
||||
if ((techcommVotesFor ?? 0) < techcommThresholdVal) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
computeThreshold,
|
||||
computeInertiaFactor,
|
||||
computeRequiredRatio,
|
||||
isAdopted,
|
||||
smithThreshold,
|
||||
techcommThreshold,
|
||||
}
|
||||
}
|
||||
100
frontend/app/composables/useWebSocket.ts
Normal file
100
frontend/app/composables/useWebSocket.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Composable for WebSocket connectivity to receive live vote updates.
|
||||
*
|
||||
* Connects to the backend WS endpoint and allows subscribing to
|
||||
* individual vote session channels for real-time tally updates.
|
||||
*/
|
||||
export function useWebSocket() {
|
||||
const config = useRuntimeConfig()
|
||||
let ws: WebSocket | null = null
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const connected = ref(false)
|
||||
const lastMessage = ref<any>(null)
|
||||
|
||||
/**
|
||||
* Open a WebSocket connection to the backend live endpoint.
|
||||
*/
|
||||
function connect() {
|
||||
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
||||
return
|
||||
}
|
||||
|
||||
const wsUrl = config.public.apiBase
|
||||
.replace(/^http/, 'ws')
|
||||
.replace(/\/api\/v1$/, '/api/v1/ws/live')
|
||||
|
||||
ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
connected.value = true
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
connected.value = false
|
||||
reconnect()
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
connected.value = false
|
||||
}
|
||||
|
||||
ws.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
lastMessage.value = JSON.parse(event.data)
|
||||
} catch {
|
||||
lastMessage.value = event.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to real-time updates for a vote session.
|
||||
*/
|
||||
function subscribe(sessionId: string) {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ action: 'subscribe', session_id: sessionId }))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from a vote session's updates.
|
||||
*/
|
||||
function unsubscribe(sessionId: string) {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ action: 'unsubscribe', session_id: sessionId }))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully close the WebSocket connection.
|
||||
*/
|
||||
function disconnect() {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = null
|
||||
}
|
||||
if (ws) {
|
||||
ws.onclose = null
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
connected.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a reconnection attempt after a delay.
|
||||
*/
|
||||
function reconnect() {
|
||||
if (reconnectTimer) return
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null
|
||||
connect()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
return { connected, lastMessage, connect, subscribe, unsubscribe, disconnect }
|
||||
}
|
||||
Reference in New Issue
Block a user