v3 : socle Rust (moteur pur, accès trustinWallet, serveur) ; maquette Nuxt rangée dans maquette/
- crates/ld-engine : formule inertielle (parité avec la maquette, garde C), coquille / changement de fond, assistant d'édition à règles, composition d'un document sous vote permanent (adoptions, aperçus, rivales). - crates/trustin-access : qui voit, qui ouvre, qui gère les accès. - crates/ld-server : API axum + SQLite, connexion Ğ1 (client OAuth2 de connect.monnaie-libre.fr), invitations, entrée en vigueur automatique, signalements de contradiction, traductions éditoriales, publication git, taille de la toile lue sur la chaîne. - seeds/acte-engagement-certification.json : 33 articles, fr + en + es ; en vigueur : préambule, E1, E2 (seed a minima). - maquette/ : frontend, backend et docker v2 déplacés tels quels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* WoT members threshold formula for binary votes.
|
||||
*
|
||||
* Exact TypeScript port of backend/app/engine/threshold.py,
|
||||
* smith_threshold.py and techcomm_threshold.py (Python is the
|
||||
* reference implementation — same guards, same ceil rounding).
|
||||
*
|
||||
* Core formula:
|
||||
* Result = C + B^W + (M + (1-M) * (1 - (T/W)^G)) * max(0, T - C)
|
||||
*
|
||||
* Where:
|
||||
* C = constantBase
|
||||
* B = baseExponent
|
||||
* W = wotSize (corpus of eligible voters)
|
||||
* T = totalVotes (for + against)
|
||||
* M = majorityRatio (majorityPct / 100)
|
||||
* G = gradientExponent
|
||||
*
|
||||
* Inertia behaviour:
|
||||
* - Low participation (T << W) -> near-unanimity required
|
||||
* - High participation (T -> W) -> simple majority M suffices
|
||||
*
|
||||
* Reference test case:
|
||||
* wotSize=7224, votesFor=97, votesAgainst=23 (total=120)
|
||||
* params M50 B.1 G.2 => threshold=94, adopted (97 >= 94)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Compute the minimum number of *for* votes required for adoption.
|
||||
*
|
||||
* A vote passes when `votesFor >= wotThreshold(...)`.
|
||||
*
|
||||
* @param wotSize - Size of the eligible voter corpus (WoT members)
|
||||
* @param totalVotes - Number of votes cast (for + against)
|
||||
* @param majorityPct - Majority percentage (0-100). 50 = simple majority at full participation
|
||||
* @param baseExponent - B in the formula; B^W is a vanishingly small offset when W is large
|
||||
* @param gradientExponent - G controls how fast the super-majority decays toward M
|
||||
* @param constantBase - C, a fixed additive floor on the threshold
|
||||
* @returns The ceiling of the computed threshold
|
||||
*/
|
||||
export function wotThreshold(
|
||||
wotSize: number,
|
||||
totalVotes: number,
|
||||
majorityPct: number = 50,
|
||||
baseExponent: number = 0.1,
|
||||
gradientExponent: number = 0.2,
|
||||
constantBase: number = 0.0,
|
||||
): number {
|
||||
if (wotSize <= 0) {
|
||||
throw new Error('wotSize doit etre strictement positif')
|
||||
}
|
||||
if (totalVotes < 0) {
|
||||
throw new Error('totalVotes ne peut pas etre negatif')
|
||||
}
|
||||
if (majorityPct < 0 || majorityPct > 100) {
|
||||
throw new Error('majorityPct doit etre entre 0 et 100')
|
||||
}
|
||||
|
||||
const C = constantBase
|
||||
const B = baseExponent
|
||||
const W = wotSize
|
||||
const T = totalVotes
|
||||
const M = majorityPct / 100.0
|
||||
const G = gradientExponent
|
||||
|
||||
// Guard: if no votes, threshold is at least ceil(C + B^W)
|
||||
if (T === 0) {
|
||||
return Math.ceil(C + B ** W)
|
||||
}
|
||||
|
||||
// Core formula
|
||||
const participationRatio = T / W
|
||||
const inertiaFactor = 1.0 - participationRatio ** G
|
||||
const requiredRatio = M + (1.0 - M) * inertiaFactor
|
||||
const result = C + B ** W + requiredRatio * Math.max(0.0, T - C)
|
||||
|
||||
return Math.ceil(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Smith sub-WoT threshold criterion.
|
||||
*
|
||||
* Formula: ceil(smithWotSize ^ S)
|
||||
*
|
||||
* @param smithWotSize - Number of active Smith members (forgerons)
|
||||
* @param exponent - S in the formula
|
||||
* @returns Minimum Smith votes required
|
||||
*/
|
||||
export function smithThreshold(smithWotSize: number, exponent: number = 0.1): number {
|
||||
if (smithWotSize <= 0) {
|
||||
throw new Error('smithWotSize doit etre strictement positif')
|
||||
}
|
||||
return Math.ceil(smithWotSize ** exponent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Technical Committee threshold criterion.
|
||||
*
|
||||
* Formula: ceil(cotecSize ^ T)
|
||||
*
|
||||
* @param cotecSize - Number of Technical Committee members
|
||||
* @param exponent - T in the formula
|
||||
* @returns Minimum TechComm votes required
|
||||
*/
|
||||
export function techcommThreshold(cotecSize: number, exponent: number = 0.1): number {
|
||||
if (cotecSize <= 0) {
|
||||
throw new Error('cotecSize doit etre strictement positif')
|
||||
}
|
||||
return Math.ceil(cotecSize ** exponent)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Election — simple plurality (BLUEPRINT-V2.md Δ28, « Modalités » #4)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
import type { FormulaParams, Id, Vote } from '~/types/domain'
|
||||
|
||||
/**
|
||||
* Outcome of an election tally.
|
||||
*
|
||||
* Discriminated union on `outcome`:
|
||||
* - 'elected' — a single person leads by simple plurality.
|
||||
* - 'tie' — several persons share the top count. The engine NEVER
|
||||
* breaks a tie (no randomness, no first-come): the closure
|
||||
* flow proposes a chained runoff among `exAequoIds`, or a
|
||||
* draw only if the Pact planned it (FormulaParams.tieBreak).
|
||||
* - 'rejected' — reason 'quorum': participants (blanks included) below
|
||||
* electionMinParticipants; `required` = that quorum.
|
||||
* reason 'no-designation': quorum reached (or absent) but
|
||||
* every vote is blank — nobody was designated, and an
|
||||
* empty tie would be meaningless; `required` still carries
|
||||
* the quorum (0 when none) for display purposes.
|
||||
*/
|
||||
export type ElectionOutcome =
|
||||
| {
|
||||
outcome: 'elected'
|
||||
winnerId: Id
|
||||
counts: Record<Id, number>
|
||||
blanks: number
|
||||
participants: number
|
||||
}
|
||||
| {
|
||||
outcome: 'tie'
|
||||
exAequoIds: Id[]
|
||||
counts: Record<Id, number>
|
||||
blanks: number
|
||||
participants: number
|
||||
}
|
||||
| {
|
||||
outcome: 'rejected'
|
||||
reason: 'quorum' | 'no-designation'
|
||||
participants: number
|
||||
required: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Tally an election by simple plurality.
|
||||
*
|
||||
* CONTRACT — the caller passes the LAST ACTIVE votes only: one vote per
|
||||
* voter, `supersedesVoteId` chains already resolved (the store filters
|
||||
* superseded votes). The engine does NOT deduplicate by voterId;
|
||||
* `participants` is simply `votes.length`.
|
||||
*
|
||||
* Rules (Δ28):
|
||||
* - A vote without `choicePersonId` is a BLANK: it counts for
|
||||
* participation (quorum), never for designation.
|
||||
* - Quorum: when `formula.electionMinParticipants` is set and
|
||||
* participants (blanks included) < quorum ⇒ rejected ('quorum').
|
||||
* - Designation by simple PLURALITY of the non-blank votes.
|
||||
* - Tie at the top ⇒ 'tie' with `exAequoIds` sorted (lexicographic —
|
||||
* a deterministic display order, NEVER a tie-break: the engine does
|
||||
* not pick a winner among equals, no randomness, no first-come).
|
||||
* - Zero designation (all blanks) ⇒ rejected ('no-designation').
|
||||
*
|
||||
* @param votes - Last active votes of the session (see contract above)
|
||||
* @param formula - Protocol formula params (only electionMinParticipants is read)
|
||||
* @returns The election outcome — never a tie silently broken
|
||||
*/
|
||||
export function electionResult(
|
||||
votes: Vote[],
|
||||
formula: Pick<FormulaParams, 'electionMinParticipants'>,
|
||||
): ElectionOutcome {
|
||||
const participants = votes.length
|
||||
const quorum = formula.electionMinParticipants ?? 0
|
||||
|
||||
if (formula.electionMinParticipants !== undefined && participants < formula.electionMinParticipants) {
|
||||
return { outcome: 'rejected', reason: 'quorum', participants, required: formula.electionMinParticipants }
|
||||
}
|
||||
|
||||
const counts: Record<Id, number> = {}
|
||||
let blanks = 0
|
||||
for (const vote of votes) {
|
||||
if (vote.choicePersonId) {
|
||||
counts[vote.choicePersonId] = (counts[vote.choicePersonId] ?? 0) + 1
|
||||
} else {
|
||||
blanks++ // blank: participation only, never designation
|
||||
}
|
||||
}
|
||||
|
||||
const designatedIds = Object.keys(counts)
|
||||
if (designatedIds.length === 0) {
|
||||
return { outcome: 'rejected', reason: 'no-designation', participants, required: quorum }
|
||||
}
|
||||
|
||||
const topCount = Math.max(...designatedIds.map(id => counts[id]!))
|
||||
const leaders = designatedIds.filter(id => counts[id] === topCount)
|
||||
|
||||
if (leaders.length === 1) {
|
||||
return { outcome: 'elected', winnerId: leaders[0]!, counts, blanks, participants }
|
||||
}
|
||||
|
||||
return { outcome: 'tie', exAequoIds: [...leaders].sort(), counts, blanks, participants }
|
||||
}
|
||||
Reference in New Issue
Block a user