v2 : conception ultracode + moteur de formules porté en TS
- Blueprint définitif (docs/dev/BLUEPRINT-V2.md + JSON) : panel 4 visions + 3 juges, corpus doctrinal (Mon nom est personne, Une économie du don, SejeteralO), 4 critiques d'intégration, 3 vérificateurs, réparations - Corpus d'intention + lectures brutes archivés - Moteur porté Python→TS pur : threshold (inertie WoT, Smith, ComTech), nuancé 6 niveaux, DSL modeParams — 45 tests vitest, parité croisée vérifiée (Forgeron W=7224 T=120 ⇒ 94) - Contrat de domaine v2 (types/domain.ts) + lexique UI (lexicon.ts) - vitest + idb-keyval en dépendances Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Mode-params DSL: parse and format compact parameter strings.
|
||||
*
|
||||
* Exact TypeScript port of backend/app/engine/mode_params.py
|
||||
* (Python is the reference implementation for parsing; the Python
|
||||
* module has no formatter, so formatModeParams() is defined as the
|
||||
* canonical inverse of parseModeParams()).
|
||||
*
|
||||
* 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'
|
||||
|
||||
// Ordered list of recognised codes and their target keys + types
|
||||
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' },
|
||||
}
|
||||
|
||||
// Regex: a single uppercase letter followed by a numeric value (int or float,
|
||||
// possibly starting with '.' for values like .1 meaning 0.1)
|
||||
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 parameter object.
|
||||
*
|
||||
* Defaults are applied for any code not present in the string.
|
||||
*
|
||||
* @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
|
||||
}
|
||||
|
||||
for (const match of paramsStr.matchAll(PARAM_RE)) {
|
||||
const code = match[1]!
|
||||
const rawValue = match[2]!
|
||||
|
||||
const entry = CODES[code]
|
||||
if (!entry) {
|
||||
throw new Error(`Code de parametre inconnu : '${code}'`)
|
||||
}
|
||||
|
||||
const { key, type } = entry
|
||||
|
||||
if (type === 'int') {
|
||||
// Python: int(float(raw)) truncates toward zero
|
||||
;(result as Record<keyof ModeParams, unknown>)[key] = Math.trunc(parseFloat(rawValue))
|
||||
} else if (type === 'float') {
|
||||
;(result as Record<keyof ModeParams, unknown>)[key] = parseFloat(rawValue)
|
||||
} else {
|
||||
;(result as Record<keyof ModeParams, unknown>)[key] = parseFloat(rawValue) !== 0.0
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a numeric value in DSL form: 0.1 -> ".1", 1 -> "1", 2.5 -> "2.5".
|
||||
*/
|
||||
function formatValue(value: number): string {
|
||||
const s = String(value)
|
||||
return value > 0 && value < 1 ? s.replace(/^0\./, '.') : s
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a parameter object back into a canonical mode-params string.
|
||||
*
|
||||
* Canonical form matches the strings used across the project
|
||||
* (e.g. "D30M50B.1G.2S.1"): D, M, B, G always present in that order,
|
||||
* then C when non-zero, then S/T/N when set, then R1 when ratio mode.
|
||||
* Round-trip: parseModeParams(formatModeParams(p)) is identical to p.
|
||||
*
|
||||
* @param params - Parameters to encode (missing keys take defaults)
|
||||
* @returns Compact string, e.g. "D30M50B.1G.2"
|
||||
*/
|
||||
export function formatModeParams(params: Partial<ModeParams> = {}): string {
|
||||
const p = getDefaults()
|
||||
for (const key of Object.keys(p) as Array<keyof ModeParams>) {
|
||||
const value = params[key]
|
||||
if (value !== undefined) {
|
||||
;(p[key] as ModeParams[typeof key]) = value
|
||||
}
|
||||
}
|
||||
|
||||
const parts: string[] = [
|
||||
`D${formatValue(p.duration_days)}`,
|
||||
`M${formatValue(p.majority_pct)}`,
|
||||
`B${formatValue(p.base_exponent)}`,
|
||||
`G${formatValue(p.gradient_exponent)}`,
|
||||
]
|
||||
|
||||
if (p.constant_base !== 0) {
|
||||
parts.push(`C${formatValue(p.constant_base)}`)
|
||||
}
|
||||
if (p.smith_exponent !== null) {
|
||||
parts.push(`S${formatValue(p.smith_exponent)}`)
|
||||
}
|
||||
if (p.techcomm_exponent !== null) {
|
||||
parts.push(`T${formatValue(p.techcomm_exponent)}`)
|
||||
}
|
||||
if (p.ratio_multiplier !== null) {
|
||||
parts.push(`N${formatValue(p.ratio_multiplier)}`)
|
||||
}
|
||||
if (p.is_ratio_mode) {
|
||||
parts.push('R1')
|
||||
}
|
||||
|
||||
return parts.join('')
|
||||
}
|
||||
Reference in New Issue
Block a user