Files
decision/maquette/frontend/tests/seeds/duniter-g1.spec.ts
YvvandClaude Opus 5.5 67ddee28b2 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>
2026-10-21 02:04:42 +02:00

368 lines
16 KiB
TypeScript
Raw Permalink 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 d’engagement 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 d’inertie 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 d’engagement 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 d’inertie 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)
}
})
})