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>
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user