forked from yvv/decision
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,118 @@
|
||||
/**
|
||||
* Ported from backend/app/tests/test_nuanced.py — every pytest case
|
||||
* exists here with the same expected values.
|
||||
*
|
||||
* Levels: 0-CONTRE, 1-PAS DU TOUT, 2-PAS D'ACCORD, 3-NEUTRE, 4-D'ACCORD, 5-TOUT A FAIT
|
||||
* Positive = levels 3 + 4 + 5
|
||||
* Adoption requires: positive_pct >= threshold (80%) AND total >= min_participants (59).
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { nuancedResult } from '../../app/engine/nuanced'
|
||||
|
||||
/** Build a votes array from level repetitions, e.g. fill([5, 20], [4, 20]). */
|
||||
function fill(...groups: Array<[level: number, count: number]>): number[] {
|
||||
const votes: number[] = []
|
||||
for (const [level, count] of groups) {
|
||||
for (let i = 0; i < count; i++) votes.push(level)
|
||||
}
|
||||
return votes
|
||||
}
|
||||
|
||||
describe('nuancedResult — adoption', () => {
|
||||
it('59 positive + 10 negative = 69 total => adopted (85.51% >= 80%)', () => {
|
||||
const votes = fill([5, 20], [4, 20], [3, 19], [2, 5], [1, 3], [0, 2])
|
||||
const result = nuancedResult(votes, 80, 59)
|
||||
|
||||
expect(result.total).toBe(69)
|
||||
expect(result.positive_count).toBe(59)
|
||||
expect(result.positive_pct).toBeCloseTo(85.51, 1)
|
||||
// Python cross-check: round(59/69*100, 2) == 85.51
|
||||
expect(result.positive_pct).toBe(85.51)
|
||||
expect(result.threshold_met).toBe(true)
|
||||
expect(result.min_participants_met).toBe(true)
|
||||
expect(result.adopted).toBe(true)
|
||||
})
|
||||
|
||||
it('all 59 voters at level 5 => 100% positive, adopted', () => {
|
||||
const votes = fill([5, 59])
|
||||
const result = nuancedResult(votes, 80, 59)
|
||||
|
||||
expect(result.total).toBe(59)
|
||||
expect(result.positive_count).toBe(59)
|
||||
expect(result.positive_pct).toBe(100.0)
|
||||
expect(result.adopted).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nuancedResult — rejection', () => {
|
||||
it('40 positive + 30 negative = 70 total => threshold not met (57.14% < 80%)', () => {
|
||||
const votes = fill([5, 15], [4, 15], [3, 10], [2, 10], [1, 10], [0, 10])
|
||||
const result = nuancedResult(votes, 80, 59)
|
||||
|
||||
expect(result.total).toBe(70)
|
||||
expect(result.positive_count).toBe(40)
|
||||
expect(result.positive_pct).toBeCloseTo(57.14, 1)
|
||||
expect(result.threshold_met).toBe(false)
|
||||
expect(result.min_participants_met).toBe(true) // 70 >= 59
|
||||
expect(result.adopted).toBe(false)
|
||||
})
|
||||
|
||||
it('55 total < 59 min participants => rejected despite 90.9% positive', () => {
|
||||
const votes = fill([5, 30], [4, 10], [3, 10], [1, 3], [0, 2])
|
||||
const result = nuancedResult(votes, 80, 59)
|
||||
|
||||
expect(result.total).toBe(55)
|
||||
expect(result.positive_count).toBe(50)
|
||||
expect(result.positive_pct).toBeGreaterThan(80)
|
||||
expect(result.threshold_met).toBe(true)
|
||||
expect(result.min_participants_met).toBe(false)
|
||||
expect(result.adopted).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nuancedResult — edge cases', () => {
|
||||
it('exactly 80% positive votes passes the threshold', () => {
|
||||
// 80 positive out of 100 = exactly 80%
|
||||
const votes = fill([5, 40], [4, 20], [3, 20], [2, 10], [1, 5], [0, 5])
|
||||
const result = nuancedResult(votes, 80, 59)
|
||||
|
||||
expect(result.total).toBe(100)
|
||||
expect(result.positive_count).toBe(80)
|
||||
expect(result.positive_pct).toBe(80.0)
|
||||
expect(result.threshold_met).toBe(true)
|
||||
expect(result.min_participants_met).toBe(true)
|
||||
expect(result.adopted).toBe(true)
|
||||
})
|
||||
|
||||
it('79 positive out of 100 = 79% < 80% => rejected', () => {
|
||||
const votes = fill([5, 39], [4, 20], [3, 20], [2, 11], [1, 5], [0, 5])
|
||||
const result = nuancedResult(votes, 80, 59)
|
||||
|
||||
expect(result.total).toBe(100)
|
||||
expect(result.positive_count).toBe(79)
|
||||
expect(result.positive_pct).toBe(79.0)
|
||||
expect(result.threshold_met).toBe(false)
|
||||
expect(result.adopted).toBe(false)
|
||||
})
|
||||
|
||||
it('zero votes => not adopted', () => {
|
||||
const result = nuancedResult([], 80, 59)
|
||||
expect(result.total).toBe(0)
|
||||
expect(result.positive_count).toBe(0)
|
||||
expect(result.positive_pct).toBe(0.0)
|
||||
expect(result.adopted).toBe(false)
|
||||
})
|
||||
|
||||
it('vote level outside 0-5 throws', () => {
|
||||
expect(() => nuancedResult([5, 3, 6])).toThrow(/invalide/)
|
||||
})
|
||||
|
||||
it('per-level breakdown is correct', () => {
|
||||
const votes = [0, 1, 2, 3, 4, 5, 5, 4, 3]
|
||||
const result = nuancedResult(votes, 50, 1)
|
||||
|
||||
expect(result.per_level_counts).toEqual({ 0: 1, 1: 1, 2: 1, 3: 2, 4: 2, 5: 2 })
|
||||
expect(result.positive_count).toBe(6) // 2+2+2
|
||||
expect(result.total).toBe(9)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user