Files
decision/frontend/tests/seeds/duniter-g1.spec.ts
T
YvvandClaude Fable 5 6f2bf62295 v2 : seed Duniter Ğ1 extrait sans perte (bundle 331 Ko + 22 tests)
- export_seed_bundle.py : parsing ast de seed.py (zéro dépendance DB),
  uuid5 déterministes, sha256 reproductible
- 33 clauses Certification + 59 Forgeron v2.0.0-fr, voteRecord 97/23/19
  W=7224 seuil 97 D30M50B.1G.2S.1, 11 personnes (5 forgerons), 3 mandats,
  10 protocoles, Pacte 20 clauses, 118 décisions fondatrices chaînées
- Écarts v1 documentés dans le script (sessions de démo non portées,
  ComTech reconstruit depuis la recherche forum, presets câblés par clause)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 09:48:13 +02:00

368 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Test de NON-PERTE du seed Ğ1 — BLUEPRINT-V2.md « Seeds » (bloquant).
*
* Vérifie que frontend/app/data/seeds/duniter-g1.bundle.json (émis par
* backend/scripts/export_seed_bundle.py depuis backend/seed.py) conserve
* l'intégralité du contenu réel Ğ1 :
* - 33 clauses Certification + 59 clauses Forgeron, exactement ;
* - presets d'inertie conservés (les 4 valeurs présentes sur chaque doc,
* répartition exacte du seed v1 : cert 8/19/4/2, forgeron 4/49/4/2) ;
* - provenance.voteRecord == { for: 97, against: 23, invalid: 19,
* wotSize: 7224, thresholdRequired: 97 }, modeParams 'D30M50B.1G.2S.1' ;
* - 11 personnes dont 5 smiths ; 3 mandats ; ≥ 7 protocoles dont le
* Consentement (invariant) ;
* - chaque clause courante chaînée à sa décision fondatrice adoptée ;
* - chaque entité porte collectiveId / createdAt / updatedAt.
*/
import { describe, expect, it } from 'vitest'
import { resolveSettings } from '~/engine/settings'
import type {
Bundle,
Clause,
ClauseVersion,
Decision,
InertiaPreset,
Mandate,
Person,
Protocol,
TextDoc,
Vote,
VoteSession,
} from '~/types/domain'
import raw from '../../app/data/seeds/duniter-g1.bundle.json'
const bundle = raw as unknown as Bundle
const docBySlug = (slug: string): TextDoc => {
const doc = bundle.docs.find((d) => d.slug === slug)
expect(doc, `doc ${slug}`).toBeDefined()
return doc as TextDoc
}
const clausesOf = (doc: TextDoc): Clause[] => bundle.clauses.filter((c) => c.docId === doc.id)
const versionById = new Map<string, ClauseVersion>(bundle.versions.map((v) => [v.id, v]))
const decisionById = new Map<string, Decision>(bundle.decisions.map((d) => [d.id, d]))
const protocolById = new Map<string, Protocol>(bundle.protocols.map((p) => [p.id, p]))
const personByName = new Map<string, Person>(bundle.people.map((p) => [p.displayName, p]))
const inertiaDistribution = (doc: TextDoc): Record<string, number> => {
const dist: Record<string, number> = {}
for (const clause of clausesOf(doc)) dist[clause.inertia] = (dist[clause.inertia] ?? 0) + 1
return dist
}
describe('bundle duniter-g1 — schéma v2', () => {
it('est un Bundle schemaVersion 2 avec assents:[]', () => {
expect(bundle.schemaVersion).toBe(2)
expect(bundle.exportedAt).toBeTruthy()
expect(bundle.assents).toEqual([])
expect(bundle.concerns).toEqual([])
expect(bundle.objections).toEqual([])
expect(bundle.advices).toEqual([])
})
it('collectif : slug/template/transparence/couleur/icône de la spec Seeds', () => {
const c = bundle.collective
expect(c.slug).toBe('duniter-g1')
expect(c.template).toBe('free-currency')
expect(c.isTransparent).toBe(true)
expect(c.color).toBe('#22c55e')
expect(c.icon).toBe('i-lucide-network')
expect(bundle.docs.find((d) => d.id === c.pactDocId)?.role).toBe('pact')
expect(bundle.circles.find((ci) => ci.id === c.rootCircleId)?.name).toBe('Toile de confiance')
})
it('chaque entité porte collectiveId, createdAt et updatedAt', () => {
const collections = [
bundle.people,
bundle.circles,
bundle.decisions,
bundle.concerns,
bundle.objections,
bundle.advices,
bundle.assents,
bundle.mandates,
bundle.docs,
bundle.clauses,
bundle.versions,
bundle.protocols,
bundle.sessions,
bundle.votes,
] as { id: string; collectiveId: string; createdAt: string; updatedAt: string }[][]
for (const collection of collections) {
for (const item of collection) {
expect(item.collectiveId, `collectiveId de ${item.id}`).toBe(bundle.collective.id)
expect(item.createdAt, `createdAt de ${item.id}`).toBeTruthy()
expect(item.updatedAt, `updatedAt de ${item.id}`).toBeTruthy()
}
}
expect(bundle.collective.createdAt).toBeTruthy()
expect(bundle.collective.updatedAt).toBeTruthy()
})
})
describe('non-perte — Acte dengagement Certification (33 items)', () => {
const doc = docBySlug('engagement-certification')
const clauses = clausesOf(doc)
it('contient exactement 33 clauses aux codes uniques', () => {
expect(clauses).toHaveLength(33)
expect(new Set(clauses.map((c) => c.code)).size).toBe(33)
})
it('conserve la répartition des 4 presets dinertie du seed v1 (8/19/4/2)', () => {
expect(inertiaDistribution(doc)).toEqual({ low: 8, standard: 19, high: 4, max: 2 })
})
it('conserve le texte voté (échantillon E1 — unicité du compte)', () => {
const e1 = clauses.find((c) => c.code === 'E1')
expect(e1).toBeDefined()
const version = versionById.get(e1!.currentVersionId!)
expect(version?.content).toContain('un seul et unique compte cocréateur de monnaie G1')
})
it('porte la provenance v1 (sources + contributeurs)', () => {
expect(doc.provenance?.sources.length).toBeGreaterThanOrEqual(6)
expect(doc.provenance?.contributors?.join(' ')).toContain('1000i100')
})
})
describe('non-perte — Acte dengagement forgeron (59 items v2.0.0-fr)', () => {
const doc = docBySlug('engagement-forgeron')
const clauses = clausesOf(doc)
it('contient exactement 59 clauses aux codes uniques, en versionLabel 2.0.0-fr', () => {
expect(clauses).toHaveLength(59)
expect(new Set(clauses.map((c) => c.code)).size).toBe(59)
for (const clause of clauses) {
expect(versionById.get(clause.currentVersionId!)?.versionLabel).toBe('2.0.0-fr')
}
})
it('conserve les 18 clauses aspirant (A1..A18) et les 16 certificateur (C1..C16)', () => {
const codes = new Set(clauses.map((c) => c.code))
for (let i = 1; i <= 18; i++) expect(codes.has(`A${i}`), `A${i}`).toBe(true)
for (let i = 1; i <= 16; i++) expect(codes.has(`C${i}`), `C${i}`).toBe(true)
})
it('conserve la répartition des 4 presets dinertie du seed v1 (4/49/4/2)', () => {
expect(inertiaDistribution(doc)).toEqual({ low: 4, standard: 49, high: 4, max: 2 })
})
it('voteRecord === {for:97, against:23, invalid:19, wotSize:7224, thresholdRequired:97}', () => {
const record = doc.provenance?.voteRecord
expect(record).toBeDefined()
expect(record!.result).toMatchObject({
for: 97,
against: 23,
invalid: 19,
wotSize: 7224,
thresholdRequired: 97,
})
expect(record!.modeParams).toBe('D30M50B.1G.2S.1')
expect(record!.period).toBe('2026-01-07 → 2026-02-06')
expect(record!.url).toBe(
'https://forum.monnaie-libre.fr/t/vote-engagement-forgeron-v2-0-0/33165',
)
})
})
describe('non-perte — Comité Technique et Pacte', () => {
it('engagement-comite-tech : clauses présentes, source du vote 33293 tracée', () => {
const doc = docBySlug('engagement-comite-tech')
const clauses = clausesOf(doc)
expect(clauses.length).toBeGreaterThanOrEqual(4)
expect(doc.provenance?.sources.some((s) => s.url.includes('33293'))).toBe(true)
// Contenu reconstruit (vote en cours au moment de la recherche) — noté :
expect(doc.provenance?.notes).toContain('research_duniter_forum.md')
})
it('pacte-duniter-g1 : rôle pact, description sacralisée, A1 « Notre finalité »', () => {
const pact = docBySlug('pacte-duniter-g1')
expect(pact.role).toBe('pact')
expect(pact.description).toBe('Notre contrat social — sacralisé, jamais immuable')
const a1 = clausesOf(pact).find((c) => c.code === 'A1')
expect(a1?.title).toBe('Notre finalité')
const content = versionById.get(a1!.currentVersionId!)?.content ?? ''
for (const word of ['Autonomie', 'Équilibre', 'Liaison', 'toile de confiance Ğ1']) {
expect(content).toContain(word)
}
})
it('le Pacte résout les réglages v2 (resolveSettings) avec les seuils du seed', () => {
const pact = docBySlug('pacte-duniter-g1')
const settings = resolveSettings(clausesOf(pact), bundle.versions)
expect(settings.triage).toEqual({
smallGroupMax: 5,
collectiveMin: 50,
consentMax: 7,
objectionWindowHours: 48,
adviceWindowHours: 72,
framingDays: 14,
concernEscalateRatio: 0.5,
recurrenceThreshold: 3,
reviewDelayDays: 90,
requireEffects: 'binding',
})
// consent OBLIGATOIRE + large = binaire inertiel (héritage free-currency)
expect(protocolById.get(settings.protocolByRange.consent)?.method).toBe('consent')
expect(protocolById.get(settings.protocolByRange.large!)?.name).toBe('Vote WoT standard')
expect(protocolById.get(settings.protocolByRange.nuanced!)?.method).toBe('nuanced')
expect(protocolById.get(settings.protocolByRange.parametric!)?.method).toBe('parametric')
expect(protocolById.get(settings.protocolByRange.election!)?.method).toBe('election')
})
it('clauseByInertia : les 4 presets pointent les formules v1 (M/G intacts)', () => {
const pact = docBySlug('pacte-duniter-g1')
const settings = resolveSettings(clausesOf(pact), bundle.versions)
const byInertia = settings.protocolByRange.clauseByInertia
expect(byInertia).toBeDefined()
const expected: Record<InertiaPreset, { M: number; G: number }> = {
low: { M: 50, G: 0.1 },
standard: { M: 50, G: 0.2 },
high: { M: 60, G: 0.4 },
max: { M: 66, G: 0.6 },
}
for (const preset of Object.keys(expected) as InertiaPreset[]) {
const protocol = protocolById.get(byInertia![preset])
expect(protocol, preset).toBeDefined()
expect(protocol!.formula.majorityPct, preset).toBe(expected[preset].M)
expect(protocol!.formula.gradientExponent, preset).toBe(expected[preset].G)
expect(protocol!.formula.baseExponent, preset).toBe(0.1)
}
// La clause qui régit l'inertie est elle-même en preset 'max' (méta-protection)
const metaClauses = clausesOf(pact).filter((c) =>
c.settingKey?.startsWith('protocols.clauseByInertia.'),
)
expect(metaClauses.length).toBe(4)
for (const clause of metaClauses) expect(clause.inertia, clause.code).toBe('max')
})
})
describe('non-perte — personnes, protocoles, mandats', () => {
it('11 personnes dont exactement 5 smiths', () => {
expect(bundle.people).toHaveLength(11)
expect(bundle.people.filter((p) => p.wotStatus === 'smith')).toHaveLength(5)
for (const name of ['Moul', 'Poka', 'Hugo', 'Elois', 'Cgeek']) {
expect(personByName.get(name)?.wotStatus, name).toBe('smith')
}
})
it('≥ 7 protocoles, tous ballot open, avec le Consentement OBLIGATOIRE', () => {
expect(bundle.protocols.length).toBeGreaterThanOrEqual(7)
for (const p of bundle.protocols) expect(p.ballot, p.name).toBe('open')
expect(bundle.protocols.filter((p) => p.method === 'consent')).toHaveLength(1)
const methods = new Set(bundle.protocols.map((p) => p.method))
for (const m of ['consent', 'binary', 'nuanced', 'parametric', 'election']) {
expect(methods.has(m as Protocol['method']), m).toBe(true)
}
})
it('formules v1 intactes : WoT standard, Smith S.1, ComTech T.1, nuancé 59/80', () => {
const wot = bundle.protocols.find((p) => p.name === 'Vote WoT standard')!
expect(wot.modeParams).toBe('D30M50B.1G.2')
expect(wot.formula).toMatchObject({ majorityPct: 50, baseExponent: 0.1, gradientExponent: 0.2 })
expect(wot.description).toContain('dernier recours des très grands corps')
expect(wot.description).toContain('héritage Toile de Confiance')
const smith = bundle.protocols.find((p) => p.name === 'Vote forgeron (Smith)')!
expect(smith.modeParams).toBe('D30M50B.1G.2S.1')
expect(smith.formula.smithExponent).toBe(0.1)
const tech = bundle.protocols.find((p) => p.name === 'Vote Comité Technique')!
expect(tech.modeParams).toBe('D30M50B.1G.2T.1')
expect(tech.formula.techcommExponent).toBe(0.1)
const nuanced = bundle.protocols.find((p) => p.method === 'nuanced')!
expect(nuanced.durationDays).toBe(30)
expect(nuanced.formula.nuancedMinParticipants).toBe(59)
expect(nuanced.formula.nuancedThresholdPct).toBe(80)
const parametric = bundle.protocols.find((p) => p.method === 'parametric')!
expect(parametric.durationDays).toBe(14)
expect(parametric.formula.parametricMinParticipants).toBe(5)
const election = bundle.protocols.find((p) => p.method === 'election')!
expect(election.durationDays).toBe(14)
expect(election.formula.electionMinParticipants).toBe(5)
expect(election.formula.tieBreak).toBe('runoff')
})
it('3 mandats : ComTech actif (Cgeek, 1 rapport dû) · Admin Forgerons proposé · Modération active', () => {
expect(bundle.mandates).toHaveLength(3)
const comtech = bundle.mandates.find((m) => m.title.includes('Comité Technique')) as Mandate
expect(comtech.status).toBe('active')
expect(comtech.holderId).toBe(personByName.get('Cgeek')!.id)
const due = comtech.reports.filter((r) => !r.deliveredAt)
expect(due.length).toBeGreaterThanOrEqual(1)
const admin = bundle.mandates.find((m) => m.title.includes('Administrateur')) as Mandate
expect(admin.status).toBe('proposed')
const forum = bundle.mandates.find((m) => m.title.includes('Modération')) as Mandate
expect(forum.status).toBe('active')
expect(forum.domain.tags).toEqual(['forum', 'moderation'])
})
it('le mandat proposé est EN vote nuancé : session ouverte + 2-3 votes', () => {
const admin = bundle.mandates.find((m) => m.title.includes('Administrateur')) as Mandate
const origin = decisionById.get(admin.originDecisionId)!
expect(origin.status).toBe('voting')
expect(origin.createsMandate).toBeDefined()
expect(protocolById.get(origin.protocolId!)?.method).toBe('nuanced')
const session = bundle.sessions.find((s) => s.decisionId === origin.id) as VoteSession
expect(session).toBeDefined()
expect(session.status).toBe('open')
expect(session.corpusSize).toBe(session.corpusPersonIds.length)
const sessionVotes = bundle.votes.filter((v) => v.sessionId === session.id) as Vote[]
expect(sessionVotes.length).toBeGreaterThanOrEqual(2)
expect(sessionVotes.length).toBeLessThanOrEqual(3)
for (const vote of sessionVotes) {
expect(typeof vote.value).toBe('number')
expect(bundle.people.some((p) => p.id === vote.voterId)).toBe(true)
}
})
})
describe('invariant — la décision est le pivot', () => {
it('chaque clause courante a sa décision fondatrice adoptée, chaînée', () => {
for (const clause of bundle.clauses) {
expect(clause.currentVersionId, `currentVersionId de ${clause.code}`).toBeTruthy()
const version = versionById.get(clause.currentVersionId!)!
expect(version.status, `version de ${clause.code}`).toBe('current')
const decision = decisionById.get(version.decisionId)!
expect(decision, `décision fondatrice de ${clause.code}`).toBeDefined()
expect(decision.status).toBe('adopted')
expect(decision.route).toBe('collective')
expect(decision.title).toBe(`Adoption fondatrice — ${clause.code}`)
expect(decision.amendsClauseId).toBe(clause.id)
expect(decision.decidedAt).toBeTruthy()
}
})
it('décisions historiques : Runtime Upgrade 1100 gravée + licence v0.4.0 en formulation', () => {
const runtime = bundle.decisions.find((d) => d.title === 'Runtime Upgrade 1100')!
expect(runtime.status).toBe('adopted')
expect(runtime.weight).toBe('structural')
expect(runtime.engraving?.proofLevel).toBe('local')
expect(runtime.engraving?.sha256).toMatch(/^[0-9a-f]{64}$/)
expect(runtime.review?.dueAt).toBeTruthy()
expect(runtime.brief?.effects.length).toBeGreaterThanOrEqual(1)
expect(runtime.brief?.effects.some((e) => e.target && e.measured)).toBe(true)
const licence = bundle.decisions.find((d) => d.title === 'Évolution Licence G1 v0.4.0')!
expect(licence.status).toBe('framing')
const proposed = bundle.versions.filter(
(v) => v.decisionId === licence.id && v.status === 'proposed',
)
expect(proposed.length).toBeGreaterThanOrEqual(1)
const certDoc = docBySlug('engagement-certification')
const certClauseIds = new Set(clausesOf(certDoc).map((c) => c.id))
for (const version of proposed) {
expect(certClauseIds.has(version.clauseId), 'version proposée liée à une clause cert').toBe(true)
}
})
})