forked from yvv/decision
- 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>
1618 lines
60 KiB
Python
1618 lines
60 KiB
Python
#!/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()
|