Sprint 1 : scaffolding complet de Glibredecision

Plateforme de decisions collectives pour Duniter/G1.
Backend FastAPI async + PostgreSQL (14 tables, 8 routers, 6 services,
moteur de vote avec formule d'inertie WoT/Smith/TechComm).
Frontend Nuxt 4 + Nuxt UI v3 + Pinia (9 pages, 5 stores).
Infrastructure Docker + Woodpecker CI + Traefik.
Documentation technique et utilisateur (15 fichiers).
Seed : Licence G1, Engagement Forgeron v2.0.0, 4 protocoles de vote.
30 tests unitaires (formules, mode params, vote nuance) -- tous verts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-02-28 12:46:11 +01:00
commit 25437f24e3
100 changed files with 10236 additions and 0 deletions

View File

@@ -0,0 +1,164 @@
/**
* TypeScript mirror of the Python mode_params parser.
*
* A mode-params string encodes voting formula parameters in a compact format.
* Example: "D30M50B.1G.2T.1"
*
* Supported codes:
* D = duration_days (int)
* M = majority_pct (int, 0-100)
* B = base_exponent (float)
* G = gradient_exponent (float)
* C = constant_base (float)
* S = smith_exponent (float)
* T = techcomm_exponent (float)
* N = ratio_multiplier (float)
* R = is_ratio_mode (bool, 0 or 1)
*
* Values may start with a dot for decimals < 1, e.g. "B.1" means base_exponent=0.1.
*/
export interface ModeParams {
duration_days: number
majority_pct: number
base_exponent: number
gradient_exponent: number
constant_base: number
smith_exponent: number | null
techcomm_exponent: number | null
ratio_multiplier: number | null
is_ratio_mode: boolean
}
type CodeType = 'int' | 'float' | 'bool'
const CODES: Record<string, { key: keyof ModeParams; type: CodeType }> = {
D: { key: 'duration_days', type: 'int' },
M: { key: 'majority_pct', type: 'int' },
B: { key: 'base_exponent', type: 'float' },
G: { key: 'gradient_exponent', type: 'float' },
C: { key: 'constant_base', type: 'float' },
S: { key: 'smith_exponent', type: 'float' },
T: { key: 'techcomm_exponent', type: 'float' },
N: { key: 'ratio_multiplier', type: 'float' },
R: { key: 'is_ratio_mode', type: 'bool' },
}
const PARAM_RE = /([A-Z])(\d*\.?\d+)/g
function getDefaults(): ModeParams {
return {
duration_days: 30,
majority_pct: 50,
base_exponent: 0.1,
gradient_exponent: 0.2,
constant_base: 0.0,
smith_exponent: null,
techcomm_exponent: null,
ratio_multiplier: null,
is_ratio_mode: false,
}
}
/**
* Parse a mode-params string into a structured ModeParams object.
*
* @param paramsStr - Compact parameter string, e.g. "D30M50B.1G.2T.1"
* @returns Parsed parameters with defaults for codes not found
* @throws Error if an unrecognised code letter is found
*/
export function parseModeParams(paramsStr: string): ModeParams {
const result = getDefaults()
if (!paramsStr || !paramsStr.trim()) {
return result
}
let match: RegExpExecArray | null
PARAM_RE.lastIndex = 0
while ((match = PARAM_RE.exec(paramsStr)) !== null) {
const code = match[1]
const rawValue = match[2]
if (!(code in CODES)) {
throw new Error(`Code de parametre inconnu : '${code}'`)
}
const { key, type } = CODES[code]
if (type === 'int') {
;(result as any)[key] = Math.floor(parseFloat(rawValue))
} else if (type === 'float') {
;(result as any)[key] = parseFloat(rawValue)
} else if (type === 'bool') {
;(result as any)[key] = parseFloat(rawValue) !== 0
}
}
return result
}
/**
* Encode a ModeParams object into a compact mode-params string.
*
* Only includes parameters that differ from defaults.
*
* @param params - Parameters to encode
* @returns Compact string, e.g. "D30M50B.1G.2"
*/
export function encodeModeParams(params: Partial<ModeParams>): string {
const defaults = getDefaults()
const parts: string[] = []
const codeEntries = Object.entries(CODES) as [string, { key: keyof ModeParams; type: CodeType }][]
for (const [code, { key, type }] of codeEntries) {
const value = params[key]
if (value === undefined || value === null) continue
if (value === defaults[key]) continue
if (type === 'int') {
parts.push(`${code}${value}`)
} else if (type === 'float') {
const numVal = value as number
if (numVal < 1 && numVal > 0) {
parts.push(`${code}${numVal.toString().replace(/^0/, '')}`)
} else {
parts.push(`${code}${numVal}`)
}
} else if (type === 'bool') {
parts.push(`${code}${value ? 1 : 0}`)
}
}
return parts.join('')
}
/**
* Format a mode-params string for human display.
*
* @param paramsStr - Compact parameter string
* @returns Human-readable description in French
*/
export function formatModeParams(paramsStr: string): string {
const params = parseModeParams(paramsStr)
const parts: string[] = []
parts.push(`Duree: ${params.duration_days} jours`)
parts.push(`Majorite: ${params.majority_pct}%`)
parts.push(`Base: ${params.base_exponent}`)
parts.push(`Gradient: ${params.gradient_exponent}`)
if (params.constant_base > 0) {
parts.push(`Constante: ${params.constant_base}`)
}
if (params.smith_exponent !== null) {
parts.push(`Smith: ${params.smith_exponent}`)
}
if (params.techcomm_exponent !== null) {
parts.push(`TechComm: ${params.techcomm_exponent}`)
}
return parts.join(' | ')
}