Files
decision/backend/scripts/export_seed_bundle.py
T
YvvandClaude Fable 5 f707b5f15d v2 : moteurs purs complets + ambiances + persistance + seed Atelier du Canal
- engine/ : parametric (médiane basse, cristallisation, impact linear-share,
  bimodalité), state (canTransition 9 gardes doctrinales + windowOutcome),
  settings (resolveSettings + replis), triage (R-U→R6, phrases françaises),
  impact (concernés calculés), électionResult (blanc, quorum, égalité sans
  départage machine) — 296 tests vitest verts
- moods.css v2 : Source/Margelle/Nappe/Minuit (champ lexical du puits),
  tokens routes/états, socle borderless, print A4, tampon 井
- data/persistence.ts : IndexedDB local-first, export/import Bundle, lignée
- Seed Atelier du Canal (145 Ko, tous les états de l'UI) + test
- backend/scripts/export_seed_bundle.py (extraction Ğ1, bundle à générer)
- test anti-lexique (marqueur ld-v2)

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

1618 lines
60 KiB
Python
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.
#!/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:<item_type>`).
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()