#!/usr/bin/env python3 """Export du seed backend v1 (backend/seed.py) vers un Bundle JSON v2. Émet frontend/app/data/seeds/duniter-g1.bundle.json au schéma Bundle v2 (frontend/app/types/domain.ts), sans perte de contenu Ğ1. ISOLATION DES DONNÉES (aucune dépendance DB) : Importer seed.py déclencherait `from app.database import …` (SQLAlchemy, driver async, config). Ce script n'importe donc PAS seed.py : il le PARSE avec `ast` et extrait les structures par évaluation littérale tolérante : - littéraux au niveau module : GENESIS_CERTIFICATION, ENGAGEMENT_CERTIFICATION_ITEMS (33), GENESIS_FORGERON, ENGAGEMENT_FORGERON_ITEMS (59), VOTER_NAMES (11), RUNTIME_UPGRADE_STEPS ; - littéraux internes aux fonctions : configs (FormulaConfigs ×7), protocols (VotingProtocols ×3), mandates_data (×3), steps (licence), orgs_data, kwargs des appels get_or_create / QualificationProtocol. Les valeurs non littérales (ids SQLAlchemy, datetime.now()…) sont ignorées par l'extracteur et recalculées côté v2 (dates historiques fixes, uuid5 déterministes). REPRODUCTIBILITÉ : uuid5(NAMESPACE fixe, slug) partout + dates constantes (aucun datetime.now) → deux exécutions produisent un JSON identique octet à octet. ÉCARTS ASSUMÉS (voir rapport imprimé en fin d'exécution) : - les 3 VoteSessions de démo v1 (10 pour / 1 contre simulés sur les 3 premiers items forgeron) ne sont PAS portées : votes factices sans contenu Ğ1 réel — le vrai vote (97/23/19) vit dans provenance.voteRecord ; - l'organisation v1 'axiom-team' n'est pas exportée (un bundle = UN collectif) ; - icon 'i-lucide-network' (spec BLUEPRINT-V2 « Seeds ») remplace 'i-lucide-globe' (v1) ; la description de l'organisation v1 est conservée dans la provenance du Pacte ; - v1 mappait TOUS les items cert sur « Vote WoT standard » (son inertia_protocol_map pointait le même protocole pour les 4 presets) ; v2 câble clauseByInertia par preset (règle R3 du blueprint) — les FormulaConfigs v1 basse/haute/très haute deviennent des protocoles ; - item_type v1 (preamble/section/clause/rule/verification) conservé dans les tags de la décision fondatrice de chaque clause (`v1:`). Usage : backend/.venv/bin/python backend/scripts/export_seed_bundle.py """ from __future__ import annotations import ast import hashlib import json import uuid from pathlib import Path HERE = Path(__file__).resolve() BACKEND = HERE.parents[1] REPO = HERE.parents[2] SEED_PATH = BACKEND / "seed.py" OUT_PATH = REPO / "frontend" / "app" / "data" / "seeds" / "duniter-g1.bundle.json" # ───────────────────────────────────────────────────────────── # Extraction AST — aucun import de seed.py # ───────────────────────────────────────────────────────────── _SKIP = object() def _tolerant(node: ast.AST): """Évaluation littérale tolérante : tout nœud non littéral est ignoré.""" if isinstance(node, ast.Constant): return node.value if isinstance(node, ast.Dict): out = {} # (clé None = **unpacking — absent des littéraux extraits de seed.py) for key_node, value_node in zip(node.keys, node.values): if key_node is None: continue key = _tolerant(key_node) value = _tolerant(value_node) if key is _SKIP or value is _SKIP: continue out[key] = value return out if isinstance(node, (ast.List, ast.Tuple)): return [v for v in (_tolerant(e) for e in node.elts) if v is not _SKIP] if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): operand = _tolerant(node.operand) return -operand if operand is not _SKIP else _SKIP return _SKIP def _assign_value(scope: ast.AST, name: str): """Valeur littérale de la première assignation `name = …` dans le scope.""" for node in ast.walk(scope): target = None if ( isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) ): target = node.targets[0].id elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): target = node.target.id if target == name and getattr(node, "value", None) is not None: value = _tolerant(node.value) if value is not _SKIP: return value raise KeyError(f"assignation littérale introuvable : {name}") def _function(tree: ast.Module, name: str) -> ast.AST: for node in tree.body: if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name: return node raise KeyError(f"fonction introuvable : {name}") def _call_kwargs(scope: ast.AST, callee: str) -> dict: """Kwargs littéraux du premier appel `callee(...)` du scope.""" for node in ast.walk(scope): if not isinstance(node, ast.Call): continue func = node.func fname = func.id if isinstance(func, ast.Name) else getattr(func, "attr", None) if fname != callee: continue kwargs = {} for kw in node.keywords: if kw.arg is None: continue value = _tolerant(kw.value) if value is not _SKIP: kwargs[kw.arg] = value return kwargs raise KeyError(f"appel introuvable : {callee}") def _json_dumps_arg(scope: ast.AST): """Premier argument littéral d'un appel json.dumps(...) dans le scope.""" for node in ast.walk(scope): if ( isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "dumps" and node.args ): value = _tolerant(node.args[0]) if value is not _SKIP: return value return None _tree = ast.parse(SEED_PATH.read_text(encoding="utf-8")) GEN_CERT = _assign_value(_tree, "GENESIS_CERTIFICATION") CERT_ITEMS = _assign_value(_tree, "ENGAGEMENT_CERTIFICATION_ITEMS") GEN_FORGE = _assign_value(_tree, "GENESIS_FORGERON") FORGE_ITEMS = _assign_value(_tree, "ENGAGEMENT_FORGERON_ITEMS") VOTER_NAMES = _assign_value(_tree, "VOTER_NAMES") RUNTIME_STEPS = _assign_value(_tree, "RUNTIME_UPGRADE_STEPS") FORMULAS_V1 = _assign_value(_function(_tree, "seed_formula_configs"), "configs") PROTOCOLS_V1 = _assign_value(_function(_tree, "seed_voting_protocols"), "protocols") MANDATES_V1 = _assign_value(_function(_tree, "seed_mandates"), "mandates_data") ORGS_V1 = _assign_value(_function(_tree, "seed_organizations"), "orgs_data") LICENCE_STEPS = _assign_value(_function(_tree, "seed_decision_licence_evolution"), "steps") _fn_qual = _function(_tree, "seed_qualification_protocol") QUAL_V1 = _call_kwargs(_fn_qual, "QualificationProtocol") QUAL_MODALITIES_V1 = _json_dumps_arg(_fn_qual) or [] CERT_DOC_META = _call_kwargs( _function(_tree, "seed_document_engagement_certification"), "get_or_create" ) FORGE_DOC_META = _call_kwargs( _function(_tree, "seed_document_engagement_forgeron"), "get_or_create" ) RUNTIME_META = _call_kwargs(_function(_tree, "seed_decision_runtime_upgrade"), "get_or_create") LICENCE_META = _call_kwargs(_function(_tree, "seed_decision_licence_evolution"), "get_or_create") # ── Garde-fous de non-perte à l'extraction ─────────────────── assert len(CERT_ITEMS) == 33, f"attendu 33 items Certification, extrait {len(CERT_ITEMS)}" assert len(FORGE_ITEMS) == 59, f"attendu 59 items Forgeron, extrait {len(FORGE_ITEMS)}" assert len(VOTER_NAMES) == 11, f"attendu 11 votants, extrait {len(VOTER_NAMES)}" assert len(MANDATES_V1) == 3, f"attendu 3 mandats v1, extrait {len(MANDATES_V1)}" assert len(FORMULAS_V1) == 7, f"attendu 7 FormulaConfigs, extrait {len(FORMULAS_V1)}" assert len(PROTOCOLS_V1) == 3, f"attendu 3 VotingProtocols, extrait {len(PROTOCOLS_V1)}" _vr = GEN_FORGE["vote_record"]["result"] assert (_vr["pour"], _vr["contre"], _vr["nuls_invalides"]) == (97, 23, 19) assert (_vr["wot_size"], _vr["threshold_required"]) == (7224, 97) # ───────────────────────────────────────────────────────────── # Constantes v2 — ids déterministes et dates historiques fixes # ───────────────────────────────────────────────────────────── NAMESPACE = uuid.uuid5(uuid.NAMESPACE_URL, "https://libredecision.org/seeds/duniter-g1") def uid(slug: str) -> str: return str(uuid.uuid5(NAMESPACE, slug)) COLLECTIVE_ID = uid("collective:duniter-g1") EXPORTED_AT = "2026-08-11T12:00:00Z" T_GENESIS = "2025-12-01T10:00:00Z" # création du collectif, adoption cert + Pacte T_CERT_DRAFT = "2025-11-15T10:00:00Z" T_FORGE_OPEN = "2026-01-07T09:00:00Z" # ouverture du vote forgeron (seed v1) T_FORGE_ADOPT = "2026-02-06T18:00:00Z" # clôture/adoption (97/23, WoT 7224) T_COMTECH_OPEN = "2026-02-04T09:00:00Z" T_COMTECH_ADOPT = "2026-03-06T18:00:00Z" PID = {name: uid(f"person:{name.lower()}") for name in VOTER_NAMES} SMITH_NAMES = VOTER_NAMES[:5] # v1 : is_smith=(i < 5) C_ROOT = uid("circle:toile-de-confiance") C_FORGE = uid("circle:forgerons") C_TECH = uid("circle:comite-technique") DOC_CERT = uid("doc:engagement-certification") DOC_FORGE = uid("doc:engagement-forgeron") DOC_TECH = uid("doc:engagement-comite-tech") DOC_PACT = uid("doc:pacte-duniter-g1") P_CONSENT = uid("protocol:consentement") P_WOT = uid("protocol:vote-wot-standard") P_NUANCED = uid("protocol:vote-nuance") P_SMITH = uid("protocol:vote-forgeron-smith") P_TECHCOMM = uid("protocol:vote-comite-technique") P_PARAMETRIC = uid("protocol:reglage-collectif") P_ELECTION = uid("protocol:election-sans-candidat") P_INERTIA_LOW = uid("protocol:inertie-basse") P_INERTIA_HIGH = uid("protocol:inertie-haute") P_INERTIA_MAX = uid("protocol:inertie-tres-haute") # Presets v1 → InertiaPreset v2 (spec : very_high → 'max') PRESET_MAP = {"low": "low", "standard": "standard", "high": "high", "very_high": "max"} # R3 : protocole d'amendement d'une clause = clauseByInertia[preset] PROTO_BY_INERTIA = { "low": P_INERTIA_LOW, "standard": P_WOT, "high": P_INERTIA_HIGH, "max": P_INERTIA_MAX, } def _fmt(value: float) -> str: """0.1 → '.1', 50 → '50' (DSL modeParams, forme canonique).""" if value == int(value): return str(int(value)) s = str(value) return s[1:] if s.startswith("0.") else s def entity(eid: str, created: str, updated: str | None = None, **fields) -> dict: return { "id": eid, "collectiveId": COLLECTIVE_ID, "createdAt": created, "updatedAt": updated or created, **fields, } # ───────────────────────────────────────────────────────────── # Personnes (11, dont 5 smiths — v1 VOTER_NAMES) # ───────────────────────────────────────────────────────────── people = [] for i, name in enumerate(VOTER_NAMES): # Même dérivation d'adresse que seed.py (sha256(name)[:32] préfixé '5') address = "5" + hashlib.sha256(name.encode()).hexdigest()[:32] people.append( entity( PID[name], T_GENESIS, displayName=name, isMe=False, duniterAddress=address, wotStatus="smith" if i < 5 else "member", ) ) # ───────────────────────────────────────────────────────────── # Cercles (3, typés kind) # ───────────────────────────────────────────────────────────── COMTECH_MEMBERS = ["Elois", "Cgeek", "Maaltir", "Hugo", "Tuxmain"] # v1 : outcome de l'élection circles = [ entity( C_ROOT, T_GENESIS, name="Toile de confiance", purpose="Tous les membres de la toile de confiance Ğ1 — le corps souverain du collectif.", memberIds=[PID[n] for n in VOTER_NAMES], domains=["toile", "certification", "licence"], ), entity( C_FORGE, T_GENESIS, name="Forgerons", purpose="Les forgerons (validateurs de blocs) de la blockchain Duniter V2 — la sous-toile Smith.", kind="team", memberIds=[PID[n] for n in SMITH_NAMES], parentCircleId=C_ROOT, domains=["forge", "noeuds", "runtime"], ), entity( C_TECH, T_GENESIS, name="Comité Technique", purpose="Le Comité Technique protège la souveraineté de la communauté sur les mises à jour de la blockchain.", kind="team", memberIds=[PID[n] for n in COMTECH_MEMBERS], parentCircleId=C_ROOT, domains=["runtime", "code", "on-chain"], ), ] # ───────────────────────────────────────────────────────────── # Protocoles (10) — FormulaParams complets depuis les FormulaConfigs v1 # ───────────────────────────────────────────────────────────── _f_std = FORMULAS_V1["Standard Licence G1"] _f_low = FORMULAS_V1["Inertie basse (Annexes)"] _f_high = FORMULAS_V1["Inertie haute (Formule)"] _f_vhigh = FORMULAS_V1["Inertie très haute (Méta-réglage)"] _f_smith = FORMULAS_V1["Forgeron avec Smith"] _f_tech = FORMULAS_V1["Comité Tech"] _f_nuanced = FORMULAS_V1["Vote Nuance"] def _formula(cfg: dict, **extra) -> dict: return { "majorityPct": cfg["majority_pct"], "baseExponent": cfg["base_exponent"], "gradientExponent": cfg["gradient_exponent"], "constantBase": cfg["constant_base"], **extra, } def _binary_mode(cfg: dict, suffix: str = "") -> str: return ( f"D{cfg['duration_days']}M{_fmt(cfg['majority_pct'])}" f"B{_fmt(cfg['base_exponent'])}G{_fmt(cfg['gradient_exponent'])}{suffix}" ) def protocol(pid, name, method, description, days, formula, mode, pact_clause=None): p = entity( pid, T_GENESIS, name=name, method=method, description=description, durationDays=days, ballot="open", formula=formula, modeParams=mode, ) if pact_clause: p["pactClauseId"] = uid(f"clause:pacte-duniter-g1:{pact_clause}") return p protocols = [ protocol( P_CONSENT, "Consentement", "consent", "Ça me va / J'objecte — zéro objection argumentée maintenue vaut adoption. " "La modalité par défaut des petits corps, et le repli universel de toute " "clé de protocole absente. Hors du réversible, au moins un accord " "explicite est requis à l'échéance.", 7, {"majorityPct": 100, "baseExponent": 0.1, "gradientExponent": 0, "constantBase": 0}, "D7M100B.1G0", pact_clause="M1", ), protocol( P_WOT, "Vote WoT standard", "binary", "L'outil de dernier recours des très grands corps — héritage Toile de " "Confiance. " + PROTOCOLS_V1["Vote WoT standard"]["description"], _f_std["duration_days"], _formula(_f_std), PROTOCOLS_V1["Vote WoT standard"]["mode_params"], pact_clause="M2", ), protocol( P_NUANCED, "Vote nuancé", "nuanced", "Vote nuancé à 6 niveaux — 0 « Pas du tout » à 5 « Tout à fait » : chacun " "se prononce en nuances, pas en camps. Histogramme des 6 niveaux visible " "pendant et après le vote ; commentaire obligatoire sur 0-1. " "(v1 : « " + _f_nuanced["description"] + " »)", _f_nuanced["duration_days"], _formula( _f_nuanced, nuancedMinParticipants=_f_nuanced["nuanced_min_participants"], nuancedThresholdPct=_f_nuanced["nuanced_threshold_pct"], ), _binary_mode(_f_nuanced), pact_clause="M3", ), protocol( P_SMITH, "Vote forgeron (Smith)", "binary", PROTOCOLS_V1["Vote forgeron (Smith)"]["description"], _f_smith["duration_days"], _formula(_f_smith, smithExponent=_f_smith["smith_exponent"]), PROTOCOLS_V1["Vote forgeron (Smith)"]["mode_params"], ), protocol( P_TECHCOMM, "Vote Comité Technique", "binary", PROTOCOLS_V1["Vote Comité Technique"]["description"], _f_tech["duration_days"], _formula(_f_tech, techcommExponent=_f_tech["techcomm_exponent"]), PROTOCOLS_V1["Vote Comité Technique"]["mode_params"], ), protocol( P_PARAMETRIC, "Réglage collectif", "parametric", "Décider au curseur — chacun pose sa valeur, la médiane basse éclaire, " "le geste du garant cristallise. Quorum : 5 participants.", 14, { "majorityPct": 50, "baseExponent": 0.1, "gradientExponent": 0, "constantBase": 0, "parametricMinParticipants": 5, }, "D14M50B.1G0", pact_clause="M4", ), protocol( P_ELECTION, "Élection sans candidat", "election", "Désigner sans candidature — chacun nomme la personne qu'il estime la " "mieux placée. Vote blanc possible (compte pour la participation, pas " "pour la désignation). En cas d'égalité : session de départage entre " "ex æquo — jamais l'outil.", 14, { "majorityPct": 50, "baseExponent": 0.1, "gradientExponent": 0, "constantBase": 0, "electionMinParticipants": 5, "tieBreak": "runoff", }, "D14M50B.1G0", pact_clause="M5", ), protocol( P_INERTIA_LOW, "Inertie basse (Annexes)", "binary", _f_low["description"], _f_low["duration_days"], _formula(_f_low), _binary_mode(_f_low), pact_clause="N1", ), protocol( P_INERTIA_HIGH, "Inertie haute (Formule)", "binary", _f_high["description"], _f_high["duration_days"], _formula(_f_high), _binary_mode(_f_high), pact_clause="N3", ), protocol( P_INERTIA_MAX, "Inertie très haute (Méta-réglage)", "binary", _f_vhigh["description"], _f_vhigh["duration_days"], _formula(_f_vhigh), _binary_mode(_f_vhigh), pact_clause="N4", ), ] # ───────────────────────────────────────────────────────────── # TextDocs, Clauses, ClauseVersions, Décisions fondatrices # ───────────────────────────────────────────────────────────── docs: list[dict] = [] clauses: list[dict] = [] versions: list[dict] = [] decisions: list[dict] = [] def _steps_md(steps: list[dict]) -> str: lines = [] for s in steps: line = f"{s.get('step_order')}. **{s.get('title')}** — {s.get('description', '').strip()}" if s.get("status"): line += f" *(v1 : {s['status']}" if s.get("outcome"): line += f" — {s['outcome']}" line += ")*" lines.append(line) return "\n".join(lines) def add_doc(doc_id, slug, title, role, description, provenance, created, updated=None): docs.append( entity( doc_id, created, updated, slug=slug, title=title, role=role, description=description, provenance=provenance, ) ) def add_clause( *, doc_slug: str, doc_id: str, code: str, section: str, position: int, title: str, inertia: str, content: str, version_label: str, created: str, adopted: str, author: str, protocol_id: str, scope_circles: list[str], tags: list[str], setting_key: str | None = None, setting_value=None, body: str | None = None, ): cid = uid(f"clause:{doc_slug}:{code}") vid = uid(f"version:{doc_slug}:{code}:current") did = uid(f"decision:fondatrice:{doc_slug}:{code}") clause = entity( cid, adopted, docId=doc_id, section=section, position=position, code=code, title=title, inertia=inertia, currentVersionId=vid, ) if setting_key is not None: clause["settingKey"] = setting_key clauses.append(clause) version = entity( vid, created, adopted, clauseId=cid, decisionId=did, versionLabel=version_label, content=content, status="current", adoptedAt=adopted, ) if setting_value is not None: version["settingValue"] = setting_value versions.append(version) decisions.append( entity( did, created, adopted, authorId=PID[author], title=f"Adoption fondatrice — {code}", body=body or ( f"Adoption fondatrice de la clause {code} « {title} » " f"(document {doc_slug}). Import du seed v1 — contenu en vigueur " "dans la communauté Ğ1." ), tags=tags, reversibility="costly", weight="binding", urgent=False, scope={"selfOnly": False, "circleIds": scope_circles, "personIds": []}, route="collective", triageRule="R3", routeOverridden=False, amendsClauseId=cid, protocolId=protocol_id, status="adopted", decidedAt=adopted, stewardIds=[], measurerIds=[], visibility="collective", ) ) return cid def _sources_from_genesis(genesis: dict) -> list[dict]: sources = [] src = genesis.get("source_document", {}) if src: item = {"title": src["title"], "url": src["url"]} if src.get("date"): item["date"] = src["date"] if src.get("version"): item["version"] = src["version"] sources.append(item) if src.get("repo"): sources.append({"title": "Dépôt git officiel (licence Ğ1)", "url": src["repo"]}) for topic in genesis.get("forum_synthesis", []): sources.append({"title": topic["title"], "url": topic["url"]}) tools = genesis.get("référence_tools", {}) tool_labels = { "g1vote_repo": "g1vote-view (dépôt)", "g1vote_live": "g1vote-view (live)", "cesium": "Cesium", "gecko": "Gecko", "pad_source": "Pad source (charte forgeron)", } for key, label in tool_labels.items(): if tools.get(key): sources.append({"title": label, "url": tools[key]}) return sources def _forum_status_notes(genesis: dict) -> str: lines = [] for topic in genesis.get("forum_synthesis", []): line = f"- {topic['title']} : {topic.get('status', '?')}" if topic.get("posts"): line += f" ({topic['posts']} messages)" lines.append(line) return "\n".join(lines) # ── Doc 1 : Acte d'engagement Certification (33 clauses) ───── add_doc( DOC_CERT, "engagement-certification", CERT_DOC_META["title"], "reference", CERT_DOC_META["description"], { "sources": _sources_from_genesis(GEN_CERT), "contributors": [f"{c['name']} — {c['role']}" for c in GEN_CERT["contributors"]], "notes": ( "Déclencheur formule (v1) : " + GEN_CERT["formula_trigger"] + "\n\nÉtat des discussions forum au moment du seed v1 :\n" + _forum_status_notes(GEN_CERT) + f"\n\nDocument v1 : version {CERT_DOC_META['version']}, " f"type {CERT_DOC_META['doc_type']}, statut {CERT_DOC_META['status']}." ), }, T_GENESIS, ) for item in CERT_ITEMS: preset = PRESET_MAP[item["inertia_preset"]] add_clause( doc_slug="engagement-certification", doc_id=DOC_CERT, code=item["position"], section=item["section_tag"], position=item["sort_order"], title=item["title"], inertia=preset, content=item["current_text"], version_label=CERT_DOC_META["version"], created=T_CERT_DRAFT, adopted=T_GENESIS, author="Galuel", protocol_id=PROTO_BY_INERTIA[preset], scope_circles=[C_ROOT], tags=["engagement-certification", item["section_tag"], f"v1:{item['item_type']}"], ) # ── Doc 2 : Acte d'engagement forgeron (59 clauses v2.0.0-fr) ─ _vote_record_v1 = GEN_FORGE["vote_record"] add_doc( DOC_FORGE, "engagement-forgeron", FORGE_DOC_META["title"], "reference", FORGE_DOC_META["description"], { "sources": _sources_from_genesis(GEN_FORGE) + [{"title": "g1vote — suivi du vote 33165", "url": _vote_record_v1["g1vote_url"]}], "voteRecord": { "url": _vote_record_v1["topic_url"], "modeParams": _vote_record_v1["mode_params"], "period": _vote_record_v1["period"], "result": { "for": _vr["pour"], "against": _vr["contre"], "invalid": _vr["nuls_invalides"], "wotSize": _vr["wot_size"], "thresholdRequired": _vr["threshold_required"], "status": _vr["status"], }, }, "contributors": [f"{c['name']} — {c['role']}" for c in GEN_FORGE["contributors"]], "notes": ( f"Critère Smith du vote v2.0.0-fr : {_vr['smith_pour']} forgerons pour / " f"{_vr['smith_contre']} contre. Adresses de vote — pour : " f"{_vote_record_v1['addresses']['pour']} ; contre : " f"{_vote_record_v1['addresses']['contre']}.\n\n" "Actions d'implémentation votées :\n- " + "\n- ".join(GEN_FORGE["implementation_actions"]) + "\n\nDéclencheur formule (v1) : " + GEN_FORGE["formula_trigger"] + "\n\nÉtat des discussions forum au moment du seed v1 :\n" + _forum_status_notes(GEN_FORGE) ), }, T_FORGE_OPEN, T_FORGE_ADOPT, ) for item in FORGE_ITEMS: preset = PRESET_MAP[item["inertia_preset"]] add_clause( doc_slug="engagement-forgeron", doc_id=DOC_FORGE, code=item["position"], section=item["section_tag"], position=item["sort_order"], title=item["title"], inertia=preset, content=item["current_text"], version_label=GEN_FORGE["source_document"]["version"], # '2.0.0-fr' created=T_FORGE_OPEN, adopted=T_FORGE_ADOPT, author="Elois", # v1 : tous les items forgeron votés au protocole Smith (double critère) protocol_id=P_SMITH, scope_circles=[C_ROOT, C_FORGE], tags=["engagement-forgeron", item["section_tag"], f"v1:{item['item_type']}"], ) # ── Doc 3 : Acte d'engagement du Comité Technique (6 clauses) ─ # Contenu ABSENT de seed.py — reconstruit depuis research_duniter_forum.md §3 # (Engagement Comité Tech v2.0.0-fr, vote 2026-02-04 → 2026-03-06, en cours # au moment de la recherche). Documenté dans provenance.notes. COMTECH_CLAUSES = [ { "code": "CT1", "section": "mission", "title": "Mission du Comité Technique", "inertia": "standard", "content": ( "Le Comité Technique protège la souveraineté de la communauté Ğ1 " "sur les mises à jour de la blockchain. Ses missions :\n\n" "- Auditer le code\n" "- Détecter le code malveillant\n" "- Vérifier les fonctionnalités annoncées\n" "- Déployer uniquement les mises à jour servant la communauté" ), }, { "code": "CT2", "section": "engagements", "title": "Respect des règles en vigueur", "inertia": "standard", "content": ( "Chaque membre du Comité Technique s'engage à respecter les règles " "décrites dans la version en vigueur du présent document." ), }, { "code": "CT3", "section": "engagements", "title": "Démission en cas de désaccord", "inertia": "standard", "content": ( "Chaque membre s'engage à démissionner s'il est en désaccord " "avec les règles en vigueur." ), }, { "code": "CT4", "section": "engagements", "title": "Révocation des membres en violation", "inertia": "standard", "content": ( "Chaque membre s'engage à voter pour retirer le mandat des " "collègues qui violent visiblement les directives du présent document." ), }, { "code": "CT5", "section": "formule", "title": "Conditions d'adoption", "inertia": "high", "content": ( "L'adoption d'une modification exige le seuil unani-majoritaire " "de la toile ET le support minimum des membres du comité :\n\n" "```\nvotesPour >= ceil(WotSize^0.1 + (0.5 + (1 - 0.5) " "× (1 - (TotalVotes/WotSize)^0.2)) × TotalVotes)\n```\n" "ET\n" "```\nvotesCoTecPour >= ceil(CoTecSize^0.1)\n```\n\n" "Mode compact : D30M50B.1G.2T.1." ), }, { "code": "CT6", "section": "composition", "title": "Composition du comité", "inertia": "high", "content": ( "L'entrée et la sortie d'un membre nécessitent l'approbation des " "2/3 des membres existants (modifiable uniquement par mise à jour " "runtime avec validation des 2/3)." ), }, ] add_doc( DOC_TECH, "engagement-comite-tech", "Acte d'engagement du Comité Technique", "reference", "Acte d'engagement des membres du Comité Technique Duniter V2 " "(v2.0.0-fr). Protège la souveraineté de la communauté Ğ1 sur les mises " "à jour de la blockchain : audit du code, détection de code malveillant, " "déploiement des seules mises à jour servant la communauté.", { "sources": [ { "title": "Vote : Engagement Comité Tech 2.0.0-fr", "url": "https://forum.monnaie-libre.fr/t/vote-engagement-comite-tech-2-0-0-fr/33293", "date": "2026-02-04", "version": "2.0.0-fr", }, { "title": "g1vote — mode D30M50B.1G.2T.1 (talk 32960)", "url": "https://g1vote-view-237903.pages.duniter.org/", }, ], "notes": ( "Contenu ABSENT du seed v1 (seed.py) — reconstruit depuis la " "synthèse interne research_duniter_forum.md (§3). Le vote " "(2026-02-04 → 2026-03-06) était en cours au moment de la " "recherche : texte intégral non disponible, clauses condensées " "depuis la synthèse. Méthode de vote : 0,01 Ğ1 envoyés aux " "adresses désignées (POUR ou CONTRE)." ), }, T_COMTECH_OPEN, T_COMTECH_ADOPT, ) for i, c in enumerate(COMTECH_CLAUSES): add_clause( doc_slug="engagement-comite-tech", doc_id=DOC_TECH, code=c["code"], section=c["section"], position=i + 1, title=c["title"], inertia=c["inertia"], content=c["content"], version_label="2.0.0-fr", created=T_COMTECH_OPEN, adopted=T_COMTECH_ADOPT, author="Cgeek", protocol_id=P_TECHCOMM, scope_circles=[C_ROOT, C_TECH], tags=["engagement-comite-tech", c["section"]], ) # ── Doc 4 : Pacte duniter-g1 (20 clauses, settingKeys) ─────── _org_v1 = next(o for o in ORGS_V1 if o["slug"] == "duniter-g1") A1_TEXT = ( "Nous, membres de la toile de confiance Ğ1, nous donnons cet outil pour " "décider ensemble sans remettre notre souveraineté à quiconque.\n\n" "**Autonomie** — chacun décide de ce qui ne concerne que lui. La monnaie " "libre garantit à chacun sa part de création monétaire ; notre gouvernance " "garantit à chacun sa part de décision.\n\n" "**Équilibre** — aucun pouvoir sans contrepoids : tout mandat est borné, " "révocable et rend compte ; toute règle porte son inertie et reste " "amendable par ceux qu'elle gouverne.\n\n" "**Liaison** — la confiance circule de personne à personne, comme les " "certifications : nos décisions relient ceux qu'elles concernent, du petit " "cercle à la toile entière.\n\n" "Notre finalité : une toile de confiance vivante où la monnaie et la " "décision appartiennent également à chacun — sacralisée dans ce Pacte, " "jamais immuable." ) PACT_CLAUSES = [ # (code, section, title, inertia, settingKey, settingValue, content) ("A1", "preambule", "Notre finalité", "high", None, None, A1_TEXT), ( "P1", "triage", "Petit cercle", "standard", "triage.smallGroupMax", QUAL_V1["small_group_max"], "Jusqu'à 5 personnes concernées, la voie légère suffit : demander " "l'avis de chacun, écouter, puis décider.", ), ( "P2", "triage", "Grand corps", "standard", "triage.collectiveMin", QUAL_V1["collective_wot_min"], "Au-delà de 50 personnes concernées, la décision passe par le " "protocole des grands corps — pour la toile Ğ1 : le vote binaire " "inertiel, héritage Toile de Confiance.", ), ( "P3", "triage", "Tour de consentement", "standard", "triage.consentMax", 7, "Jusqu'à 7 personnes, un tour d'accord suffit : sans objection " "argumentée maintenue, c'est adopté.", ), ( "P4", "triage", "Fenêtre d'objection", "standard", "triage.objectionWindowHours", 48, "Toute décision sous mandat ou en voie légère reste ouverte à " "l'objection pendant 48 heures avant d'entrer en vigueur.", ), ( "P5", "triage", "Fenêtre d'avis", "standard", "triage.adviceWindowHours", 72, "Quand la voie est l'avis, chacun dispose de 72 heures pour le donner " "avant que l'auteur décide.", ), ( "P6", "triage", "Temps de formulation", "standard", "triage.framingDays", 14, "Quatorze jours pour s'instruire et formuler des contre-propositions " "avant l'ouverture du vote des décisions qui engagent.", ), ( "P7", "triage", "Affluence", "standard", "triage.concernEscalateRatio", 0.5, "Quand la moitié des personnes concernées se déclare, le traitement " "collectif devient obligatoire : élargir le périmètre, ou motiver " "publiquement son maintien.", ), ( "P8", "triage", "Récurrence", "standard", "triage.recurrenceThreshold", 3, "À la troisième décision semblable, l'outil propose de réclamer un " "mandat ou de créer une règle.", ), ( "P9", "triage", "Épreuve du réel", "standard", "triage.reviewDelayDays", 90, "Toute décision structurante ou irréversible est revue au plus tard " "90 jours après son adoption : ça tient, à revoir, ou à révoquer.", ), ( "P10", "triage", "Matière exigée", "standard", "triage.requireEffects", "binding", "Ouvrir un vote collectif sur une décision qui engage exige d'énoncer " "au moins un effet recherché — et une cible mesurable quand elle est " "structurante.", ), ( "M1", "protocoles", "Protocole de consentement", "high", "protocols.consent", P_CONSENT, "Le consentement est notre modalité de base et le repli universel : " "toute clé de protocole non résolue y retombe.", ), ( "M2", "protocoles", "Protocole des grands corps", "high", "protocols.large", P_WOT, "Au-delà du grand corps, la toile Ğ1 vote pour/contre sous la formule " "d'inertie héritée de la Toile de Confiance — l'outil de dernier " "recours des très grands corps.", ), ( "M3", "protocoles", "Protocole nuancé", "high", "protocols.nuanced", P_NUANCED, "Entre le tour de consentement et le grand corps : le vote nuancé à " "6 niveaux — chacun se prononce en nuances, pas en camps.", ), ( "M4", "protocoles", "Réglage collectif", "high", "protocols.parametric", P_PARAMETRIC, "Quand la décision est un nombre, un taux ou un montant : décider au " "curseur — la médiane basse éclaire, le geste du garant cristallise.", ), ( "M5", "protocoles", "Élection sans candidat", "high", "protocols.election", P_ELECTION, "Pour désigner une personne : élection sans candidat, vote blanc " "possible, départage entre ex æquo décidé d'avance.", ), ( "N1", "inertie", "Inertie basse", "max", "protocols.clauseByInertia.low", P_INERTIA_LOW, "Amender une clause d'inertie basse (annexes, recommandations) : " "G = 0,1 — M = 50 %.", ), ( "N2", "inertie", "Inertie standard", "max", "protocols.clauseByInertia.standard", P_WOT, "Amender une clause d'inertie standard (engagements fondamentaux et " "techniques) : G = 0,2 — M = 50 %.", ), ( "N3", "inertie", "Inertie haute", "max", "protocols.clauseByInertia.high", P_INERTIA_HIGH, "Amender une clause d'inertie haute (formule de vote) : " "G = 0,4 — M = 60 %.", ), ( "N4", "inertie", "Inertie très haute — méta-réglage", "max", "protocols.clauseByInertia.max", P_INERTIA_MAX, "Amender le réglage de l'inertie lui-même exige la quasi-unanimité : " "G = 0,6 — M = 66 %. La protection des règles de modification — " "l'héritage N1 des actes Ğ1.", ), ] add_doc( DOC_PACT, "pacte-duniter-g1", "Pacte de la toile Ğ1", "pact", "Notre contrat social — sacralisé, jamais immuable", { "sources": [], "notes": ( f"Import v1 : organisation « {_org_v1['name']} » " f"({_org_v1['org_type']}, transparente) — " f"« {_org_v1['description']} » Seuils importés du protocole de " f"qualification v1 « {QUAL_V1['name']} » " f"(small_group_max={QUAL_V1['small_group_max']}, " f"collective_wot_min={QUAL_V1['collective_wot_min']}, " f"modalités par défaut : {', '.join(QUAL_MODALITIES_V1)})." ), }, T_GENESIS, ) for i, (code, section, title, inertia, key, value, content) in enumerate(PACT_CLAUSES): add_clause( doc_slug="pacte-duniter-g1", doc_id=DOC_PACT, code=code, section=section, position=i + 1, title=title, inertia=inertia, content=content, version_label="1.0.0", created=T_CERT_DRAFT, adopted=T_GENESIS, author="Cgeek", protocol_id=P_CONSENT, scope_circles=[C_ROOT], tags=["pacte-duniter-g1", section], setting_key=key, setting_value=value, ) # ───────────────────────────────────────────────────────────── # Décisions historiques # ───────────────────────────────────────────────────────────── # ── Runtime Upgrade 1100 — adoptée, structurelle, gravée ───── D_RUNTIME = uid("decision:runtime-upgrade-1100") _runtime_decided = "2026-05-20T17:00:00Z" decisions.append( entity( D_RUNTIME, "2026-05-02T09:00:00Z", _runtime_decided, authorId=PID["Cgeek"], title="Runtime Upgrade 1100", body=( RUNTIME_META["description"] + "\n\n" + RUNTIME_META["context"] + "\n\n**Processus (import v1 — titre v1 : « Runtime Upgrade », " "type runtime_upgrade) :**\n" + _steps_md(RUNTIME_STEPS) ), brief={ "context": RUNTIME_META["context"], "sources": [ {"label": "Dépôt Duniter V2", "url": "https://git.duniter.org/nodes/rust/duniter-v2s"}, ], "effects": [ { "label": "Réseau stable après la mise à niveau", "target": "zéro anomalie critique sous 7 jours", "measured": { "note": "Aucune anomalie critique observée sur 7 jours — métriques réseau nominales.", "at": "2026-05-27T17:00:00Z", "byId": PID["Moul"], }, }, { "label": "Nœuds validateurs synchronisés", "target": "100 % des validateurs à jour sous 48 h", "measured": { "note": "Ensemble des validateurs en ligne à jour en 36 h.", "at": "2026-05-22T09:00:00Z", "byId": PID["Tuxmain"], }, }, ], }, resources={ "note": "Coordination des forgerons pour la synchronisation des nœuds — fenêtre de maintenance.", "unit": "heures", }, tags=["runtime", "upgrade", "on-chain"], reversibility="costly", weight="structural", urgent=False, scope={"selfOnly": False, "circleIds": [C_ROOT, C_FORGE, C_TECH], "personIds": []}, route="collective", triageRule="R5", routeOverridden=False, protocolId=P_TECHCOMM, status="adopted", decidedAt=_runtime_decided, review={"dueAt": "2026-08-18T17:00:00Z"}, stewardIds=[PID["Cgeek"]], measurerIds=[PID["Moul"], PID["Tuxmain"]], visibility="collective", engraving={ "sha256": hashlib.sha256( f"runtime-upgrade-1100|{_runtime_decided}".encode() ).hexdigest(), "engravedAt": _runtime_decided, "proofLevel": "local", }, ) ) # ── Évolution Licence Ğ1 v0.4.0 — en formulation, versions proposées ── D_LICENCE = uid("decision:evolution-licence-0.4.0") decisions.append( entity( D_LICENCE, "2026-07-28T09:00:00Z", "2026-08-01T09:00:00Z", authorId=PID["Poka"], title="Évolution Licence G1 v0.4.0", body=( LICENCE_META["description"] + "\n\n**Processus (import v1) :**\n" + _steps_md(LICENCE_STEPS) ), baselineNote="Aujourd'hui : Licence Ğ1 v0.3.0 en vigueur.", brief={ "context": LICENCE_META["context"], "sources": [ { "label": "Préparation licence v0.4.0 (topic 32375)", "url": "https://forum.monnaie-libre.fr/t/preparation-dune-proposition-devolution-de-la-licence-1/32375", }, { "label": "Proposition Charte 1.0, rejetée (topic 31066)", "url": "https://forum.monnaie-libre.fr/t/proposition-charte-1-0/31066", }, ], "effects": [ { "label": "Une licence clarifiée, comprise par les nouveaux certificateurs", "target": "adoption de la v0.4.0 au seuil WoT", } ], }, resources={ "note": "Co-rédaction et animation du vote — temps bénévole des contributeurs.", "unit": "heures", }, tags=["licence", "certification"], reversibility="costly", weight="structural", urgent=False, scope={"selfOnly": False, "circleIds": [C_ROOT], "personIds": []}, route="collective", triageRule="R5", routeOverridden=False, protocolId=P_NUANCED, status="framing", windowEndsAt="2026-08-15T09:00:00Z", stewardIds=[PID["Poka"]], measurerIds=[], visibility="collective", ) ) # Versions proposées (liées aux clauses cert E3 et E7, contre-propositions v0.4.0) versions.append( entity( uid("version:engagement-certification:E3:0.4.0-draft"), "2026-08-05T09:00:00Z", clauseId=uid("clause:engagement-certification:E3"), decisionId=D_LICENCE, versionLabel="0.4.0-draft", content=( "Je me suis assuré de connaître suffisamment la personne qui gère " "cette clé publique : je sais la recontacter par au moins deux " "canaux indépendants, je suis en mesure de repérer un double-compte, " "et je m'engage à signaler tout doute aux experts de la communauté. " "« Avoir vu » quelqu'un ne suffit pas." ), status="proposed", ) ) versions.append( entity( uid("version:engagement-certification:E7:0.4.0-draft"), "2026-08-05T09:00:00Z", clauseId=uid("clause:engagement-certification:E7"), decisionId=D_LICENCE, versionLabel="0.4.0-draft", content=( "J'ai rencontré la personne physiquement (préférable), **OU** j'ai " "vérifié à distance le lien personne / clé publique par au moins " "trois moyens de communication différents et indépendants, dont un " "échange en visio en direct." ), status="proposed", ) ) # ───────────────────────────────────────────────────────────── # Mandats (3) + décisions d'origine, session de vote, votes # ───────────────────────────────────────────────────────────── _m_comtech = next(m for m in MANDATES_V1 if m["mandate_type"] == "techcomm") _m_smith = next(m for m in MANDATES_V1 if m["mandate_type"] == "smith") _m_forum = next(m for m in MANDATES_V1 if m["mandate_type"] == "custom") # ── Mandat 1 : Comité Technique — actif, holder Cgeek, 1 rapport dû ── D_M_COMTECH = uid("decision:mandat-comite-technique") M_COMTECH = uid("mandate:comite-technique") decisions.append( entity( D_M_COMTECH, "2025-08-20T09:00:00Z", "2025-09-10T18:00:00Z", authorId=PID["Elois"], title=_m_comtech["title"], body=( _m_comtech["description"] + "\n\n**Parcours (import v1 — type techcomm) :**\n" + _steps_md(_m_comtech["steps"]) ), tags=["mandat", "comite-technique", "v1:techcomm"], reversibility="costly", weight="binding", urgent=False, scope={"selfOnly": False, "circleIds": [C_ROOT], "personIds": []}, route="collective", triageRule="R5", routeOverridden=False, protocolId=P_NUANCED, createsMandate={ "title": _m_comtech["title"], "domainCircleIds": [C_TECH], "domainTags": ["runtime", "code", "on-chain"], "durationDays": 365, "reportEveryDays": 180, }, status="adopted", decidedAt="2025-09-10T18:00:00Z", stewardIds=[PID["Cgeek"]], measurerIds=[], visibility="collective", ) ) mandates = [ entity( M_COMTECH, "2025-09-15T09:00:00Z", title=_m_comtech["title"], holderId=PID["Cgeek"], originDecisionId=D_M_COMTECH, domain={"circleIds": [C_TECH], "tags": ["runtime", "code", "on-chain"]}, startsAt="2025-09-15T09:00:00Z", endsAt="2026-09-15T09:00:00Z", electorCircleId=C_ROOT, nominationMethod="nuanced-vote", reports=[{"dueAt": "2026-03-15T09:00:00Z"}], # rapport de mi-mandat dû, non remis status="active", ) ] # ── Mandat 2 : Administrateur des Forgerons — proposé, EN vote nuancé ── # v1 : status 'voting' (rotation 2026-Q1) ; la session v2 est positionnée # OUVERTE à la date du seed (2026-07-25 → 2026-08-24) — écart documenté. D_M_SMITH = uid("decision:mandat-admin-forgerons") M_SMITH = uid("mandate:admin-forgerons") S_SMITH = uid("session:mandat-admin-forgerons") decisions.append( entity( D_M_SMITH, "2026-07-10T09:00:00Z", "2026-07-25T09:00:00Z", authorId=PID["Inso"], title=_m_smith["title"], body=( _m_smith["description"] + "\n\n**Parcours (import v1 — type smith, statut v1 : voting) :**\n" + _steps_md(_m_smith["steps"]) ), tags=["mandat", "forgerons", "v1:smith"], reversibility="costly", weight="binding", urgent=False, scope={"selfOnly": False, "circleIds": [C_ROOT, C_FORGE], "personIds": []}, route="collective", triageRule="R5", routeOverridden=False, protocolId=P_NUANCED, createsMandate={ "title": "Administrateur des Forgerons", "domainCircleIds": [C_FORGE], "domainTags": ["forge", "noeuds"], "durationDays": 180, }, status="voting", windowEndsAt="2026-08-24T09:00:00Z", stewardIds=[PID["Inso"]], measurerIds=[], visibility="collective", ) ) mandates.append( entity( M_SMITH, "2026-07-10T09:00:00Z", title=_m_smith["title"], holderId=PID["Moul"], # v1 : mandatee = Moul (Forgeron senior) originDecisionId=D_M_SMITH, domain={"circleIds": [C_FORGE], "tags": ["forge", "noeuds"]}, startsAt="2026-09-01T09:00:00Z", endsAt="2027-03-01T09:00:00Z", electorCircleId=C_FORGE, nominationMethod="nuanced-vote", reports=[], status="proposed", ) ) sessions = [ entity( S_SMITH, "2026-07-25T09:00:00Z", decisionId=D_M_SMITH, protocolId=P_NUANCED, corpusPersonIds=[PID[n] for n in VOTER_NAMES], corpusSize=len(VOTER_NAMES), opensAt="2026-07-25T09:00:00Z", closesAt="2026-08-24T09:00:00Z", status="open", ) ] votes = [ entity( uid("vote:mandat-admin-forgerons:tuxmain"), "2026-07-26T14:00:00Z", sessionId=S_SMITH, voterId=PID["Tuxmain"], value=5, comment="Rôle nécessaire — l'embarquement des nouveaux forgerons traîne depuis des mois.", ), entity( uid("vote:mandat-admin-forgerons:hugo"), "2026-07-27T10:30:00Z", sessionId=S_SMITH, voterId=PID["Hugo"], value=4, ), entity( uid("vote:mandat-admin-forgerons:matograine"), "2026-07-29T19:45:00Z", sessionId=S_SMITH, voterId=PID["Matograine"], value=2, comment="Réservé : je préfère une rotation plus courte, renouvelée à chaque trimestre.", ), ] # ── Mandat 3 : Modération Forum — actif, tags forum/moderation ── # v1 : status 'draft' — la spec Seeds v2 le pose actif (écart documenté). D_M_FORUM = uid("decision:mandat-moderation-forum") M_FORUM = uid("mandate:moderation-forum") decisions.append( entity( D_M_FORUM, "2026-04-10T09:00:00Z", "2026-04-25T18:00:00Z", authorId=PID["Hugo"], title=_m_forum["title"], body=( _m_forum["description"] + "\n\n**Parcours (import v1 — type custom, statut v1 : draft) :**\n" + _steps_md(_m_forum["steps"]) ), tags=["mandat", "forum", "moderation", "v1:custom"], reversibility="easy", weight="binding", urgent=False, scope={"selfOnly": False, "circleIds": [C_ROOT], "personIds": []}, route="collective", triageRule="R5", routeOverridden=False, protocolId=P_ELECTION, createsMandate={ "title": _m_forum["title"], "domainCircleIds": [C_ROOT], "domainTags": ["forum", "moderation"], "durationDays": 365, "reportEveryDays": 180, }, status="adopted", decidedAt="2026-04-25T18:00:00Z", stewardIds=[PID["Tortue"]], measurerIds=[], visibility="collective", ) ) mandates.append( entity( M_FORUM, "2026-05-01T09:00:00Z", title=_m_forum["title"], holderId=PID["Tortue"], originDecisionId=D_M_FORUM, domain={"circleIds": [C_ROOT], "tags": ["forum", "moderation"]}, startsAt="2026-05-01T09:00:00Z", endsAt="2027-05-01T09:00:00Z", electorCircleId=C_ROOT, nominationMethod="election-no-candidate", reports=[{"dueAt": "2026-11-01T09:00:00Z"}], status="active", ) ) # ───────────────────────────────────────────────────────────── # Collectif + Bundle # ───────────────────────────────────────────────────────────── collective = { "id": COLLECTIVE_ID, "slug": "duniter-g1", "name": _org_v1["name"], "color": _org_v1["color"], # '#22c55e' (v1 == spec) "icon": "i-lucide-network", # spec Seeds v2 (v1 : i-lucide-globe, noté) "template": "free-currency", "isTransparent": _org_v1["is_transparent"], "pactDocId": DOC_PACT, "rootCircleId": C_ROOT, "createdAt": T_GENESIS, "updatedAt": EXPORTED_AT, } bundle = { "schemaVersion": 2, "exportedAt": EXPORTED_AT, "collective": collective, "people": people, "circles": circles, "decisions": decisions, "concerns": [], "objections": [], "advices": [], "assents": [], "mandates": mandates, "docs": docs, "clauses": clauses, "versions": versions, "protocols": protocols, "sessions": sessions, "votes": votes, } # ───────────────────────────────────────────────────────────── # Validation interne (invariants) puis écriture # ───────────────────────────────────────────────────────────── def _validate() -> None: ids: set[str] = set() for key in ( "people", "circles", "decisions", "mandates", "docs", "clauses", "versions", "protocols", "sessions", "votes", ): for item in bundle[key]: assert item["id"] not in ids, f"id dupliqué : {item['id']} ({key})" ids.add(item["id"]) assert item["collectiveId"] == COLLECTIVE_ID, f"collectiveId manquant ({key})" assert item["createdAt"] and item["updatedAt"], f"dates manquantes ({key})" decision_ids = {d["id"] for d in decisions} version_ids = {v["id"] for v in versions} protocol_ids = {p["id"] for p in protocols} person_ids = {p["id"] for p in people} circle_ids = {c["id"] for c in circles} doc_ids = {d["id"] for d in docs} for clause in clauses: assert clause["docId"] in doc_ids assert clause["currentVersionId"] in version_ids, f"version manquante : {clause['code']}" for version in versions: assert version["decisionId"] in decision_ids, f"décision manquante : {version['id']}" for decision in decisions: assert decision["authorId"] in person_ids if decision.get("protocolId"): assert decision["protocolId"] in protocol_ids for cid in decision["scope"]["circleIds"]: assert cid in circle_ids for mandate in mandates: assert mandate["holderId"] in person_ids assert mandate["originDecisionId"] in decision_ids assert mandate["electorCircleId"] in circle_ids for session_ in sessions: assert session_["decisionId"] in decision_ids assert session_["protocolId"] in protocol_ids for vote in votes: assert vote["voterId"] in person_ids assert vote["sessionId"] in {s["id"] for s in sessions} # Décision fondatrice pour CHAQUE clause courante by_id_version = {v["id"]: v for v in versions} by_id_decision = {d["id"]: d for d in decisions} for clause in clauses: version = by_id_version[clause["currentVersionId"]] founding = by_id_decision[version["decisionId"]] assert founding["status"] == "adopted", f"fondatrice non adoptée : {clause['code']}" assert founding["title"].startswith("Adoption fondatrice — ") # Invariant consent (importBundle le valide aussi) consent = [p for p in protocols if p["method"] == "consent"] assert len(consent) == 1 # Comptages de non-perte cert = [c for c in clauses if c["docId"] == DOC_CERT] forge = [c for c in clauses if c["docId"] == DOC_FORGE] assert len(cert) == 33 and len(forge) == 59 assert len(people) == 11 assert sum(1 for p in people if p["wotStatus"] == "smith") == 5 assert len(mandates) == 3 assert len(protocols) >= 7 def _dist(doc_id: str) -> dict: out: dict[str, int] = {} for clause in clauses: if clause["docId"] == doc_id: out[clause["inertia"]] = out.get(clause["inertia"], 0) + 1 return dict(sorted(out.items())) def main() -> None: _validate() OUT_PATH.parent.mkdir(parents=True, exist_ok=True) payload = json.dumps(bundle, ensure_ascii=False, indent=2) + "\n" OUT_PATH.write_text(payload, encoding="utf-8") print("=" * 64) print("Export seed v1 → Bundle v2 : duniter-g1.bundle.json") print("=" * 64) print(f"Sortie : {OUT_PATH}") print(f"Taille : {len(payload.encode('utf-8'))} octets") print() print("Diff de comptage seed.py (extrait) → bundle (émis) :") print(f" items Certification : {len(CERT_ITEMS):>3} → {len([c for c in clauses if c['docId'] == DOC_CERT]):>3} clauses {_dist(DOC_CERT)}") print(f" items Forgeron : {len(FORGE_ITEMS):>3} → {len([c for c in clauses if c['docId'] == DOC_FORGE]):>3} clauses {_dist(DOC_FORGE)}") print(f" items Comité Tech : 0 → {len([c for c in clauses if c['docId'] == DOC_TECH]):>3} clauses (research_duniter_forum.md §3)") print(f" clauses Pacte : 0 → {len([c for c in clauses if c['docId'] == DOC_PACT]):>3} (settingKeys v2 ; seuils v1 qualification)") print(f" VOTER_NAMES : {len(VOTER_NAMES):>3} → {len(people):>3} personnes ({sum(1 for p in people if p['wotStatus'] == 'smith')} smiths)") print(f" mandates_data : {len(MANDATES_V1):>3} → {len(mandates):>3} mandats") print(f" VotingProtocols : {len(PROTOCOLS_V1):>3} → {len(protocols):>3} protocoles (7 FormulaConfigs portées)") print(f" décisions : 2 → {len(decisions):>3} (dont {len([d for d in decisions if d['title'].startswith('Adoption fondatrice')])} fondatrices)") print(f" sessions / votes : 3 → {len(sessions):>3} / {len(votes):>3} (3 sessions de démo v1 NON portées — votes factices)") print(f" versions : — → {len(versions):>3} ({len([v for v in versions if v['status'] == 'proposed'])} proposées, licence v0.4.0)") print() print("voteRecord forgeron :", json.dumps( next(d for d in docs if d["id"] == DOC_FORGE)["provenance"]["voteRecord"]["result"], ensure_ascii=False, )) if __name__ == "__main__": main()