From 53d8752e40285ffba163719d5d5063e7099d0ded Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 11 Aug 2026 05:59:33 +0200 Subject: [PATCH 1/8] =?UTF-8?q?v2=20:=20conception=20ultracode=20+=20moteu?= =?UTF-8?q?r=20de=20formules=20port=C3=A9=20en=20TS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Blueprint définitif (docs/dev/BLUEPRINT-V2.md + JSON) : panel 4 visions + 3 juges, corpus doctrinal (Mon nom est personne, Une économie du don, SejeteralO), 4 critiques d'intégration, 3 vérificateurs, réparations - Corpus d'intention + lectures brutes archivés - Moteur porté Python→TS pur : threshold (inertie WoT, Smith, ComTech), nuancé 6 niveaux, DSL modeParams — 45 tests vitest, parité croisée vérifiée (Forgeron W=7224 T=120 ⇒ 94) - Contrat de domaine v2 (types/domain.ts) + lexique UI (lexicon.ts) - vitest + idb-keyval en dépendances Co-Authored-By: Claude Fable 5 --- docs/dev/BLUEPRINT-V2.md | 1018 ++++++++++++++++++++++ docs/dev/blueprint-v2-final.json | 687 +++++++++++++++ docs/dev/blueprint-v2-panel.json | 511 +++++++++++ docs/dev/corpus-intention.json | 525 +++++++++++ docs/dev/lectures-corpus-brut.json | 971 +++++++++++++++++++++ frontend/app/engine/index.ts | 11 + frontend/app/engine/modeParams.ts | 163 ++++ frontend/app/engine/nuanced.ts | 109 +++ frontend/app/engine/threshold.ts | 110 +++ frontend/app/lexicon.ts | 251 ++++++ frontend/app/types/domain.ts | 521 +++++++++++ frontend/package-lock.json | 326 ++++++- frontend/package.json | 7 +- frontend/tests/engine/modeParams.spec.ts | 127 +++ frontend/tests/engine/nuanced.spec.ts | 118 +++ frontend/tests/engine/threshold.spec.ts | 174 ++++ frontend/vitest.config.ts | 15 + 17 files changed, 5641 insertions(+), 3 deletions(-) create mode 100644 docs/dev/BLUEPRINT-V2.md create mode 100644 docs/dev/blueprint-v2-final.json create mode 100644 docs/dev/blueprint-v2-panel.json create mode 100644 docs/dev/corpus-intention.json create mode 100644 docs/dev/lectures-corpus-brut.json create mode 100644 frontend/app/engine/index.ts create mode 100644 frontend/app/engine/modeParams.ts create mode 100644 frontend/app/engine/nuanced.ts create mode 100644 frontend/app/engine/threshold.ts create mode 100644 frontend/app/lexicon.ts create mode 100644 frontend/app/types/domain.ts create mode 100644 frontend/tests/engine/modeParams.spec.ts create mode 100644 frontend/tests/engine/nuanced.spec.ts create mode 100644 frontend/tests/engine/threshold.spec.ts create mode 100644 frontend/vitest.config.ts diff --git a/docs/dev/BLUEPRINT-V2.md b/docs/dev/BLUEPRINT-V2.md new file mode 100644 index 0000000..42bd949 --- /dev/null +++ b/docs/dev/BLUEPRINT-V2.md @@ -0,0 +1,1018 @@ +# libreDecision v2 — Blueprint définitif + +_Généré par la ronde de conception ultracode du 2026-08-11 : panel de 4 visions + 3 juges, corpus doctrinal (« Mon nom est personne », « Une économie du don », SejeteralO), 4 critiques d'intégration, architecte final, 3 vérificateurs adversariaux, réparations intégrées._ + +## Manifeste + +Personne n'a jamais vécu en démocratie — alors nous la rêvons de jour, et nous la commençons à notre échelle. libreDecision attrape chaque décision en une phrase et propose le chemin le plus court : décide seul et trace-le, décide sous mandat, écoute puis décide, ou décidez ensemble — jamais plus lourd que l'enjeu. Ici on ne délègue pas, on mandate, et on reprend ; on n'est pas consulté, on s'instruit, puis on se prononce ; on ne s'affronte pas en deux camps, on règle des curseurs — et la position de chacun reste visible dans le faisceau, la médiane n'écrase personne, la dernière main est toujours humaine. Les concernés en premier lieu ont voix par construction, chacun voit pourquoi ; lever la main « ça me concerne » est un droit, pas une demande. Chaque décision dit ce qu'elle engage, ce qu'elle cherche à changer, et repasse l'épreuve du réel : rien n'est définitif, toute décision en vigueur garde sa poignée, se tromper est une donnée qu'on consigne. Les règles du jeu elles-mêmes — le Pacte — s'écrivent au fil de l'eau, par ceux qu'elles gouvernent, sous les boussoles Autonomie, Équilibre, Liaison. Du trio d'amis à la toile Ğ1 : le même geste, dix secondes, et l'observatoire qui apprend de nous. La ville change, le puits demeure. + +## Delta vs blueprint v1 (chaque changement justifié par la doctrine) + +### Δ1 + +**Changement.** CollectiveSettings.protocolByRange.inertialBinary renommé 'large' (Id résolu par le Pacte, le nom ne présume plus la méthode). Gabarits blank/informal/association/cooperative/commune/symmetric : 'large' pointe le protocole nuancé 6 niveaux (nuancedMinParticipants/nuancedThresholdPct existants). SEULS free-currency et les seeds Ğ1 pointent le binaire inertiel. R5 >50 hors clause : défaut nuancé ; R3 (clause visée) garde clauseByInertia — héritage Ğ1 (33+59 clauses, D30M50B.1G.2) intact, à sa place. + +**Raison doctrinale.** « Le clivage binaire est un fléau contre la réflexion, un ennemi fatal : il la tue » ; « proscrire le binaire oui/non comme format par défaut » ; le binaire n'est légitime que « remis à sa place dans la boîte à outils » — jamais destination naturelle des grands corpus (le RIC disqualifié : « tir à la corde »). + +### Δ2 + +**Changement.** VoteMethod += 'parametric' (UI « Réglage collectif / décider au curseur »). Decision.paramSpec { params ≤7, constraint 'none'|'sum100', impactAttrKey? } ; ParamDef { key, label métier obligatoire, kind 'slider'|'share', min/max/step, unit?, baseline?, derived? } ; Vote.values?: number[] ; engine/parametric.ts (validateVote, resolveDerived, medianByElement, computeMyImpact, detectBimodality, crystallize). SPECS VERROUILLÉES (réparations) : médiane BASSE par élément (nombre pair ⇒ élément central inférieur — chaque valeur médiane est une valeur effectivement votée, donc conforme au step par construction, testée vitest) ; sum100 ⇒ EXACTEMENT une part 'derived' exigée à la création du paramSpec ; au vote, la géométrie clampe dynamiquement les parts votées pour que le derived reste dans ses bornes ET validateVote rejette sinon ; à la cristallisation, le derived n'est jamais agrégé : il est résolu depuis la médiane des parts votées, et s'il sort de ses bornes ⇒ clamp à la borne + renormalisation proportionnelle des parts non-derived (somme 100 restaurée, spec testée). Dégénérescences : 0 vote ⇒ baseline, jamais d'écran vide. Jamais dans le chemin 10 s : alternative « C'est un réglage » + suggérée quand la phrase contient nombre/%/montant. + +**Raison doctrinale.** « Raisonner curseur / table de mixage » est LE geste central des deux livres ; leçons SejeteralO « voter = régler une forme, pas choisir un camp », « médiane élément par élément » avec l'invariant « le résultat est lui-même une position que chacun aurait pu proposer » — invariant qui EXIGE la médiane basse et la résolution sous contrainte ; « robustesse numérique de série : dégénérescences spécifiées ». + +### Δ3 + +**Changement.** RÉPARATION — Cristallisation = geste humain daté : la clôture paramétrique passe par VoteSession.status 'frozen' à closesAt (votes figés, médiane figée affichée) puis le GARANT (stewardIds, à défaut l'auteur) fait le geste « Cristalliser » → 'closed' + crystallizedById/crystallizedAt, la médiane devient position adoptée (ou rejected si quorum parametricMinParticipants non atteint — constaté au même geste). Relance au Fil du garant s'il tarde. Si detectBimodality positif, le geste affiche en évidence « Deux positions distinctes — cristalliser quand même / scinder / reformuler » : jamais de cristallisation automatique. Consent/nuancé/binaire gardent la clôture automatique (le moteur ne fait qu'y constater un seuil sur un contenu formulé par des humains) ; l'élection clôt automatiquement SAUF égalité (geste requis, cf. delta élection). + +**Raison doctrinale.** Tension du corpus explicitement tranchée : « l'agrégat éclaire une délibération, la cristallisation reste un geste humain distinct et daté » ; chez SejeteralO la publication est distincte de l'agrégation vivante ; un calcul qui produit un contenu adopté sans geste humain = « décision algorithmique », repoussoir absolu. + +### Δ4 + +**Changement.** Matière : Decision.brief { context?, sources?, symptomsVsCauses?, effects: Effect[] } avec Effect { label, target?, measured? }. RÉPARATION — garde restreinte : ≥1 effet recherché exigé (state.ts, framing→voting / draft→voting) UNIQUEMENT pour les sessions collectives de poids binding/structural, gouverné par la clause de Pacte 'triage.requireEffects' ∈ 'none'|'structural'|'binding' (défaut 'binding' ; 'none' seedé dans blank ET informal) ; quand elle s'applique au structural, ≥1 effet doit porter une cible mesurable (target). Le consentement léger (weight light) et le trio n'y sont JAMAIS soumis ; le tunnel ne l'affiche jamais — la matière s'écrit en framing. L'épreuve du réel réutilise effects : tableau « recherché / cible / constaté » au-dessus des 3 boutons, mesures saisies par les measurerIds. + +**Raison doctrinale.** « Il n'est pas consulté, il est instruit » ; « aucun vote sans matière » — mais « jamais plus lourd que l'enjeu » (manifeste) : le collectif règle sa propre exigence (couche méta), et la boucle « acquisition collective des expériences » est fermée par le même champ. + +### Δ5 + +**Changement.** RÉPARATION — Bloc « S'instruire » condensé : en salle de vote et fiche (framing/voting), un RÉSUMÉ COMPACT toujours visible avant les boutons (titre, effets recherchés, « Ce que ça engage ») avec versions+diff, avis/objections et provenance en DÉPLIABLES internes ; le bloc entier se replie automatiquement aux visites suivantes de la même personne (préférence locale par décision) — voter sur un titre nu reste impossible, re-voter reste léger. + +**Raison doctrinale.** « Aucun vote sans matière » concilié avec la leçon SejeteralO « agir d'abord, comprendre ensuite » : pas de mur de scroll mobile à chaque re-vote d'un vote révisable. + +### Δ6 + +**Changement.** Ressources : Decision.resources { note, amount?, unit 'heures'|'€'|'DU'|string } + Decision.measurerIds à côté de stewardIds. Huile/eau : DU et € JAMAIS convertis ni additionnés (Observatoire : lignes par unité). RÉPARATION — localisation de la garde : la ligne « Ce que ça engage » est SUGGÉRÉE-dépliée dans le tunnel (toujours facultative, ne bloque jamais) ; l'exigence (weight != 'light' hors solo) vit UNIQUEMENT dans la garde d'ouverture de fenêtre/session de state.ts, même mécanique que brief.effects — le tunnel n'affiche jamais un champ obligatoire qui ne change pas le chemin. + +**Raison doctrinale.** « Chaque décision implique une allocation de ressources, donc une décision connexe incontournable » (MNP p.89) ; « des équipes pour réaliser et des équipes pour mesurer » ; l'huile et l'eau ne se mélangent jamais ; et la métrique produit « 2 gestes / 10 s » reste intacte pour le cas nominal. + +### Δ7 + +**Changement.** Suppression de toute jauge de confiance sur les mandats. Bloc « Exercice du mandat » : faits comptés jamais synthétisés (« 12 décisions tracées · 1 objection · 2/2 rapports », listes cliquables). Le flux public devient « Feux de la rampe » (« toutes les décisions prises sous ce mandat — contestables »). Rendu seulement, zéro champ. + +**Raison doctrinale.** « On peut évaluer des choses ; mais les personnes ? Alerte rouge » ; « la confiance exclut le contrôle » — une jauge de CONFIANCE sur un pouvoir contrôlé est un oxymore ; « Feux de la rampe » est le mot doctrinal exact. + +### Δ8 + +**Changement.** Protocol.ballot: 'open' | 'secret' (défaut 'open'), gouverné par le Pacte. Mode secret v2 : décomptes agrégés et commentaires anonymisés seuls, étiquette « secret d'affichage — démo » ; crypto réelle avec la synchro (v3, noté au contrat des stores). + +**Raison doctrinale.** « Modalité de vote secret vs ouvert comme paramètre de chaque décision » (leçon fouloscopie) — « l'outil doit savoir faire les deux, et le périmètre en décide » ; même honnêteté d'étiquetage que la gravure. + +### Δ9 + +**Changement.** RÉPARATION — Historique des re-votes jamais opposable : la chaîne supersedesVoteId n'est JAMAIS affichée publiquement sous le nom d'un votant — visible du seul auteur du vote ; même en ballot 'open', seul le DERNIER vote actif d'une personne apparaît sous son nom, et le décompte agrégé est recalculé. Libellé : « Ton vote est remplaçable — ton historique n'est visible que de toi. » Invariant écrit au modèle + test. + +**Raison doctrinale.** Tension « Mémoire vive vs droit à l'erreur » tranchée par le corpus : « archiver les mandats, pardonner les positions ; l'historique des votes révisés n'est jamais opposable à une personne » — sinon contrôle des personnes entre elles, le glas doctrinal. + +### Δ10 + +**Changement.** Gabarit 'blank' « Page blanche — observatoire d'abord » en tête de /creer : Pacte d'une seule clause (« nous consignons nos décisions ; le reste s'écrira au fil de l'eau ») + clause A1 finalité. RÉPARATION — cohérence settings : blank seede AU MINIMUM le protocole « Consentement » ; protocolByRange.consent est OBLIGATOIRE (invariant : tout collectif a un protocole de consentement, validé à l'import), les autres clés optionnelles ; resolveSettings spécifie le REPLI : toute clé non résolue retombe sur consent ; si consent introuvable (bundle corrompu), le triage route 'advice' avec bannière « Aucun protocole — crée-le ou décide sur avis » — jamais un crash. Les 5 gabarits existants réétiquetés « point de départ — tout est amendable par décision ». + +**Raison doctrinale.** « L'observatoire des décisions AVANT de protocoliser » ; contrat social « écrit au fil de l'eau, sans préfigurer » ; « une chorégraphie du don ne se télécharge pas en .ods » — et la robustesse spécifiée (« jamais d'écran vide ») fait partie de la leçon SejeteralO. + +### Δ11 + +**Changement.** Gabarit 'symmetric' « Institution symétrique » : Pacte en transparence totale, TextDoc reference « Déclaration de symétrie », première décision « Déclarer notre symétrie », 2 mandats suggérés Ambassadeur et Négociateur. Données pures templates.ts. + +**Raison doctrinale.** « Tu ne veux pas — Ô Institution — tant pis, je crée une symétrie » (chapitre entier de MNP) ; le poids = le volume des personnes qui l'habitent ; représentants « ambassadeurs et négociateurs, jamais décideurs ». + +### Δ12 + +**Changement.** DecisionRoute += 'record' + Decision.decidedHow. Alternative permanente « Déjà tranché — je le consigne » → adopted immédiat, zéro fenêtre (2 gestes). /decisions/memoire → /decisions/observatoire « L'Observatoire » ; ratio consigné/outillé = maturité ; maturation (famille R6, jamais bloquante) : ≥3 consignations partageant ≥2 tags ⇒ « Cette pratique est mûre — protocolise-la en quelques phrases » pré-remplissant une clause de Pacte. + +**Raison doctrinale.** « Consigner comment elles se prennent dans les faits, avant de protocoliser » puis « traduire en protocoles de quelques phrases quand c'est mûr » — le flux consigner→observer→protocoliser devient le chemin ordinaire ; l'inversion des noms (Mémoire/Observatoire) est corrigée. + +### Δ13 + +**Changement.** chainKind += 'element' + Concern.priority?: 0|1|2|3. « Découper en micro-décisions » sur toute décision structural en framing ; onglet « Éléments » + « Cartographie de clôture » auto-générée. RÉPARATION — clôture du dossier : nouvelle transition framing→closed du parent, gardée par « tous les éléments dans un état terminal » ; jamais automatique : quand la garde est vraie, carte au Fil du garant « Le dossier est complet — clore et publier la cartographie de clôture » ; le geste clôt, la cartographie devient le PV du dossier. Libellé UI : « décision incrémentielle » réservé à CE mode. + +**Raison doctrinale.** La « décision incrémentielle » est le nom PROPOSÉ PAR L'AUTEUR (modèle Taïwan) : « chacun navigue dans les points qui le concernent, pondère ses préférences » ; « cartographie de clôture visible de tous » — et la clôture est un geste humain, cohérent avec la cristallisation. + +### Δ14 + +**Changement.** Decision.baselineNote?: string (« Aujourd'hui : … ») suggéré routes advice/collective (jamais solo). Fiche : « Aujourd'hui → Proposé » ; ParamDef.baseline = marqueur « actuel » ; l'épreuve du réel relit l'écart. Les clauses gardent leur diff (qui EST leur baseline). + +**Raison doctrinale.** Leçon SejeteralO : « la baseline : toujours afficher l'existant — une décision se lit comme un écart au statu quo, mesurable ». + +### Δ15 + +**Changement.** Person.attributes?: Record (déclarés, édités dans /donnees, jamais affichés à autrui ni agrégés) + ParamSpec.impactAttrKey. RÉPARATION — computeMyImpact 'linear-share' SPÉCIFIÉ : ne s'applique QUE si constraint='sum100' ET resources.amount présent ET impactAttrKey présent ; pour chaque part p (derived inclus) : montant(p) = resources.amount × p/100 ; ma quote-part(p) = montant(p) × attr(moi) / Σ attr(déclarants du corpus) ; résultat par curseur + total, calculé aux trois positions (la mienne / médiane / baseline). attr(moi) absent ou Σ=0 ⇒ null (jamais un chiffre inventé — la carte montre alors seulement l'écart collectif en valeurs de curseurs). L'exemple seedé « budget d'ateliers 1 200 € × 'heures/mois' » devient calculable. Étiquette « estimation sur données déclarées ». + +**Raison doctrinale.** Principe produit n°5 « qu'est-ce que ça change pour moi » ; SejeteralO « Mon prix au m³ » et son triptyque ma-position/médiane/référence — version local-first honnête, bornée, et désormais calculable ; le moteur sur données réelles importées reste v3. + +### Δ16 + +**Changement.** Composant Faisceau : nuancé → histogramme des 6 niveaux sous la jauge + commentaires en liste — jamais un simple adopté/rejeté ; paramétrique → strip-plot par curseur (points translucides anonymes, médiane trait épais, baseline marqueur creux). detectBimodality ⇒ bannière non bloquante « Deux positions distinctes se dessinent sur X — scinder la question ou changer de référentiel ? » ouvrant le découpage ou une reformulation. + +**Raison doctrinale.** « La diversité reste visible : le faisceau » ; « la médiane peut masquer une distribution bimodale » ; le « changement de référentiel » doctrinal devient un geste produit. + +### Δ17 + +**Changement.** Garde d'affluence dans state.ts : concernEscalateRatio atteint ⇒ la fenêtre/session NE PEUT PAS se clore sans traitement — élargir d'un cran (1 tap, re-triage journalisé) ou maintenir avec Decision.scopeKeptNote motivée publique, affichée en évidence. + +**Raison doctrinale.** La montée d'échelle est DÉCIDÉE (« on en change quand le peuple le décide ») — l'asymétrie de dérogation appliquée au périmètre, LE lieu doctrinal de la subsidiarité. + +### Δ18 + +**Changement.** Circle.kind?: 'place'|'theme'|'team' — iconographie, groupement, ordre des suggestions seulement, jamais un droit. Hint non bloquant si 'team' > 12. Gabarits typent leurs cercles. + +**Raison doctrinale.** Les 3 vecteurs du périmètre : le topologique gagne une existence déclarative sans machinerie géo ; « au-delà de douze, les complications commencent ». + +### Δ19 + +**Changement.** Collective.lineage?: { sourceSlug, exportedAt, sha256 } posé à l'import d'un bundle étranger (« essaimé de … »). PAS de parentCollectiveId ; pattern v3 documenté : une grappe = un collectif ordinaire dont les membres sont des mandataires d'autres collectifs. + +**Raison doctrinale.** La « scission féconde » exige la mémoire d'origine ; le corpus proscrit la fédération dans le modèle (« représentation, voix unique ») — composition pure. + +### Δ20 + +**Changement.** Clause « A1 — Notre finalité » seedée dans chaque gabarit (texte sous vote permanent, défaut portant les boussoles), affichée sous le nom du collectif ; préambule de Pacte proposé et modifiable portant Autonomie–Équilibre–Liaison. + +**Raison doctrinale.** « Chaque groupe doit se donner une finalité » ; « boussoles proposées, jamais imposées » ; coexistence « sans être concurrents ». + +### Δ21 + +**Changement.** Passerelles hors-écran : (1) CSS print A4 + « Imprimer » sur fiche (PV) et salle (affiche de session) ; (2) « agir en tant que » requalifié « mode atelier » via recordedById?: Id sur Vote/Advice/Objection/Assent (« saisi par X pour Y — atelier du ») ; (3) micro-copy « à trancher en présence ? » sur structural. + +**Raison doctrinale.** « Toute décision numérique doit être praticable sans écran » ; « intermédiation humaine pour les démunis du numérique » ; « plus le numérique prend de place, plus il faut en donner à la relation humaine ». + +### Δ22 + +**Changement.** frontend/app/lexicon.ts : source UNIQUE des libellés français + test vitest anti-lexique (déléguer/délégation, dette, retard, en souffrance, corpus, snapshot, CONTRE, verdict, triage, oligarchie, peuple adulte…). Trois règles de ton : (1) tutoyer la personne dans ses gestes ; (2) actes collectifs à l'infinitif (« Adopter notre Pacte ») ; (3) sujet grammatical des phrases générées = le collectif ou la légitimité, jamais la formule/le système. + +**Raison doctrinale.** « L'auteur voudra retrouver ses mots exacts » ; « politiquement explicable » : c'est la légitimité qui exige, pas les maths — l'algorithme au service, jamais souverain. + +### Δ23 + +**Changement.** Renommages UI (types inchangés) : carte /decider « Le chemin » (explanation seule, code R* au dépliable/PV/journal) ; « Concerné·e en premier lieu / en second lieu » (pastille pleine / main levée) ; chip « Concernés » ; panneau « Périmètre » ; « Mandats — les pouvoirs confiés » ; Fil « À toi de décider » compteur nu ; « Qui vote : 11 personnes — liste arrêtée le 12/08 » (W/T/M/B/G/C à l'Atelier seulement) ; échelle nuancée « Pas du tout · Plutôt non · Réservé · Plutôt oui · Oui · Tout à fait » ; revoyure « L'épreuve du réel » / « Le réel a-t-il suivi ? » / « Ça tient / À revoir / À révoquer » ; « C'est urgent » + badge « Décidé en urgence — le collectif ratifie » ; « Révoquées — ce qu'on en a appris » ; routes à la première personne ; sous-titre du Pacte « Notre contrat social — sacralisé, jamais immuable » ; fenêtre framing « N jours pour s'instruire et formuler des contre-propositions ». + +**Raison doctrinale.** « On ne délègue pas, on abandonne » ; « périmètre » est LE concept pivot ; le registre dette/peur est le couple de l'asservissement ; « le verdict n'est jamais un juge » ; « toute étiquette évacue la moitié de la population ». + +### Δ24 + +**Changement.** Bouton « Explorer » en salle paramétrique : sandbox de la médiane, curseurs libres, dérivés + « Pour moi » recalculés en live, « Partir de la médiane / du statu quo / de ma position », « En faire ma position », étiquette « exploration — ne compte pas ». Rétro-manipulation résultat→contrainte : v3. + +**Raison doctrinale.** Leçon SejeteralO « manipulation de l'agrégat pour explorer » ; « déplacer, évaluer, ajuster, recommencer » ; abaisse le coût psychologique du premier vote. + +### Δ25 + +**Changement.** Dépersonnalisation d'affichage : contenu et diff d'abord, auteur replié (visible au tap) dans le bloc formulation et la salle. Pas de mode anonyme réel en v2 mono-poste. + +**Raison doctrinale.** « On débat du contenu, pas de l'auteur » (@personne) — anti ad-hominem, anti-sauveur. + +### Δ26 + +**Changement.** Désirabilité : tampon 井 incliné -10° « Décidé » (translateY 0.12s) + « C'est décidé — et révisable, comme tout ici » ; /creer se signe « Osons, nous verrons » ; manifeste public sans vocabulaire de livraison, clos sur « La ville change, le puits demeure » ; termes-diagnostics réservés à la page-manifeste. + +**Raison doctrinale.** « Rendre désirable plutôt que convaincre » exige que décider soit beau ; le couple plaisir+rêve. + +### Δ27 + +**Changement.** RÉPARATION — Silence-vaut-accord résolu par l'entité Assent : « Ça me va » est stocké (Assent { decisionId, personId, recordedById? }, append-only, dans le Bundle). Règle unique des fenêtres d'objection : à échéance sans objection ouverte — reversibility 'easy' ⇒ adopted (silence vaut accord) ; costly/irreversible ⇒ adopted SEULEMENT si ≥1 Assent d'un concerné autre que l'auteur, sinon la fenêtre se PROLONGE d'un cran (windowEndsAt + objectionWindowHours, relance au Fil des concernés) — jamais d'adoption par pur silence hors easy. La contradiction v1 est levée, le comportement à échéance est défini. + +**Raison doctrinale.** « Il ne cède pas un consentement, il négocie — chaque virage » : hors du réversible, l'accord est un geste, pas une absence ; et le silence-vaut-accord réservé au réversible était déjà l'engagement du blueprint — désormais tenu mécaniquement. + +### Δ28 + +**Changement.** RÉPARATION — Élection codable : Vote.choicePersonId?: Id (invariant : EXACTEMENT un de value / values / choicePersonId selon la méthode, validé par le store). Vote déposé sans choicePersonId = blanc (compte pour la participation, pas pour la désignation). engine/threshold.ts += electionResult(votes, corpus) : quorum optionnel electionMinParticipants (FormulaParams) sinon rejected ; désignation à la PLURALITÉ simple des derniers votes actifs ; ÉGALITÉ ⇒ résultat 'tie' avec exAequoIds — l'outil ne départage JAMAIS : la clôture propose une session de départage chaînée restreinte aux ex æquo (pré-remplie), ou le tirage au sort entre ex æquo si le protocole du Pacte l'a prévu d'avance. Salle élection : désignation par avatar, bouton « Vote blanc », règle affichée en une phrase. + +**Raison doctrinale.** L'élection « remise à sa place dans la boîte à outils » doit être réellement outillée pour les mandats ; « l'outil représente, agrège et éclaire — il ne décide jamais » : une égalité n'est pas tranchée par un algorithme, et le tirage au sort n'est légitime que décidé d'avance par le Pacte. + +### Δ29 + +**Changement.** RÉPARATION — canTransition étendu : engine/state.ts expose canTransition(decision, to, ctx: { concerns: Concern[], settings: CollectiveSettings, session?: VoteSession, children?: Decision[], assents?: Assent[], objections?: Objection[] }) — les gardes d'affluence (|déclarés| vs |calculés|), de matière (requireEffects), de ressources, de frontière (windowSuspendedAt), d'Assent et de clôture de dossier (children terminaux) ont leurs entrées dans la signature ; le contexte est assemblé par le store, la fonction reste pure et testable. + +**Raison doctrinale.** Machine à états UNIQUE réellement implémentable : toute mutation passe par canTransition, et les gardes doctrinales (subsidiarité, instruction, consentement) sont du code testé, pas des intentions. + +### Δ30 + +**Changement.** Verdict enrichi : parametricHint (nombre/%/montant détecté), alternatives permanentes « C'est un réglage — décidez au curseur », « Déjà tranché — je le consigne », binaire avec son coût (« deux camps, un perdant ») ; TriageContext.similarRecordedCount ; suggestion.kind += 'protocolize'. + +**Raison doctrinale.** « L'outil représente, agrège et éclaire — il ne décide jamais » : les modalités doctrinales sont proposées, jamais imposées. + +## Modèle de domaine + +```ts +// ───────────────────────────────────────────────────────────── +// libreDecision v2 — noyau de domaine DÉFINITIF (frontend/app/types/domain.ts) +// Local-first, prêt-synchro. Identifiants anglais, UI française via app/lexicon.ts. +// INVARIANT 1 : LA DÉCISION EST L'OBJET PIVOT — mandats, clauses, révocations, +// ratifications, éléments de dossier en sont des spécialisations chaînées. +// INVARIANT 2 : l'unification est un fait d'architecture, JAMAIS un vocabulaire d'UI. +// INVARIANT 3 : aucun agrégat/score sur une personne, nulle part — on évalue des choses. +// INVARIANT 4 : aucun calcul ne produit un contenu adopté sans geste humain daté +// (cristallisation paramétrique, départage d'élection, clôture de dossier). +// ───────────────────────────────────────────────────────────── + +type Id = string // UUIDv7 — triable par temps, prêt pour la synchro +type ISODate = string // ISO 8601 UTC +type Json = string | number | boolean | null | Json[] | { [k: string]: Json } + +// Toute entité : scoping collectif + horloge de synchro + soft-delete. +// Prêt-synchro : jamais de suppression physique (archivedAt) ; +// Vote/Advice/Objection/Assent append-only (merge trivial) ; le reste LWW par updatedAt. +interface Entity { + id: Id + collectiveId: Id + createdAt: ISODate + updatedAt: ISODate + archivedAt?: ISODate +} + +// ── Le tenant ──────────────────────────────────────────────── +type CollectiveTemplate = + | 'blank' // « Page blanche — observatoire d'abord » : Pacte 1 clause + A1, protocole Consentement SEUL + | 'informal' | 'association' | 'cooperative' | 'commune' + | 'free-currency' // héritage Ğ1 — SEUL gabarit où protocolByRange.large = binaire inertiel + | 'symmetric' // « Institution symétrique » — le geste fondateur militant (MNP) + +interface Collective { + id: Id + slug: string + name: string + color: string // hex accent + icon: string // i-lucide-* + template: CollectiveTemplate + isTransparent: boolean + pactDocId: Id // TextDoc role='pact' — les réglages EN document voté + rootCircleId: Id + lineage?: { // essaimage : posé auto à l'import d'un bundle d'un autre collectif + sourceSlug: string; exportedAt: ISODate; sha256: string + } // PAS de parentCollectiveId — la fédération n'est jamais une table + createdAt: ISODate + updatedAt: ISODate +} + +// ── Les gens ───────────────────────────────────────────────── +interface Person extends Entity { + displayName: string + isMe: boolean // profil local (v2 mono-poste ; « mode atelier » pour la saisie en présence) + attributes?: Record // attributs DÉCLARÉS par la personne (clés libres : 'heures/mois'…), + // édités dans /donnees — servent UNIQUEMENT la carte « Pour moi » ; + // jamais affichés à autrui, jamais agrégés (invariant 3) + duniterAddress?: string // SS58 — réservé auth Duniter (hors scope v2) + wotStatus?: 'member' | 'smith' +} + +interface Circle extends Entity { + name: string + purpose: string + kind?: 'place' | 'theme' | 'team' // lieu / thème / équipe — iconographie, groupement des sélecteurs, + // ordre des suggestions SEULEMENT, jamais un droit ; + // hint non bloquant si 'team' > 12 (« au-delà de douze… ») + memberIds: Id[] // v2 : liste nominative = SEULE règle d'appartenance (auditable) + parentCircleId?: Id // emboîtement (couches d'oignon) — racine = rootCircle + domains: string[] // tags — SUGGESTION/découverte uniquement, jamais un droit de vote +} +// Corpus d'une décision = union CALCULABLE : membres des cercles du scope +// ∪ personnes nommées ∪ titulaires des mandats dont le domaine intersecte. +// Chaque inclusion garde sa raison (Concern.reason) — auditable en un tap. + +// ── LA DÉCISION (pivot) ────────────────────────────────────── +type Reversibility = 'easy' | 'costly' | 'irreversible' +type Weight = 'light' | 'binding' | 'structural' + +type DecisionRoute = + | 'solo' // « Je décide » (trace facultative hors mandat) + | 'mandate' // « Je décide, sous mandat » (trace obligatoire + fenêtre d'objection) + | 'transmit' // « Je transmets » — le mandat d'un autre couvre + | 'advice' // « J'écoute, puis je décide » + | 'collective' // « Nous décidons » (modalité du protocole) + | 'record' // « Déjà tranché — je le consigne » : l'observatoire en un geste, adopted immédiat + +type DecisionStatus = + | 'draft' | 'advice' | 'objection' | 'framing' | 'voting' + | 'adopted' | 'rejected' | 'revoked' | 'transmitted' | 'closed' + +type TriageRule = 'R-U' | 'R0a' | 'R0b' | 'R0c' | 'R2' | 'R3' | 'R4' | 'R5' | 'R6' + +// La matière à réflexion — « il n'est pas consulté, il est instruit » +interface Effect { + label: string // effet recherché, une ligne + target?: string // cible mesurable optionnelle (« ≤ 400 € », « +5 membres ») + measured?: { note: string; at: ISODate; byId: Id } // constaté — saisi par les measurerIds à la revoyure +} +interface Brief { + context?: string + sources?: { label: string; url: string }[] + symptomsVsCauses?: string // qualification doctrinale du sujet + effects: Effect[] // GARDE (state.ts, ouverture de session collective UNIQUEMENT) : + // exigé si weight ∈ requireEffects du Pacte ('binding' couvre binding+structural) ; + // structural sous garde ⇒ ≥1 effet avec target. JAMAIS pour weight 'light', + // JAMAIS dans le tunnel, JAMAIS en solo/record. +} + +// La décision connexe de ressources — « chaque décision implique une allocation » +interface Resources { + note: string // « Ce que ça engage » en une phrase + amount?: number + unit?: 'heures' | '€' | 'DU' | string // huile/eau : unités JAMAIS converties ni additionnées entre elles +} + +// Le réglage collectif — « raisonner curseur / table de mixage » +type ParamKind = 'slider' | 'share' // 'curve' réservé v3 (SejeteralO complet) +interface ParamDef { + key: string + label: string // libellé métier OBLIGATOIRE — jamais de a, b, c bruts + kind: ParamKind + min: number; max: number; step: number + unit?: string + baseline?: number // statu quo — marqueur creux toujours affiché + derived?: boolean // résolu par la contrainte, jamais voté — étiquette « calculé » +} +interface ParamSpec { + params: ParamDef[] // ≤ 7 (leçon SejeteralO : petit espace manipulable) + constraint: 'none' | 'sum100' // sum100 : les 'share' totalisent 100 ; EXACTEMENT UNE part 'derived' + // exigée (validé à la création du spec) — la variable d'absorption ; + // résolue linéairement (100 − Σ autres) + impactAttrKey?: string // clé de Person.attributes → carte « Pour moi » ('linear-share') +} + +interface Decision extends Entity { + authorId: Id + title: string // LA phrase de capture — seul champ obligatoire + body?: string // progressive disclosure + brief?: Brief // matière — cf. garde d'ouverture ci-dessus + baselineNote?: string // « Aujourd'hui : … » — suggéré routes advice/collective, jamais solo + resources?: Resources // SUGGÉRÉ-déplié au tunnel (jamais bloquant) ; EXIGÉ à la garde + // d'ouverture de fenêtre/session (state.ts) si weight != 'light' hors solo + tags: string[] // découverte + récurrence — JAMAIS un corpus + + // — verdict de triage (pré-rempli par le moteur, ajustable) — + reversibility: Reversibility + weight: Weight + urgent: boolean + scope: { + selfOnly: boolean + circleIds: Id[] // plancher : élargir libre, JAMAIS restreindre sous le calcul + personIds: Id[] + } + route: DecisionRoute + triageRule: TriageRule // relégué au dépliable « pourquoi ? », PV, journal — jamais en carte + routeOverridden: boolean + overrideNote?: string // OBLIGATOIRE si allègement (asymétrie de dérogation) + scopeKeptNote?: string // affluence atteinte + périmètre maintenu ⇒ note motivée PUBLIQUE obligatoire + + // — spécialisations chaînées (le pliage génératif) — + underMandateId?: Id // décidée SOUS ce mandat → feux de la rampe du mandat + createsMandate?: MandateDraft + amendsClauseId?: Id + protocolId?: Id // résolu par le Pacte, ajustable en alourdissant + paramSpec?: ParamSpec // si modalité paramétrique (« Réglage collectif ») + parentDecisionId?: Id + chainKind?: 'ratification' | 'revision' | 'revocation' | 'element' + // 'element' : micro-décision d'un dossier découpé (« décision incrémentielle »). + // Parent d'éléments : transition framing→closed gardée par + // « tous les enfants element en état terminal » — GESTE du garant + // (carte au Fil « Le dossier est complet — clore »), jamais automatique ; + // la cartographie de clôture devient le PV du dossier. + decidedHow?: string // route 'record' : « comment ça s'est décidé », une phrase + + // — temps & fenêtres — + status: DecisionStatus + windowEndsAt?: ISODate + windowSuspendedAt?: ISODate // objection de frontière → compte à rebours SUSPENDU (visible) + decidedAt?: ISODate + review?: Review // obligatoire si irreversible || structural + sunsetAt?: ISODate + stewardIds: Id[] // garants — RÉALISENT (et cristallisent le paramétrique, clôturent le dossier) + measurerIds: Id[] // mesureurs — CONSTATENT (saisissent Effect.measured) : la paire doctrinale + + // — preuve & visibilité — + visibility: 'private' | 'scope' | 'collective' // anti-flicage : trace maîtrisée hors mandat + engraving?: Engraving +} + +interface Review { + dueAt: ISODate + verdict?: 'confirmed' | 'revise' | 'revoke' // UI : « Ça tient / À revoir / À révoquer » + note?: string + decidedAt?: ISODate +} + +interface Engraving { + sha256: string + engravedAt: ISODate + proofLevel: 'local' // v2 : UNIQUEMENT 'local' — « empreinte locale — démo » + ipfsCid?: string // réservé v3 — jamais affiché tant que non réel + chainRef?: string +} + +// ── Inclusion & fenêtres ───────────────────────────────────── +interface Concern extends Entity { + decisionId: Id + personId: Id + origin: 'computed' | 'declared' // UI : « concerné·e en premier lieu » / « en second lieu » + reason: string // « membre du cercle Forgerons » — affichée au tap + declaredNote?: string + beforeSnapshot: boolean // avant liste arrêtée ⇒ vote ; après ⇒ voix consultative + priority?: 0 | 1 | 2 | 3 // « pondère tes enjeux » — par élément d'un dossier ; + // nourrit la cartographie de clôture, jamais le droit de vote +} + +interface Assent extends Entity { // « Ça me va » STOCKÉ — l'accord explicite des fenêtres d'objection + decisionId: Id + personId: Id + recordedById?: Id // mode atelier +} +// RÈGLE DES FENÊTRES D'OBJECTION (state.ts) : à échéance sans objection ouverte — +// easy ⇒ adopted (silence vaut accord) ; costly/irreversible ⇒ adopted SEULEMENT si +// ≥1 Assent d'un concerné ≠ auteur, sinon PROLONGATION d'un cran (+objectionWindowHours, +// relance au Fil) — jamais d'adoption par pur silence hors easy. + +interface Objection extends Entity { + decisionId: Id + personId: Id + kind: 'content' | 'boundary' // boundary SUSPEND la fenêtre — la frontière prime sur le fond + argument: string + status: 'open' | 'withdrawn' | 'integrated' | 'escalated' + resolutionNote?: string + recordedById?: Id // mode atelier : « saisi par X pour Y — atelier du » +} + +interface Advice extends Entity { + decisionId: Id + personId: Id + position: 'favorable' | 'reserved' | 'unfavorable' + note?: string + recordedById?: Id // mode atelier +} + +// ── Le pouvoir ─────────────────────────────────────────────── +interface Mandate extends Entity { + title: string + holderId: Id + originDecisionId: Id // le mandat EST né d'une décision — cliquable + domain: { circleIds: Id[]; tags: string[] } + startsAt: ISODate + endsAt: ISODate // toujours borné + electorCircleId: Id // corpus d'élection = corpus de révocation + nominationMethod: 'ratified-self' | 'election-no-candidate' | 'nuanced-vote' | 'consent' | 'draw' | 'rotation' + reports: MandateReport[] + status: 'proposed' | 'active' | 'expired' | 'revoked' + // AUCUN agrégat/jauge : la fiche affiche des faits comptés (« Exercice du mandat »), + // jamais synthétisés ; le flux public = « Feux de la rampe » +} +interface MandateReport { dueAt: ISODate; deliveredAt?: ISODate; content?: string } +interface MandateDraft { + title: string + domainCircleIds: Id[] + domainTags: string[] + durationDays: number + reportEveryDays?: number +} + +// ── Les textes ─────────────────────────────────────────────── +interface TextDoc extends Entity { + slug: string + title: string + role: 'pact' | 'reference' + description: string // Pacte seedé : « Notre contrat social — sacralisé, jamais immuable » + provenance?: Provenance +} +interface Provenance { + sources: { title: string; url: string; date?: string; version?: string }[] + voteRecord?: { + url: string; modeParams: string; period: string + result: { for: number; against: number; invalid?: number; wotSize: number; thresholdRequired: number; status: string } + } + contributors?: string[] + notes?: string // typos des textes votés : SIGNALÉES, jamais corrigées sans vote +} + +type InertiaPreset = 'low' | 'standard' | 'high' | 'max' +// Câblage RÉEL : low={M:50,G:0.1} standard={M:50,G:0.2} high={M:60,G:0.4} max={M:66,G:0.6} (B=0.1, C=0) + +interface Clause extends Entity { + docId: Id + section: string + position: number + code: string + title: string + inertia: InertiaPreset + currentVersionId?: Id + settingKey?: string // clauses du Pacte : ex. 'triage.smallGroupMax' +} + +interface ClauseVersion extends Entity { + clauseId: Id + decisionId: Id // TOUTE version naît d'une décision (y compris fondatrice seedée) + versionLabel: string + content: string + settingValue?: Json + status: 'current' | 'proposed' | 'superseded' | 'rejected' + adoptedAt?: ISODate +} + +// ── Modalités & vote ───────────────────────────────────────── +type VoteMethod = 'consent' | 'binary' | 'nuanced' | 'parametric' | 'election' +// UI : consentement · pour/contre (outil de dernier recours) · nuancé · +// « Réglage collectif » (au curseur) · élection + +interface Protocol extends Entity { + name: string + method: VoteMethod + description: string + durationDays: number + ballot: 'open' | 'secret' // décidé par le Pacte — défaut 'open' (feux de la rampe). + // v2 mono-poste : « secret d'affichage — démo » (décomptes agrégés + // et commentaires anonymisés seuls) ; crypto réelle avec la synchro (v3) + formula: FormulaParams + modeParams: string // DSL DÉRIVÉ affiché : 'D30M50B.1G.2S.1' — jamais édité à la main + pactClauseId?: Id // référencé par le Pacte ⇒ le modifier = amender le Pacte +} +interface FormulaParams { + majorityPct: number // M + baseExponent: number // B (défaut 0.1) + gradientExponent: number // G — l'inertie + constantBase: number // C (défaut 0) + smithExponent?: number + techcommExponent?: number + nuancedMinParticipants?: number + nuancedThresholdPct?: number + parametricMinParticipants?: number // quorum du réglage collectif — non atteint ⇒ rejected au geste + electionMinParticipants?: number // quorum de l'élection — non atteint ⇒ rejected + tieBreak?: 'runoff' | 'draw' // départage d'élection DÉCIDÉ D'AVANCE par le Pacte + // (défaut 'runoff' : session de départage chaînée entre ex æquo ; + // 'draw' : tirage au sort entre ex æquo — jamais choisi par le moteur) +} + +interface VoteSession extends Entity { + decisionId: Id + protocolId: Id + corpusPersonIds: Id[] // SNAPSHOT à l'ouverture — UI : « liste arrêtée le … » + corpusSize: number // W — plus jamais 0 (lettre réservée à l'Atelier) + opensAt: ISODate + closesAt: ISODate + status: 'open' | 'frozen' | 'closed' + // 'frozen' : PARAMÉTRIQUE uniquement — à closesAt les votes sont figés, la médiane figée est affichée, + // et la session ATTEND le geste humain du garant (relance au Fil s'il tarde). + crystallizedById?: Id // le geste « Cristalliser » : qui, quand — daté et signé + crystallizedAt?: ISODate + outcome?: 'adopted' | 'rejected' | 'tie' + // 'tie' : élection à égalité — l'outil ne départage JAMAIS : session de départage chaînée + // (restreinte aux ex æquo) ou tirage au sort SI le protocole l'a prévu (tieBreak). + // Consent/nuancé/binaire : open→closed automatique à closesAt (le moteur constate un seuil + // sur un contenu formulé par des humains). Paramétrique : open→frozen→closed (geste). + // INVARIANT : tallies, histogrammes ET médianes TOUJOURS recalculés depuis Vote[] — jamais dénormalisés. +} + +interface Vote extends Entity { + sessionId: Id + voterId: Id + value?: 'for' | 'against' | 0 | 1 | 2 | 3 | 4 | 5 + // nuancé UI : 0 Pas du tout · 1 Plutôt non · 2 Réservé · 3 Plutôt oui · 4 Oui · 5 Tout à fait + values?: number[] // paramétrique — ordre de paramSpec.params HORS derived + choicePersonId?: Id // élection — désignation ; vote déposé sans choicePersonId = BLANC + // (compte pour la participation, pas pour la désignation) + // INVARIANT : EXACTEMENT UN de value / values / choicePersonId selon la méthode (validé par le store) — + // sauf blanc d'élection (aucun des trois, méthode 'election' seulement) + comment?: string // OBLIGATOIRE si négatif (against, 0, 1) + supersedesVoteId?: Id // re-vote audité — PRIVACY : la chaîne n'est JAMAIS affichée + // publiquement sous le nom d'un votant ; visible du SEUL auteur ; + // même en ballot 'open', seul le DERNIER vote actif apparaît sous un nom, + // le décompte agrégé est recalculé (« pardonner les positions ») + recordedById?: Id // mode atelier +} + +// ── Réglages résolus du Pacte (fonction pure — PAS une table settings) ── +interface CollectiveSettings { // resolveSettings(pactClauses, versions) + triage: { + smallGroupMax: number // défaut 5 + collectiveMin: number // défaut 50 + consentMax: number // défaut 7 + objectionWindowHours: number // défaut 48 + adviceWindowHours: number // défaut 72 + framingDays: number // défaut 14 — « s'instruire et formuler des contre-propositions » + concernEscalateRatio: number // défaut 0.5 — atteint ⇒ traitement OBLIGATOIRE (garde state.ts) + recurrenceThreshold: number // défaut 3 + reviewDelayDays: number // défaut 90 + requireEffects: 'none' | 'structural' | 'binding' + // clé 'triage.requireEffects' — gouverne la garde matière des sessions COLLECTIVES : + // 'binding' = binding ET structural (défaut association/cooperative/commune/free-currency/symmetric) ; + // 'structural' = structural seul ; 'none' seedé dans blank et informal. + // Sous garde, structural ⇒ ≥1 effet avec cible mesurable (target). + } + protocolByRange: { + consent: Id // OBLIGATOIRE — invariant : tout collectif a un protocole Consentement + // (tout gabarit le seede, l'import le valide) + nuanced?: Id + large?: Id // ex-inertialBinary : le nom ne présume plus la méthode. + // Gabarits standard → nuancé ; free-currency → binaire inertiel + parametric?: Id // « Réglage collectif » + election?: Id + clauseByInertia?: Record // l'héritage Ğ1 vit ici, intact + } + // REPLI SPÉCIFIÉ (resolveSettings) : toute clé optionnelle non résolue retombe sur consent ; + // consent introuvable (bundle corrompu) ⇒ le triage route 'advice' avec bannière + // « Aucun protocole — crée-le ou décide sur avis » — JAMAIS un crash. +} + +// ── Moteurs purs (frontend/app/engine/ — implémentation UNIQUE, vitest) ── +// threshold.ts : wotThreshold(W,T,{M,B,G,C}) — port exact de threshold.py (ceil) ; +// + smithThreshold, techcommThreshold, nuancedResult (histogramme 6 niveaux exposé) ; +// + electionResult(votes, corpus, formula) → { winnerId } | { outcome:'tie', exAequoIds } +// | { outcome:'rejected' } — pluralité simple des derniers votes actifs, blancs comptés +// en participation, quorum electionMinParticipants, JAMAIS de départage moteur. +// SPEC EXÉCUTABLE : cas Forgeron W=7224 T=120 M50 B.1 G.2 ⇒ 94. +// parametric.ts : validateVote(spec, values) — bornes/step/clamp dynamique sum100, sanitize NaN/inf ; +// resolveDerived(spec, values) — sum100 linéaire (100 − Σ autres), étiquette « calculé » ; +// medianByElement(votes) — MÉDIANE BASSE par curseur (pair ⇒ élément central inférieur : +// chaque valeur médiane est une valeur effectivement votée ⇒ step respecté par +// construction — « une position que chacun aurait pu proposer », testé) ; +// crystallize(spec, votes) — médiane basse des parts votées PUIS resolveDerived ; +// derived hors bornes ⇒ clamp à la borne + renormalisation proportionnelle des parts +// non-derived (somme 100 restaurée) — spec testée ; +// computeMyImpact(spec, resources, values, myAttr, corpusAttrs) — 'linear-share' : +// UNIQUEMENT si constraint='sum100' && resources.amount && impactAttrKey ; +// montant(p) = amount × p/100 ; maPart(p) = montant(p) × myAttr / Σ corpusAttrs ; +// par curseur + total, aux trois positions (mienne/médiane/baseline) ; +// myAttr absent ou Σ=0 ⇒ null — JAMAIS un chiffre inventé ; +// detectBimodality(values[]) — plus grand écart entre valeurs triées vs dispersion, +// jamais bloquant. Dégénérescences : 0 vote ⇒ baseline, jamais d'écran vide. +// triage.ts : triage(input, ctx, settings) → Verdict (règles ci-dessous) +// modeParams.ts : parse/format du DSL +// settings.ts : resolveSettings(clauses, versions) → CollectiveSettings (défauts + replis spécifiés) +// state.ts : canTransition(decision, to, ctx: { concerns: Concern[], settings: CollectiveSettings, +// session?: VoteSession, children?: Decision[], assents?: Assent[], +// objections?: Objection[] }) — machine à états UNIQUE, gardes : +// windowSuspendedAt (frontière) ; affluence (ratio ⇒ élargir ou scopeKeptNote) ; +// matière (requireEffects) ; ressources (weight != light hors solo) ; +// Assent (fenêtres non-easy) ; dossier (framing→closed si children terminaux, geste) ; +// cristallisation (paramétrique : closed exige crystallizedById). +// impact.ts : computeConcerned(scope, circles, mandates) → { personId, reason }[] +// app/lexicon.ts : source UNIQUE des libellés français + test vitest anti-lexique +// (déléguer, délégation, dette, retard, en souffrance, corpus, snapshot, +// CONTRE, verdict, triage, oligarchie, peuple adulte…). +// Règles de ton : (1) tutoyer la personne dans ses gestes ; +// (2) actes collectifs à l'infinitif (« Adopter notre Pacte ») ; +// (3) sujet des phrases générées = personnes/collectif, JAMAIS la formule. + +interface TriageInput { + title: string; tags: string[] + scope: Decision['scope'] + reversibility: Reversibility + weight: Weight + urgent: boolean + amendsClauseId?: Id +} +interface TriageContext { // assemblé par le store, consommé pur — testable sans I/O + myActiveMandates: Mandate[] + otherActiveMandates: Mandate[] + matchingClauses: Clause[] // index Q0 « déjà décidé ? » — le MÊME index que Cmd+K + similarRecentCount: number // décisions adoptées partageant ≥2 tags sur 90 j + similarRecordedCount: number // consignations 'record' partageant ≥2 tags — maturation + computedConcernedIds: Id[] +} +interface Verdict { + route: DecisionRoute + rule: TriageRule + explanation: string // UNE phrase française — affichée SEULE, sans code de règle + protocolId?: Id + windowHours?: number + framingDays?: number + reviewRequired: boolean + engravingSuggested: boolean + conservatoryChain?: boolean + parametricHint?: boolean // nombre/%/montant détecté ⇒ « C'est un réglage » mis en avant + alternatives: { route: DecisionRoute; label: string; cost: string }[] + // toujours présentes : « C'est un réglage — décidez au curseur » (parametric) ; + // « Déjà tranché — je le consigne » (record) ; le binaire avec son coût (« deux camps, un perdant ») + suggestion?: { kind: 'claim-mandate' | 'create-rule' | 'protocolize'; prefill: Json } +} + +// ── Persistance locale & synchro future ────────────────────── +interface Bundle { // export/import ET seeds — le MÊME chemin de code + schemaVersion: 2 + exportedAt: ISODate + collective: Collective + people: Person[]; circles: Circle[] + decisions: Decision[]; concerns: Concern[]; objections: Objection[]; advices: Advice[] + assents: Assent[] + mandates: Mandate[] + docs: TextDoc[]; clauses: Clause[]; versions: ClauseVersion[] + protocols: Protocol[]; sessions: VoteSession[]; votes: Vote[] +} +// data/persistence.ts : IndexedDB (idb-keyval), clé `ld2:`, débounce 500 ms. +// importBundle() valide protocolByRange.consent + pose Collective.lineage si bundle étranger. +// Prêt-synchro : UUIDv7, updatedAt (LWW), archivedAt, Vote/Advice/Objection/Assent append-only. +// Contrat des stores Pinia inchangé — l'adaptateur FastAPI futur ne change AUCUN écran. +// NOTES contrat : ballot 'secret' devra devenir cryptographique côté serveur (v3) ; +// la privacy des chaînes supersedesVoteId (visibles du seul auteur) est un invariant du contrat. +``` + +## Cycle de vie + +ÉMERGENCE — la capture : une phrase dans « Qu'est-ce que tu décides ? » (Fil ou FAB). Crée Decision{status:'draft'}. Dix secondes visées, aucun autre champ obligatoire. Alternative permanente : « Déjà tranché — je le consigne » (route 'record') → adopted immédiat avec decidedHow, zéro fenêtre — l'observatoire en un geste, 2 gestes comme le solo. + +TRIAGE — le chemin : triage() pur recalcule à chaque geste ; valider fait passer draft → l'état de départ de la route. Toujours expliqué (une phrase française, jamais de code), toujours contournable (alourdir libre 1 tap ; alléger = note obligatoire + fenêtre d'objection imposée — asymétrie journalisée). La ligne « Ce que ça engage » se DÉPLIE en suggestion si weight binding/structural hors solo mais ne bloque JAMAIS le tunnel ; « Aujourd'hui : … » suggéré sur advice/collective. + +FORMULATION (route collective, poids binding/structural) : status 'framing' — « N jours pour s'instruire et formuler des contre-propositions » (framingDays, défaut 14). La matière (brief) s'édite ICI ; contre-propositions bornées (ClauseVersions ou variantes de body avec diff, contenu d'abord, auteur replié). Sur toute décision structural en framing : « Découper en micro-décisions » — chaque élément part en /decider?parent=&chain=element avec son propre triage/état/modalité ; les concernés pondèrent leurs enjeux (Concern.priority). GARDES D'OUVERTURE (framing→voting ou draft→voting, state.ts) : (a) matière — ≥1 effet recherché exigé si weight ∈ requireEffects du Pacte ('binding' couvre binding+structural ; 'none' dans blank/informal) ; structural sous garde ⇒ ≥1 effet mesurable (target) ; (b) ressources — resources.note exigé si weight != 'light' hors solo. Les deux gardes vivent à l'ouverture, jamais au tunnel. + +DÉLIBÉRATION — selon la route : +- 'solo' selfOnly : draft → adopted immédiat (2 gestes). Trace facultative, visibility 'private' par défaut. +- 'record' : draft → adopted immédiat, decidedHow posé, trace pure. +- 'mandate' et solo-avec-tiers-dérogatoire : draft → objection (fenêtre 48 h, concernés notifiés au Fil, « Ça me va / J'objecte » — le « Ça me va » crée un Assent). À échéance sans objection ouverte : easy ⇒ adopted (silence vaut accord) ; costly/irreversible ⇒ adopted SEULEMENT si ≥1 Assent d'un concerné ≠ auteur, sinon la fenêtre se PROLONGE d'un cran (+objectionWindowHours, relance au Fil) — jamais d'adoption par pur silence hors easy. Objection 'content' maintenue → escalade framing/voting. Objection 'boundary' → windowSuspendedAt, compte à rebours SUSPENDU. +- 'advice' : draft → advice (72 h) ; avis déposés ; l'auteur décide → adopted (avis annexés). Objection formelle → escalade collective/consent. +- 'collective' : framing? → voting : VoteSession créée, liste arrêtée (union calculée + déclarés avant arrêt), W fixé. Modalités : consentement (zéro objection maintenue) ; nuancé (nuancedResult, histogramme visible en cours) ; binaire inertiel (votesFor ≥ wotThreshold) ; PARAMÉTRIQUE : curseurs nommés + statu quo + « calculé » + faisceau + médiane vivante + « Pour moi » + Explorer — values[] révisable jusqu'à closesAt (supersedesVoteId, historique visible du seul auteur) ; élection (désignation par avatar, blanc possible). + CLÔTURE : consent/nuancé/binaire → automatique à closesAt (le moteur constate un seuil sur un contenu formulé par des humains). PARAMÉTRIQUE → à closesAt la session passe 'frozen' (votes figés, médiane figée affichée) puis le GARANT cristallise (« Cristalliser » → 'closed', crystallizedById/At, médiane basse = position adoptée ; quorum parametricMinParticipants non atteint ⇒ rejected constaté au même geste ; relance au Fil s'il tarde ; bimodalité ⇒ le geste affiche « Deux positions distinctes — cristalliser quand même / scinder / reformuler », jamais d'automatisme). ÉLECTION → automatique (pluralité, quorum electionMinParticipants) SAUF égalité : outcome 'tie', l'outil ne départage jamais — session de départage chaînée entre ex æquo, ou tirage au sort si le Pacte l'a prévu (tieBreak). + +GARDE D'AFFLUENCE (transversale, state.ts) : si |déclarés| ≥ concernEscalateRatio × |calculés|, la fenêtre/session NE PEUT PAS se clore sans traitement — élargir d'un cran (1 tap, re-triage journalisé) OU scopeKeptNote motivée publique. + +ADOPTION : decidedAt posé. Micro-célébration : tampon 井 « Décidé » + « C'est décidé — et révisable, comme tout ici ». amendsClauseId → ClauseVersion 'current' automatique. createsMandate → Mandate 'active'. structural/irreversible → review.dueAt + gravure proposée. sunsetAt optionnel. Mode secret : décomptes agrégés seuls (« secret d'affichage — démo »). + +RÉALISATION / ÉVALUATION : stewardIds réalisent ; measurerIds constatent (Effect.measured). À review.dueAt : « L'ÉPREUVE DU RÉEL » — « Le réel a-t-il suivi ? », tableau recherché/cible/constaté AU-DESSUS des 3 boutons « Ça tient / À revoir / À révoquer » ; écart à baselineNote relu ; ressources réexaminées. 'confirmed' : review close (one-shot → 'closed') ; 'revise'/'revoke' : décision enfant chaînée sous protocole et inertie D'ORIGINE. + +DOSSIER DÉCOUPÉ : quand tous les éléments sont en état terminal, carte au Fil du garant « Le dossier est complet — clore et publier la cartographie de clôture » ; le geste fait framing→closed du parent, la cartographie devient le PV du dossier. Jamais automatique. + +RÉVISION / RÉVOCATION : toute décision adoptée porte « Remettre en question » — /decider pré-rempli, protocole et inertie d'origine affichés. Enfant 'revocation' adoptée → parent 'revoked' (« Révoquées — ce qu'on en a appris »). + +URGENCE : « C'est urgent » — si non irréversible, voie la plus légère MAINTENANT (badge « Décidé en urgence — le collectif ratifie », fenêtre ×2) + ratification chaînée automatique. Irréversible ⇒ refusé : « Irréversible : l'urgence ne peut pas contourner le collectif ». + +TRANSMISSION : 'transmit' → 'transmitted' (terminal pour l'auteur) ; le mandataire reprend ou l'auteur conteste le mandat. + +MACHINE À ÉTATS UNIQUE (engine/state.ts) : draft→{advice,objection,framing,voting,adopted,transmitted} ; advice→{adopted,voting} ; objection→{adopted,framing,voting} ; framing→{voting,closed} (closed : dossier complet, geste) ; voting→{adopted,rejected} ; adopted→{revoked,closed} ; + gardes windowSuspendedAt, affluence, matière, ressources, Assent, cristallisation. Toute mutation passe par canTransition(decision, to, ctx). + +## Algorithme de triage + +MOTEUR : engine/triage.ts — fonction PURE triage(input, ctx, settings) → Verdict. Tous les seuils viennent des clauses du Pacte (resolveSettings, replis spécifiés → consent) — le triage est gouverné par ceux qu'il gouverne. « Triage » et « verdict » ne sortent JAMAIS du code : l'UI dit « Le chemin ». La carte affiche l'explanation SEULE (code R* au dépliable « pourquoi ? », au PV et au journal de dérogation). Sujet grammatical : toujours le collectif ou la légitimité, jamais la formule. + +ENTRÉES EXPLICITES (3 chips max) : 1. Concernés → scope (pré-calculé tags×domains, corrigeable, plancher non restrictible). 2. Réversible ? → easy/costly/irreversible. 3. Ça engage quoi ? → light/binding/structural — si binding/structural hors solo, la ligne « Ce que ça engage » (resources.note + montant/unité) se DÉPLIE en suggestion, jamais bloquante (l'exigence vit à la garde d'ouverture de state.ts). ENTRÉES INFÉRÉES : mandats couvrants, clauses qui matchent (index Q0 = Cmd+K), récurrence, consignations similaires, « C'est urgent », clause visée, nombre/%/montant dans la phrase ⇒ parametricHint. + +RÈGLES, DANS L'ORDRE — première qui matche (libellés français FINAUX) : +- R-U URGENCE (overlay) : urgent && !irreversible ⇒ voie la plus légère MAINTENANT (badge « Décidé en urgence — le collectif ratifie », fenêtre ×2) + ratification chaînée auto. urgent && irreversible ⇒ REFUS : « Irréversible : l'urgence ne peut pas contourner le collectif. » +- R0a DÉJÀ DÉCIDÉ : clause en vigueur couvre ⇒ « C'est déjà décidé (E9, Acte Forgeron) — agis. » avec lien, OU « Conteste la règle » ⇒ R3 avec l'inertie de la clause. +- R0b MON MANDAT : ⇒ 'mandate' : « Ton mandat Modération couvre — décide, c'est tracé. » Trace obligatoire aux feux de la rampe + fenêtre d'objection 48 h. +- R0c MANDAT D'UN AUTRE : ⇒ 'transmit' : « Le mandat de Cgeek couvre — transmets-lui. » Alternative : « Conteste ce mandat ». +- R2 MOI SEUL : selfOnly ⇒ 'solo' : « Personne d'autre n'est concerné — décide. » Zéro fenêtre ; si irréversible, revoyure pré-cochée retirable (sur soi l'outil ne s'interpose pas). +- R3 CLAUSE VISÉE : amendsClauseId ⇒ 'collective', protocole = clauseByInertia[clause.inertia] (repli → consent si absent) : « Tu proposes une version de E9 — son inertie s'applique : vote de ceux qu'elle gouverne. » L'héritage Ğ1 (binaire inertiel des clauses) survit ICI, intact. +- R4 PETIT CERCLE RÉVERSIBLE : easy && |concernés| ≤ smallGroupMax(5) ⇒ 'advice' : « Réversible et 3 personnes concernées — demande leur avis puis décide. » 72 h ; objection formelle ⇒ escalade R5/consent. +- R5 COLLECTIF (défaut de l'escalade) : sinon ⇒ 'collective', modalité par taille : ≤ consentMax(7) → consentement (« Vous êtes 6 — un tour d'accord suffit : sans objection, c'est adopté. ») ; ≤ collectiveMin(50) → nuancé (« Vous êtes 23 — vote nuancé : chacun se prononce en nuances, pas en camps. ») ; > collectiveMin → protocole protocolByRange.large résolu par le Pacte (repli → consent) — gabarits standard : « Vous êtes plus de 50 — vote nuancé, la modalité que votre Pacte a choisie. » ; free-currency/Ğ1 : binaire inertiel hérité (« … la formule d'inertie que votre communauté a adoptée »). structural ⇒ + framing + matière selon requireEffects + revoyure + gravure suggérée + inertie haute ; irreversible ⇒ revoyure obligatoire. Si parametricHint : la bascule « régler plutôt que trancher » proposée sous le chemin. +- R6 RÉCURRENCE & MATURATION (overlay, jamais bloquant) : similarRecentCount ≥ 3 ⇒ « 3e fois — réclame un mandat » (createsMandate pré-rempli) ou « crée une règle » ; similarRecordedCount ≥ 3 (≥2 tags partagés) ⇒ « Cette pratique est mûre — protocolise-la en quelques phrases » (pré-remplit une clause de Pacte depuis les traces réelles). + +ALTERNATIVES PERMANENTES (« Je choisis autrement ») : « C'est un réglage (montant, taux, répartition) — décidez au curseur » (parametric, mise en avant si parametricHint) ; « Déjà tranché — je le consigne » (record) ; le binaire pour/contre avec son coût affiché (« deux camps, un perdant ») ; chaque alternative avec son coût. + +DÉROGATION ASYMÉTRIQUE : ALOURDIR libre — 1 tap journalisé. ALLÉGER = overrideNote obligatoire + fenêtre d'objection 48 h imposée, affiché en évidence. MÊME ASYMÉTRIE AU PÉRIMÈTRE : affluence atteinte ⇒ élargir (1 tap) ou scopeKeptNote motivée publique — jamais d'évaporation silencieuse. Le chemin n'est jamais un juge : il recommande, explique, et laisse faire. + +MÉTRIQUE PRODUIT : chemin nominal solo = phrase + Entrée + bouton = 2 gestes, < 10 secondes ; le tunnel n'affiche JAMAIS un champ obligatoire qui ne change pas le chemin (les gardes de matière et de ressources vivent à l'ouverture de fenêtre/session, pas au tunnel) ; brief et curseurs vivent APRÈS le tunnel. + +## Modalités + +CINQ MODALITÉS, UNE BOÎTE À OUTILS HIÉRARCHISÉE PAR LA DOCTRINE (chaque protocole est un Protocol nommé, référencé par une clause du Pacte — le modifier = amender le Pacte ; Protocol.ballot open/secret décidé par le Pacte, défaut open « feux de la rampe », secret v2 = « secret d'affichage — démo » ; dans TOUTES les modalités, l'historique de re-vote supersedesVoteId n'est visible que de son auteur — seul le dernier vote actif apparaît sous un nom) : + +1. CONSENTEMENT (« Ça me va / J'objecte ») — proposé par R5 pour ≤ consentMax(7), par les escalades d'objection, et REPLI universel de protocolByRange (tout collectif en a un — invariant). Zéro vote : le « Ça me va » est stocké (Assent), tour d'objections argumentées, zéro objection maintenue = adopté. Hors du réversible, ≥1 Assent est requis à l'échéance (sinon prolongation) — l'accord est un geste, pas une absence. + +2. NUANCÉ 6 NIVEAUX — proposé par R5 pour 8..collectiveMin(50) ET défaut des grands corpus (protocolByRange.large des gabarits standard). Libellés : 0 « Pas du tout » · 1 « Plutôt non » · 2 « Réservé » · 3 « Plutôt oui » · 4 « Oui » · 5 « Tout à fait » — jamais « CONTRE ». Commentaire OBLIGATOIRE sur 0-1. Histogramme des 6 niveaux visible en cours et à la clôture + commentaires en liste — jamais un simple adopté/rejeté. Valeurs 0-5 et résultats Ğ1 intacts. Clôture automatique à closesAt. + +3. PARAMÉTRIQUE — « Réglage collectif » / décider au curseur. JAMAIS routé d'office : proposé dans « Je choisis autrement », mis en avant si parametricHint. paramSpec : ≤7 curseurs à libellés métier ('slider' borné ou 'share' répartition), contrainte 'sum100' avec EXACTEMENT une part 'derived' (variable d'absorption, résolue 100 − Σ autres) — étiquettes « contrainte / calculé ». Salle : curseurs + statu quo + bloc calculé non éditable + faisceau translucide + médiane trait épais + carte « Pour moi » (linear-share spécifié : quote-parts de resources.amount au prorata de l'attribut déclaré — sans attribut, jamais de chiffre inventé) + « Explorer » (sandbox « exploration — ne compte pas »). Vote values[] révisable jusqu'à la clôture. Agrégation NOMMÉE : « Le collectif retient la médiane de chaque curseur — robuste aux extrêmes, et c'est une position que chacun aurait pu proposer. » (médiane BASSE : l'invariant est vrai par construction, step respecté). À closesAt : session 'frozen' — puis CRISTALLISATION = GESTE HUMAIN DATÉ du garant (« Cristalliser », crystallizedById/At ; quorum parametricMinParticipants sinon rejected ; relance au Fil ; bimodalité affichée en évidence au moment du geste : « cristalliser quand même / scinder / reformuler » — jamais d'automatisme). Pour les DOSSIERS complexes : le découpage en micro-décisions (chainKind 'element', « décision incrémentielle ») combine plusieurs modalités élément par élément, avec pondération (Concern.priority) et cartographie de clôture publiée par le geste de clôture du garant. + +4. ÉLECTION — reléguée aux seules problématiques d'échelle et de mandats : la nomination EST une décision createsMandate (6 méthodes : auto-désignation ratifiée, élection sans candidat, nuancé, consentement, tirage au sort, rotation). Jamais proposée pour une question de fond. CODÉE : Vote.choicePersonId désigne une personne du corpus ; vote déposé sans désignation = BLANC (participation, pas désignation) ; clôture automatique à closesAt : quorum electionMinParticipants sinon rejected, désignation à la PLURALITÉ simple des derniers votes actifs ; ÉGALITÉ ⇒ outcome 'tie' — l'outil ne départage JAMAIS : session de départage chaînée entre ex æquo (pré-remplie), ou tirage au sort entre ex æquo UNIQUEMENT si le protocole du Pacte l'a prévu d'avance (tieBreak:'draw'). Règle affichée en une phrase dans la salle. + +5. BINAIRE INERTIEL (pour/contre + Seuil = C + B^W + (M + (1−M)(1−(T/W)^G))·max(0,T−C)) — HÉRITAGE REMIS À SA PLACE : proposé par R3 pour l'amendement des clauses Ğ1 (clauseByInertia — les 59 Forgeron votées ainsi, D30M50B.1G.2 intact) et par R5 >50 UNIQUEMENT dans free-currency. Partout ailleurs : disponible dans « Je choisis autrement » avec son coût (« deux camps, un perdant ») ; description seedée : « l'outil de dernier recours des très grands corps — remis à sa place dans la boîte à outils ». L'Atelier des formules reste son école (seul lieu des lettres W/T/M/B/G/C et de KaTeX). + +RÉSOLUTION : protocolByRange { consent (obligatoire), nuanced?, large?, parametric?, election?, clauseByInertia? } — Ids résolus par le Pacte, repli spécifié → consent, ajustables en alourdissant. L'adoption applique automatiquement (clause current, mandat créé) — zéro bouton d'acceptation manuel ; tallies/médianes toujours recalculés depuis Vote[]. + +## Périmètres et inclusion + +LE MOT INSTALLÉ : « Périmètre » = qui est concerné par CETTE décision (panneau de la fiche, frise du chemin) ; « Cercle » = groupe durable de personnes (entité Circle, listes nominatives auditables). Les 3 vecteurs doctrinaux : ÉCHELLE D'ENJEU portée par weight/urgent/reversibility ; THÉMATIQUE par tags/domains (suggestion, jamais un droit) ; TOPOLOGIQUE déclaratif via Circle.kind 'place'|'theme'|'team' (iconographie, groupement, ordre des suggestions — jamais un droit ; hint non bloquant à 12 membres pour les 'team'). Le renommage réserve la place des vecteurs calculés (géo/WoT, v3) sans casser un libellé. + +DEUX PORTES, UNE LISTE, CHAQUE ENTRÉE SOURCÉE (Concern horodaté) : +- CONCERNÉS EN PREMIER LIEU (origin 'computed', pastille pleine) : à la validation du chemin, engine/impact.ts calcule l'union CALCULABLE (membres des cercles du scope ∪ personnes nommées ∪ titulaires des mandats intersectants). Chaque inclusion porte sa raison en clair — auditable en un tap, jamais « la machine t'a désigné ». PLANCHER ANTI-EXCLUSION : élargir libre, JAMAIS restreindre sous le calcul. +- CONCERNÉS EN SECOND LIEU (origin 'declared', main levée) : bouton « Ça me concerne » sur toute décision visible, SANS justification — un droit, pas une demande ; motif optionnel public. Déclaré AVANT la liste arrêtée ⇒ vote ; APRÈS ⇒ voix consultative — règle affichée en compte à rebours. + +OBJECTION DE FRONTIÈRE (suspensive — la contestation des frontières prime sur le fond) : « J'ai été oublié / Untel manque » ⇒ windowSuspendedAt, compte à rebours SUSPENDU (pastille pause, « compte à rebours suspendu — frontière contestée »), priorité maximale au Fil de l'auteur, jusqu'à intégration ou réponse motivée. + +MONTÉE D'ÉCHELLE — DÉCIDÉE, JAMAIS ÉVAPORÉE : si |déclarés| ≥ concernEscalateRatio(0.5) × |calculés|, bannière « Le périmètre déborde — élargis d'un cran » ET garde dans state.ts : la fenêtre/session ne peut pas se clore sans traitement — élargissement au cercle parent (couches d'oignon, 1 tap, re-triage journalisé — chemin nominal), ou maintien avec scopeKeptNote motivée PUBLIQUE, affichée en évidence comme un allègement. L'asymétrie de dérogation appliquée au lieu doctrinal de la subsidiarité. + +GRADIENT D'ENGAGEMENT (affiché sur la fiche) : observateur (lecture) < concerné (avis, objection, « Ça me va ») < lié (vote — liste arrêtée) < garant/mesureur (stewardIds réalisent et cristallisent, measurerIds constatent). + +PONDÉRATION PERSONNELLE DES ENJEUX : sur un dossier découpé, chaque concerné pose Concern.priority 0-3 (« pondère tes enjeux ») depuis le Fil ou la fiche — nourrit la hiérarchie des enjeux de la cartographie de clôture, jamais le droit de vote. + +ANTI-BRIGADING PAR CONSTRUCTION : déclarations publiques + liste arrêtée + entrée tardive consultative + escalade non-ignorable — et la formule inertielle absorbe l'afflux (gonfler W sans que T suive fait MONTER le seuil). ANTI-FLICAGE (ligne rouge) : hors mandat, trace facultative et visibility choisie ('private' par défaut en solo) ; sous mandat, trace obligatoire visible du cercle électeur — la redevabilité s'applique au pouvoir, jamais aux personnes ; Person.attributes jamais affichés à autrui ni agrégés ; l'historique des re-votes n'est jamais opposable à une personne (visible du seul auteur). + +FRACTALITÉ : Collective.lineage (« essaimé de … ») mémorise l'essaimage par bundle ; AUCUNE structure fédérale dans le modèle (choix doctrinal documenté) — pattern v3 : une grappe = un collectif ordinaire dont les membres sont des mandataires d'autres collectifs, la fédération = échange de bundles/PV, jamais une table. + +## Instruction et évaluation + +LE PRINCIPE : « Il n'est pas consulté, il est instruit » — pas de vote collectif engageant sans matière, et la même matière sert l'évaluation : la boucle d'« acquisition collective des expériences » est fermée par construction, sans jamais devenir plus lourde que l'enjeu. + +LA MATIÈRE (Decision.brief) : contexte, sources {label,url}, symptômes vs causes (la qualification doctrinale du sujet), et effects — les EFFETS RECHERCHÉS { label une ligne, target cible mesurable optionnelle, measured constaté }. GARDE DÉFINITIVE (state.ts, ouverture de session collective UNIQUEMENT — framing→voting / draft→voting) : ≥1 effet recherché exigé si weight ∈ clause de Pacte 'triage.requireEffects' ('binding' = binding et structural, défaut des gabarits structurés ; 'structural' ; 'none' seedé dans blank ET informal — le collectif règle sa propre exigence) ; sous garde, structural ⇒ ≥1 effet avec cible mesurable. Le weight 'light' (le trio, « on part le 12 ») n'y est JAMAIS soumis ; le chemin solo/record ne voit JAMAIS ces champs — 2 gestes / 10 s préservés ; la matière s'écrit en framing, pas dans le tunnel. + +LE BLOC « S'INSTRUIRE » (condensé — réparation intégrée) : en tête de la salle de vote et de la fiche (framing/voting) — un RÉSUMÉ COMPACT toujours visible avant les boutons : titre, effets recherchés, « Ce que ça engage » ; en DÉPLIABLES internes : body, versions + diff (contenu d'abord, auteur replié), avis et objections déposés, provenance (cas Ğ1 : voteRecord). Le bloc entier se replie automatiquement aux visites suivantes de la même personne (préférence locale par décision) — voter sur un titre nu reste structurellement impossible en collectif, re-voter reste léger (« agir d'abord, comprendre ensuite »). La phrase de fenêtre : « 14 jours pour s'instruire et formuler des contre-propositions ». + +LE STATU QUO (double ancre SejeteralO) : Decision.baselineNote (« Aujourd'hui : … ») suggéré routes advice/collective — en-tête de fiche « Aujourd'hui → Proposé » ; ParamDef.baseline = marqueur « actuel » sur chaque curseur + colonne de la carte « Pour moi » ; les clauses gardent leur diff (qui EST leur baseline). Une décision se lit comme un écart au statu quo, mesurable. + +LA DÉCISION CONNEXE (Decision.resources) : « Ce que ça engage » — note + montant/unité ('heures'|'€'|'DU'|libre). SUGGÉRÉE-dépliée au tunnel (jamais bloquante), EXIGÉE à la garde d'ouverture de fenêtre/session si weight != 'light' hors solo (même mécanique que la matière). Affichée fiche + résumé S'instruire + PV, réexaminée à l'épreuve du réel. Huile/eau : DU et € jamais convertis ni additionnés — agrégats de l'Observatoire par unité, lignes séparées. La paire doctrinale : stewardIds RÉALISENT (et cristallisent), measurerIds MESURENT. resources.amount est aussi le multiplicande de la carte « Pour moi » (linear-share). + +L'ÉPREUVE DU RÉEL (Review) : à review.dueAt — « Le réel a-t-il suivi ? », tableau effet recherché / cible / constaté (Effect.measured saisi par les measurerIds) AU-DESSUS des 3 boutons « Ça tient / À revoir / À révoquer » ; écart à la baseline relu ; ressources confrontées. Jamais un jugement dans le vide : « évaluer les expériences avec la curiosité du chercheur », le droit de se tromper est une donnée. + +L'OBSERVATOIRE (/decisions/observatoire) : comment nous décidons, DANS LES FAITS. Stats d'autonomie (ratio seul/mandat/avis/collectif/consigné, temps médians, participation, dérogations alourdies/allégées/périmètres maintenus, ressources engagées par unité) ; épreuves du réel rendues et dues ; « Révoquées — ce qu'on en a appris » ; élagage suggéré (protocole jamais invoqué, mandat sans trace). LA CONSIGNATION (route 'record') : « Déjà tranché — je le consigne » + decidedHow — consigner AVANT de protocoliser ; ratio consigné/outillé = maturité ; MATURATION (famille R6, jamais bloquante) : ≥3 consignations partageant ≥2 tags ⇒ « Cette pratique est mûre — protocolise-la en quelques phrases » pré-remplissant une clause de Pacte depuis les traces réelles. Le flux consigner→observer→protocoliser est le chemin ordinaire du produit, symétrique de R6 : le contrat social s'écrit au fil de l'eau, depuis la pratique. + +## Navigation + +SHELL : layouts/default.vue. Header : logo-stamp (icône puits, rotation -10°) + « libreDecision » ; WorkspaceSelector (collectifs locaux + finalité A1 en une ligne + « Créer un collectif ») ; recherche globale Cmd+K (index unique clauses/décisions/mandats = le MÊME index que Q0) ; MoodSwitcher ; bouton « Décider » (desktop). FAB « Décider » 56 px mobile, au-dessus du sceau. Sceau hexagramme #48 Tsing 井 fixe bas-droite (géométrie SVG canonique). layouts/bare.vue pour /creer. + +SIDEBAR — 4 ENTRÉES, pas une de plus : +1. Aujourd'hui (/) — le Fil + la capture. À 3 personnes, c'est tout l'outil. +2. Décisions (/decisions) — le registre + l'Observatoire. +3. Textes (/textes) — les documents sous vote permanent, Pacte épinglé. +4. Mandats (/mandats) — les pouvoirs confiés. + +DISSOLUTIONS CONSERVÉES : protocoles dans le Pacte + étape modalité du chemin ; Atelier des formules = /textes/formules (lié depuis CHAQUE seuil, seule maison des lettres W/T/M/B/G/C) ; Sanctuaire = badge 井 + fiche de preuve + filtre ; pas de page réglages (les réglages SONT le Pacte) ; préférences locales = /donnees (mood, export/import, identité, attributs personnels, mode atelier). + +ROUTES : / — Aujourd'hui · /decider — Le chemin (pré-remplissable ?clause= ?parent=&chain= ?mandat=) · /decisions — registre · /decisions/observatoire — L'Observatoire · /decisions/[id] — fiche décision (URL permanente) · /decisions/[id]/vote — salle de vote · /textes — bibliothèque · /textes/formules — Atelier des formules · /textes/[slug] — document vivant · /mandats — liste · /mandats/nouveau — wizard · /mandats/[id] — fiche mandat · /creer — onboarding (bare) · /donnees — données locales. + +COUCHES CODE : app/types/domain.ts (noyau) ; app/lexicon.ts (source unique des libellés FR + test anti-lexique) ; app/engine/ (threshold avec electionResult, parametric avec crystallize, triage, modeParams, settings, state avec ctx, impact — purs, vitest) ; app/data/ (persistence.ts IndexedDB + seeds/*.bundle.json + templates.ts 7 gabarits) ; app/stores/ (contrat commun prêt adaptateur FastAPI — invariants privacy inclus) ; app/composables/ (useMood, useSettings, useFeed, useSearch) ; app/components/ par domaine (chemin/, decisions/, votes/ dont Faisceau et Curseurs, texts/, mandates/, circles/, common/). Le Fil est un SÉLECTEUR PUR — zéro table notification (il dérive : fenêtres, votes, épreuves dues, rapports dus, frontières, affluences, cristallisations en attente, dossiers complets, suggestions R6/maturation). Budget 400 lignes max par page. CSS print A4 (PV + affiche de session). + +## Écrans + +### `/creer` — Créer un collectif + +_Onboarding en 3 écrans — le premier usage EST le tutoriel ; s'ouvre sur le rêve_ + +Ouverture : « Une démocratie qui n'a jamais existé — à votre échelle. » Écran 1 : nom, couleur, mood. Écran 2 : gabarits en 7 cartes qui TRADUISENT — « Page blanche — observatoire d'abord » EN TÊTE (Pacte : clause « nous consignons nos décisions ; le reste s'écrira au fil de l'eau » + A1 finalité, protocole Consentement SEUL installé, routes solo/consignation/avis/consentement) ; puis Informel / Association / Coopérative / Commune / Communauté monnaie libre réétiquetés « point de départ — tout est amendable par décision » ; et « Institution symétrique » (Déclaration de symétrie, mandats Ambassadeur/Négociateur suggérés, première décision « Déclarer notre symétrie »). Chaque gabarit installe Pacte pré-rempli (préambule Autonomie–Équilibre–Liaison proposé et modifiable, clause A1, clauses settingKey dont requireEffects) + protocoles (au minimum Consentement — invariant) + cercle racine typé. Langage concret (« qui doit être d'accord ? »). Écran 3 : moi + membres initiaux. Imports « Explorer Duniter Ğ1 » / « Explorer l'Atelier du Canal » par le MÊME chemin qu'un import utilisateur. Atterrissage sur / avec la première décision proposée : « Adopter notre Pacte » (collective, consentement). Signature finale « Osons, nous verrons ». Réussite < 10 minutes, testée avec des non-théoriciens. + +### `/` — Aujourd'hui + +_L'écran quotidien unique : capture + Fil personnel — la boîte de réception de la démocratie_ + +Barre de capture sticky « Qu'est-ce que tu décides ? » (Entrée → /decider). Header « À toi de décider » avec compteur nu (jamais de rouge — interdits : dette, retard, en souffrance). Fil trié par échéance, sections vides masquées : fenêtres d'objection (« Ça me va / J'objecte » inline — le Ça me va crée un Assent ; compte à rebours ; pastille pause si frontière contestée ; mention « il manque un accord explicite » si fenêtre prolongée) ; avis demandés (3 boutons inline) ; votes ouverts (jauge miniature, lien salle) ; À CRISTALLISER (garant d'un réglage collectif figé : « Les votes sont figés — cristallise la médiane », relance persistante) ; DOSSIER COMPLET (garant : « Le dossier est complet — clore et publier la cartographie de clôture ») ; égalité d'élection à départager (session chaînée proposée) ; épreuves du réel dues (« Ça tient / À revoir / À révoquer » inline) ; rapports de mandat dus ; objections de frontière à traiter (priorité haute) ; périmètre débordant (élargir / maintenir motivé) ; pondération d'enjeux demandée ; suggestions de la boucle apprenante (« 3e fois — réclame ce mandat », « Cette pratique est mûre — protocolise-la », « ce protocole n'a jamais servi — élague »). Ensuite l'activité du collectif (selon visibility). Tout est sélecteur Pinia pur. Mobile-first absolu. + +### `/decider` — Le chemin (tunnel) + +_Une phrase → un chemin exécutable en un tap — 10 s pour le routinier, 3 min pour le stratégique_ + +Phrase en haut (pré-remplie par capture ou query clause/parent/chain/mandat). Résultats Q0 « déjà décidé ? » inline dès la frappe. 3 chips : Concernés (avatars pré-calculés, plancher non restrictible), Réversible ?, Ça engage quoi ? — si binding/structural hors solo, la ligne « Ce que ça engage » se DÉPLIE en suggestion (note + montant/unité, jamais € et DU additionnés) mais ne bloque JAMAIS la validation (l'exigence vit à l'ouverture de fenêtre/session) ; « Aujourd'hui : … » suggéré sur avis/collectif. Toggle « C'est urgent » discret. Carte LE CHEMIN live : grande icône de route, l'explication en une phrase SEULE (aucun code R*), frise le périmètre / combien de temps / quel seuil, revoyure et gravure pré-cochées si exigées, UN bouton principal. « Je choisis autrement » toujours visible : alternatives avec coût, dont « C'est un réglage — décidez au curseur » (mise en avant si nombre/%/montant détecté), « Déjà tranché — je le consigne » (→ adopted immédiat + « comment ça s'est décidé »), binaire avec « deux camps, un perdant ». Alourdir 1 tap journalisé ; alléger = note + fenêtre imposée. « Pourquoi ? » dépliable (le code de règle y vit). Suggestions R6/maturation sous le chemin. Valider crée la Decision structurée dans le bon état. + +### `/decisions` — Registre des décisions + +_Toutes les décisions par état — SectionLayout conservé_ + +Pills d'état en français courant (préparation / avis / objection / formulation / vote / figé / en vigueur / clos / rejeté / révoqué) aux couleurs du cycle. Filtres : cercle (groupés par kind lieu/thème/équipe), tag, « me concerne », poids, gravées 井, consignées. Formes courtes des routes en pills (seul / sous mandat / transmis / sur avis / ensemble / consigné). Cartes : titre, icône de route, poids, échéance, jauge miniature, badge 井, badge « Décidé en urgence — le collectif ratifie ». Lien vers l'Observatoire. Zéro création ici — on crée par la capture. + +### `/decisions/[id]` — Fiche décision + +_URL permanente de l'objet pivot — progressive disclosure par état_ + +En-tête : titre, « Aujourd'hui → Proposé » si baselineNote, route en français, poids, badges (urgence, allègement avec note, périmètre maintenu avec scopeKeptNote en évidence, 井). Timeline verticale du cycle. Panneau PÉRIMÈTRE : concernés en premier lieu (pastille pleine, tap → raison) et en second lieu (main levée, note publique), « Ça me concerne », compte à rebours de la liste arrêtée, bannière d'affluence NON-IGNORABLE (élargir 1 tap / maintenir motivé), objection de frontière (pastille pause). Bloc S'INSTRUIRE (framing/voting) : résumé compact toujours visible (titre, effets, ce que ça engage) + dépliables (body, versions/diff auteur replié, avis/objections, provenance) — auto-replié aux visites suivantes. Selon l'état : bloc fenêtre (boutons inline, silence-vaut-accord affiché seulement si réversible, sinon « un accord explicite est requis » avec compteur d'Assents) ; bloc formulation (« N jours pour s'instruire… », « Découper en micro-décisions » sur structural) ; onglet ÉLÉMENTS si dossier (filtre « me concernent », états, échéances, pondération 0-3) + CARTOGRAPHIE DE CLÔTURE auto-générée + carte « Le dossier est complet — clore » quand tous terminaux (geste du garant) ; bloc session (lien salle, résultat, « Qui vote : N personnes — liste arrêtée le … », bandeau « Votes figés — en attente de cristallisation » si frozen) ; bloc vigueur (garants, mesureurs, ressources, sunset, épreuve du réel due : tableau recherché/constaté au-dessus des 3 boutons) ; chaînage cliquable. Poignée « Remettre en question » sur tout adopté (protocole et inertie D'ORIGINE affichés). Actions : Imprimer (PV A4), graver (« empreinte locale — démo »), micro-copy « à trancher en présence ? » sur structural. Budget 400 lignes. + +### `/decisions/[id]/vote` — Salle de vote + +_Le geste démocratique central — instruit, puis prononcé_ + +Bloc S'INSTRUIRE condensé en tête : résumé compact toujours visible avant les boutons (titre, effets recherchés, ce que ça engage) + dépliables (texte/diff, avis et objections, provenance) — replié automatiquement aux visites suivantes. « Qui vote : 11 personnes — liste arrêtée le 12/08 » (avatars). Selon la méthode : CONSENTEMENT (« Ça me va » stocké / « J'objecte » argumentée) ; NUANCÉ 6 niveaux (« Pas du tout » → « Tout à fait », commentaire obligatoire inline sur 0-1, HISTOGRAMME sous la jauge) ; BINAIRE hérité (jauge inertielle : arc animé, seuil qui descend quand la participation monte, phrase sujet-collectif : « 120 votants sur 7 224 : à si faible participation, il faut presque l'unanimité — 94 pour. Plus de monde vote, plus le seuil descend. », lien « comprendre ce seuil » → Atelier pré-réglé) ; PARAMÉTRIQUE « Réglage collectif » : un curseur par paramètre (libellé métier, marqueur statu quo creux, alternative clavier + champ numérique), bloc « calculé » non éditable (étiquettes contrainte/calculé), FAISCEAU strip-plot (points translucides anonymes, médiane basse trait épais), carte « POUR MOI » (ma position / médiane / statu quo — quote-parts de « ce que ça engage » au prorata de mon attribut déclaré, « estimation sur données déclarées », jamais de chiffre inventé), « EXPLORER » (sandbox « exploration — ne compte pas », partir de la médiane/du statu quo/de ma position, « En faire ma position »), phrase d'agrégation « Le collectif retient la médiane de chaque curseur — robuste aux extrêmes, et c'est une position que chacun aurait pu proposer », bannière bimodalité non bloquante ; à closesAt bandeau « Votes figés — le garant cristallise » puis bouton « CRISTALLISER » (garant seul, quorum vérifié, bimodalité rappelée : « cristalliser quand même / scinder / reformuler ») ; ÉLECTION (désignation par avatar, bouton « Vote blanc », règle en une phrase : « La personne la plus désignée est nommée ; en cas d'égalité, vous départagez — jamais l'outil » ; égalité ⇒ session de départage chaînée ou tirage au sort si le Pacte l'a prévu). Mode secret : décomptes agrégés et commentaires anonymisés seuls, « secret d'affichage — démo ». Mon vote remplaçable — « ton historique n'est visible que de toi » (chaîne supersedes jamais publique, dernier vote seul affiché sous un nom en ballot open). Bouton « Imprimer » : affiche de session A4. Clôture : distribution complète + arguments — jamais un simple adopté/rejeté ; adoption ⇒ application automatique + tampon 井 « C'est décidé — et révisable, comme tout ici ». + +### `/decisions/observatoire` — L'Observatoire + +_Comment nous décidons, dans les faits — le miroir et l'apprentissage_ + +Titre « L'Observatoire », sous-titre « comment nous décidons, dans les faits ». Stats d'autonomie : ratio seul/mandat/avis/collectif/CONSIGNÉ (consigné/outillé = maturité), temps médians par route, taux d'objection, participation, dérogations (alourdies/allégées/périmètres maintenus), ressources engagées PAR UNITÉ (heures / € / DU — lignes séparées, jamais additionnées). Épreuves du réel : dues, rendues (tenues/à revoir/révoquées). « Révoquées — ce qu'on en a appris » : chaque révoquée avec sa chaîne — curiosité du chercheur, pas de lugubre. Consignations : liste des « déjà tranché » avec decidedHow ; maturation : ≥3 consignations partageant ≥2 tags ⇒ carte « Cette pratique est mûre — protocolise-la en quelques phrases » (pré-remplit une clause de Pacte). Élagage : protocole jamais invoqué, mandat sans trace 6 mois → revue suggérée. Décisions jamais revues : relance puis archivage proposé. Filtres personne/cercle/tag/période. Graphiques sobres (skill dataviz). + +### `/textes` — Textes + +_La bibliothèque des documents sous vote permanent_ + +Pacte épinglé en tête : badge « les règles du jeu » + sous-titre « Notre contrat social — sacralisé, jamais immuable ». Cartes documents : titre, rôle, clauses, sessions en cours (mini-jauges), provenance Ğ1 (Certification 33 clauses, Forgeron 59 v2.0.0-fr, Comité Technique). « Déclaration de symétrie » pour les collectifs symmetric. Lien Atelier des formules. Filtre gravées. + +### `/textes/[slug]` — Document vivant + +_Clauses versionnées, boucle vote⇒texte fermée_ + +Sections repliables + scroll-spy. Chaque clause : code, titre, pastille d'inertie CÂBLÉE au protocole d'amendement, mini-jauge sur session réelle, statut. Tap → détail : version courante, propositions avec diff (contenu d'abord, auteur replié), décision fondatrice et amendements chaînés. « Proposer une version » → /decider?clause= (R3, inertie de la clause). Vue projetée conservée. InertiaSlider pédagogique → Atelier pré-réglé. Provenance/GenesisBlock (sources, voteRecord 97/23/19, notes — typos signalées jamais corrigées). Spécifique Pacte : clauses settingKey en clair (« Petit groupe : jusqu'à 5 personnes », « Matière exigée : décisions engageantes et structurantes »), effet IMMÉDIAT à l'adoption (resolveSettings), clause d'inertie protégée par l'inertie maximale ; clause A1 — Notre finalité en tête ; préambule Autonomie–Équilibre–Liaison. + +### `/textes/formules` — Atelier des formules + +_La pédagogie du seuil — seule maison des lettres_ + +FormulaEditor : sliders M/B/G/C (+Smith/ComTech), KaTeX de la formule inertielle, table de participation, presets d'inertie comparés. Branché sur engine/threshold.ts UNIQUE. Accessible depuis chaque seuil affiché, pré-réglé (« pourquoi 94 ? »). Cas Forgeron 2026 en un clic (W=7224, T=120 ⇒ 94). Le binaire présenté comme outil d'exception (« l'outil de dernier recours des très grands corps »). Section « Réglage collectif » : la médiane basse par curseur expliquée (robuste aux extrêmes, une position que chacun aurait pu proposer), mini-démo interactive, et la règle de cristallisation (« l'agrégat éclaire, le geste cristallise »). + +### `/mandats` — Mandats + +_La carte des pouvoirs confiés : qui peut décider quoi, jusqu'à quand_ + +Cartes par statut (proposé/actif/expiré/révoqué) : titulaire, domaine (cercles + tags), échéance, prochain rapport dû, COMPTEURS BRUTS (« 12 décisions tracées · 1 objection · 2/2 rapports ») — AUCUNE jauge, aucun agrégat. Bouton « Réclamer un mandat » → wizard (souvent pré-rempli par R6). + +### `/mandats/[id]` — Fiche mandat + +_Le pouvoir confié, visible et reprenable_ + +Titulaire, origine (la décision créatrice — cliquable), domaine explicite, période, cercle électeur. Bloc « EXERCICE DU MANDAT » : faits comptés jamais synthétisés — n décisions tracées, objections ouvertes/intégrées/escaladées (liste cliquable), rapports rendus/dus. Bloc « FEUX DE LA RAMPE » (sous-titre « toutes les décisions prises sous ce mandat — contestables ») : flux des décisions sous mandat, chacune avec sa fenêtre d'objection. Rapports de redevabilité. « Demander la révocation » en un geste : /decider chaîné (revocation) au protocole de nomination D'ORIGINE, cercle électeur comme périmètre — jamais improvisée, jamais plus coûteuse que l'élection. + +### `/mandats/nouveau` — Wizard mandat + +_Le meilleur flux v1, conservé et branché sur le pivot_ + +3 étapes : (1) rôle et domaine (titre, cercles, tags, mission en une phrase) ; (2) modalité de nomination parmi 6 (auto-désignation ratifiée, élection sans candidat — avec sa règle de clôture affichée : pluralité, blanc, départage par vous, jamais par l'outil —, nuancé, consentement, tirage au sort, rotation) avec pros/cons pédagogiques ; (3) durée bornée + cadence de rapports + cercle électeur. Sortie : une Decision createsMandate routée collective — la nomination EST une décision. + +### `/donnees` — Données locales + +_Local-first assumé : tes données, ton fichier, ta machine_ + +Export bundle JSON (schemaVersion 2), import avec validation (protocole Consentement requis, collisions) + pose de lineage (« essaimé de … »), réinitialisation par collectif. Seeds (Duniter Ğ1, Atelier du Canal). Identité locale (displayName, isMe) + MES ATTRIBUTS (Person.attributes, clés libres : 'heures/mois'… — « servent uniquement ta carte Pour moi, jamais montrés aux autres »). MODE ATELIER (ex-« agir en tant que ») : saisie assistée en présence — chaque saisie porte « saisi par X pour Y — atelier du » ; l'étiquette démo reste pour les personnages seedés. Niveaux de preuve honnêtes (empreinte locale / IPFS / chaîne — jamais affichés tant que non réels). Note synchro : « un serveur pourra se brancher plus tard, tes données ne changent pas de forme ». + +## Vocabulaire UI + +| Contexte | Terme final | Remplace | +|---|---|---| +| Carte centrale de /decider et titre du tunnel — le type TS Verdict reste en code ; « verdict » et « triage » ne sortent jamais du code | Le chemin | Le Verdict | +| Popover/panneau Périmètre — pastille pleine ; « la machine t'a désigné » est banni | Concerné·e en premier lieu | calculé (glyphe calculator) | +| Popover/panneau — la main levée reste ; « Ça me concerne » inchangé | Concerné·e en second lieu — s'est déclaré·e | déclaré | +| Chip du tunnel /decider | Concernés | Touchés | +| Panneau de la fiche + frise du chemin ; « Cercle » = groupe durable seulement ; « scope » reste code | Périmètre | Cercles (panneau) / « qui est convoqué » | +| Sidebar + rôle de /mandats/[id] ; tout dérivé de « déléguer » banni de l'UI (test anti-lexique) | Mandats — les pouvoirs confiés | les pouvoirs délégués | +| Header du Fil, compteur nu — interdits : dette, retard, en souffrance ; « dette décisionnelle » survit en métrique interne dev | À toi de décider | dette décisionnelle | +| Fiche et cartes mandat — faits comptés, jamais synthétisés ; « confiance » banni des agrégats | Exercice du mandat | Jauge de confiance | +| Flux public des décisions prises sous un mandat — contestables | Feux de la rampe | Observatoire (bloc fiche mandat) | +| /decisions/observatoire — « comment nous décidons, dans les faits » | L'Observatoire | Mémoire | +| Salle et fiche ; W/T/M/B/G/C vivent uniquement à l'Atelier des formules | Qui vote : 11 personnes — liste arrêtée le 12/08 | corpus visible : W, T / snapshot | +| Échelle nuancée — valeurs 0-5 et commentaire obligatoire 0-1 inchangés, résultats Ğ1 intacts | Pas du tout · Plutôt non · Réservé · Plutôt oui · Oui · Tout à fait | 0=CONTRE … 5=TOUT À FAIT | +| Bloc revoyure — fiche et Fil | L'épreuve du réel / « Le réel a-t-il suivi ? » | Verdict de réalité | +| 3 boutons de l'épreuve du réel (Review.verdict inchangé en code) | Ça tient / À revoir / À révoquer | confirmée / réviser / révoquer | +| Toggle du tunnel + badge ; « conservatoire » survit dans le PV et la doc | C'est urgent / « Décidé en urgence — le collectif ratifie » | décision conservatoire | +| Protocole et mode de la salle — « raisonner curseur / table de mixage » | Réglage collectif / décider au curseur | —(nouveau : VoteMethod 'parametric') | +| Clôture paramétrique : « Les votes sont figés — cristallise la médiane » ; « l'agrégat éclaire, le geste cristallise » — jamais automatique | Cristalliser | —(nouveau : geste du garant) | +| Bandeau de la salle/fiche entre closesAt et le geste du garant | Votes figés — en attente de cristallisation | —(nouveau : status 'frozen') | +| Découpage d'un dossier en micro-décisions — le nom de l'auteur réservé à CE mode (Taïwan) | Décision incrémentielle | —(nouveau : chainKind 'element') | +| Section auto-générée du dossier découpé ; publiée par le geste « clore » du garant, devient le PV du dossier | Cartographie de clôture | —(nouveau) | +| Étiquettes du paramétrique : cadre en lecture seule vs part dérivée — le votant sait ce qu'il contrôle | contrainte / calculé | —(nouveau) | +| Carte d'impact (ma position / médiane / statu quo) — « estimation sur données déclarées », jamais de chiffre inventé | Pour moi | —(nouveau) | +| Bloc de tête salle/fiche : résumé compact toujours visible + dépliables, auto-replié aux visites suivantes | S'instruire | —(nouveau, regroupe l'existant) | +| Ligne suggérée du tunnel + bloc fiche/PV — la décision connexe de ressources ; exigée à l'ouverture, jamais au tunnel | Ce que ça engage | —(nouveau : resources) | +| Alternative permanente du tunnel — l'observatoire en un geste | Déjà tranché — je le consigne | —(nouveau : route 'record') | +| Statu quo en en-tête de fiche ; marqueur « actuel » sur chaque curseur | Aujourd'hui : … / Aujourd'hui → Proposé | —(nouveau : baselineNote) | +| Libellés des routes (lexicon.ts) — le chemin est un geste, pas une catégorie ; formes courtes en pills de filtres | Je décide · Je décide, sous mandat · Je transmets · J'écoute, puis je décide · Nous décidons | Solo / Mandat / Transmettre / Avis / Collectif | +| Salle de vote : la chaîne supersedes n'est jamais publique sous un nom ; seul le dernier vote actif apparaît en ballot open | Ton vote est remplaçable — ton historique n'est visible que de toi | re-vote audité — l'historique reste (indéterminé) | +| Élection : vote déposé sans désignation — compte pour la participation, pas pour la désignation | Vote blanc | —(nouveau : élection) | +| Règle d'élection affichée en salle ; départage = session chaînée entre ex æquo ou tirage au sort si le Pacte l'a prévu | En cas d'égalité, vous départagez — jamais l'outil | —(nouveau : outcome 'tie') | +| Fenêtre d'objection non réversible prolongée : ≥1 « Ça me va » requis — le silence ne vaut accord que pour le réversible | Il manque un accord explicite | —(nouveau : Assent) | +| Description seedée du protocole binaire + Atelier ; libellé Ğ1 : « héritage Toile de Confiance » | pour/contre — « l'outil de dernier recours des très grands corps » | vote binaire (neutre) | +| Première décision proposée — actes collectifs à l'infinitif (règle de ton n°2) | Adopter notre Pacte | Adoptez votre Pacte | +| Section de l'Observatoire — droit à l'erreur, curiosité du chercheur | Révoquées — ce qu'on en a appris | Cimetière des révoquées | +| /donnees — saisie assistée en présence, passerelle hors-écran | mode atelier — « saisi par X pour Y — atelier du » | agir en tant que (pur démo) | +| Étiquette du sandbox Explorer de la salle paramétrique | exploration — ne compte pas | —(nouveau) | +| Métadonnée d'affichage d'un collectif né d'un bundle d'un autre | essaimé de … | —(nouveau : lineage) | +| Pastille pause de l'objection de frontière | compte à rebours suspendu — frontière contestée | (conservé v1 — il est juste) | + +## Seeds + +EXTRACTION — script backend/scripts/export_seed_bundle.py (venv existant) : importe seed.py (33 items Certification, 59 Forgeron, GENESIS, VOTER_NAMES ×11, mandates ×3, protocoles) et émet frontend/app/data/seeds/duniter-g1.bundle.json au schéma Bundle v2 (avec assents:[]). TEST DE NON-PERTE (vitest, bloquant) : 33+59 clauses, presets d'inertie conservés, provenance.voteRecord == {for:97, against:23, invalid:19, wotSize:7224, thresholdRequired:97, modeParams:'D30M50B.1G.2S.1'}, 11 personnes dont 5 smiths, 3 mandats, protocoles complets. Diff exigé AVANT dépréciation de seed.py. Typos des textes votés : Provenance.notes, JAMAIS corrigées. + +MAPPING Ğ1 (v1 conservé, amendé) : Collective{template:'free-currency', isTransparent:true} ; 11 Person (5 smiths) ; Circles typés kind — « Toile de confiance » (racine), « Forgerons » (team), « Comité Technique » (team) ; TextDocs Certification (33 clauses, presets câblés), Forgeron (59 clauses v2.0.0-fr, voteRecord complet), Comité Technique ; pacte-duniter-g1 avec clauses settingKey (smallGroupMax=5, collectiveMin=50, consentMax=7, fenêtres 48/72, framingDays=14, concernEscalateRatio=0.5, recurrenceThreshold=3, reviewDelayDays=90, requireEffects='binding', clause A1 « Notre finalité » adaptée à la toile), clause d'inertie en preset 'max', description du Pacte « Notre contrat social — sacralisé, jamais immuable ». protocolByRange : consent → « Consentement » (D7, OBLIGATOIRE partout), large → « Vote WoT standard » (binaire inertiel D30M50B.1G.2, description « l'outil de dernier recours des très grands corps — héritage Toile de Confiance ») — L'HÉRITAGE À SA PLACE ; clauseByInertia mappé sur les 4 presets ; + « Réglage collectif » (parametric, D14, parametricMinParticipants), « Vote nuancé » (D30), « Vote forgeron Smith » (S.1), « Vote ComTech » (T.1), « Élection sans candidat » (D14, electionMinParticipants, tieBreak:'runoff') — tous ballot:'open'. Chaque clause courante reçoit sa DÉCISION FONDATRICE. 3 Mandates (ComTech Cgeek active + rapport dû ; Administrateur Forgerons proposed EN VOTE nuancé ; Modération Forum active). Décisions historiques : Runtime Upgrade 1100 (adopted, structural, gravée, revoyure posée + brief.effects rétro-documentés), Évolution licence v0.4.0 (framing avec versions). + +DEUXIÈME COLLECTIF — atelier-du-canal.bundle.json (test permanent de marque blanche) : association fictive 12 membres, Pacte à seuils DIFFÉRENTS (smallGroupMax=4, fenêtres courtes, requireEffects='binding'), protocolByRange.large → NUANCÉ (la démonstration anti-binaire), Règlement intérieur 8 clauses, cercles typés Bureau (team) / Ateliers (theme) / Tous (racine), mandat Trésorerie (rapport dû), Person.attributes seedés ('heures/mois'). Décisions à TOUS les états : draft, advice en fenêtre, objection SUSPENDUE par frontière, objection non-easy avec 1 Assent posé (démonstration « accord explicite »), framing avec 2 versions et diff, voting nuancé en cours (histogramme vivant), adopted avec épreuve du réel due (brief.effects + measurerIds + resources posés — boucle complète), revoked (« ce qu'on en a appris »), conservatoire + ratification chaînée, UNE CONSIGNATION 'record' (« Le marché du samedi est reconduit — décidé au café, comme d'habitude ») et UN DOSSIER DÉCOUPÉ (2 éléments dont 1 terminal, priorités posées, cartographie de clôture en construction) ; et LE PARAMÉTRIQUE D'EXEMPLE : « Répartir le budget d'ateliers 2026 » en voting — resources { note:'budget annuel ateliers', amount:1200, unit:'€' }, paramSpec sum100 avec 3 parts nommées + EXACTEMENT 1 part 'derived' « Réserve (calculé) », baselines posées, impactAttrKey 'heures/mois', 5-6 votes seedés → faisceau, médiane basse et « Pour moi » calculables et vivants à l'ouverture ; une SECONDE session paramétrique 'frozen' (closesAt passé) avec la carte « À cristalliser » au Fil du garant — la cristallisation-geste démontrée. Tout contenu Ğ1 en dur dans le code est structurellement impossible. + +GABARITS (/creer, app/data/templates.ts) — SEPT : blank « Page blanche — observatoire d'abord » (Pacte : clause consignation + A1, protocole Consentement SEUL — l'invariant protocolByRange.consent satisfait, toutes les autres clés en repli → consent, requireEffects='none') ; informal (requireEffects='none') / association / cooperative / commune réétiquetés « point de départ — tout est amendable par décision », protocolByRange.large → nuancé partout, requireEffects='binding' ; free-currency (mapping Ğ1, large → binaire inertiel, unité DU par défaut dans resources) ; symmetric « Institution symétrique » (Pacte transparence totale, TextDoc « Déclaration de symétrie », première décision « Déclarer notre symétrie », mandats suggérés Ambassadeur et Négociateur). Chaque gabarit : préambule Autonomie–Équilibre–Liaison proposé et modifiable + clause A1 (défaut : boussoles vitalité/émancipation) + cercles typés kind + protocole Consentement au minimum. Les seeds s'importent par le MÊME chemin que l'import utilisateur ; lineage posé si bundle d'origine étrangère ; l'import valide l'invariant consent. + +## Directives de design + +ICONOGRAPHIE DES ROUTES (vocabulaire visuel fixe) : solo = i-lucide-zap ; sous mandat = i-lucide-key-round ; transmettre = i-lucide-send ; avis = i-lucide-message-circle ; collectif = i-lucide-users ; consignation = i-lucide-notebook-pen ; urgence = i-lucide-siren ; déjà décidé = i-lucide-book-open-check ; récurrence→mandat = i-lucide-repeat ; réglage collectif = i-lucide-sliders-horizontal ; cristallisation = i-lucide-stamp (le geste). Carte Le chemin : icon-container 14 px, nom de route en 700 À LA PREMIÈRE PERSONNE, l'explication en une phrase dessous (jamais de code R*), frise périmètre/durée/seuil, bouton principal pill 20 px plein accent, alternatives en pills fantômes. Teintes déclinées des custom properties du mood. + +PÉRIMÈTRE : stacks d'avatars (initiales, teinte déterministe de l'id), débord -8 px, « +N ». Premier lieu = pastille pleine en coin (glyphe calculator SUPPRIMÉ) ; second lieu = main levée + fond légèrement teinté. Tap → popover chaîne d'inclusion. Plancher verrouillé visuellement (pas de croix de retrait sur les premiers-lieu). Cercles emboîtés en chips imbriquées ; icône par kind (lieu/thème/équipe). + +CYCLE DE VIE : timeline verticale à pastilles pleines reliées (pas de bordure) — draft neutre, avis/objection ambre + compte à rebours, vote bleu + mini-jauge, FIGÉ bleu-gris avec icône tampon en attente (« votes figés — en attente de cristallisation »), en vigueur vert, épreuve du réel violette, révoqué terre cuite, clos gris, rejeté barré. SUSPENSION : pastille pause + fond strié + « compte à rebours suspendu — frontière contestée ». Fenêtre prolongée (Assent manquant) : ambre soutenu + « il manque un accord explicite ». Un seul mapping état→couleur. + +JAUGE INERTIELLE (signature, héritage) : arc 270°, seuil qui DESCEND quand la participation monte, chiffres clairs, phrase générée sujet-collectif. KaTeX réservé à l'Atelier. + +NUANCÉ : 6 segments en gradient sémantique construit depuis la palette du mood (skill dataviz, jamais rouge/vert crus) ; HISTOGRAMME des 6 niveaux sous la jauge (la distribution, pas seulement le résultat) ; commentaire obligatoire inline sous 0-1. + +CURSEURS (Réglage collectif) : poignées tactiles ≥ 2.25 rem AVEC alternative clavier + champ numérique (correction SejeteralO) ; libellé métier au-dessus, unité, valeur courante en 700 ; marqueur statu quo = losange creux sur le rail ; part « calculé » = rail estompé non saisissable + étiquette « calculé » ; étiquette « contrainte » sur le cadre ; clamp dynamique visible (le rail se raccourcit quand la réserve dérivée approche de ses bornes). FAISCEAU : strip-plot par curseur — chaque position = point translucide anonyme (opacity ~0.25), médiane = trait épais accent, baseline = marqueur creux ; « transparence des agrégats, discrétion des personnes ». Carte « Pour moi » : fond légèrement teinté accent, trois colonnes ma position / médiane / statu quo, mention « estimation sur données déclarées ». Sandbox Explorer : bandeau « exploration — ne compte pas », curseurs en teinte neutre. CRISTALLISATION : bouton-tampon réservé au garant, confirmation avec rappel de bimodalité le cas échéant — le geste est solennel mais léger (un tap + confirmation), jamais une cérémonie. + +MICRO-CÉLÉBRATION D'ADOPTION : le tampon 井 incliné -10° s'appose « Décidé » (translateY du système, 0.12s ease, aucune fanfare) + « C'est décidé — et révisable, comme tout ici ». + +GRAVURE ET PREUVE : badge 井 tampon incliné sur objets gravés ; fiche de preuve sha256 mono tronqué copiable + « empreinte locale — démo ». Sceau hexagramme #48 Tsing : géométrie SVG canonique (viewBox 112×105, 6 traits, trait 1 en bas = dernier rect, opacity ~0.28, drop-shadow CSS), fixe bas-droite, jamais recouvert ; FAB au-dessus avec décalage. + +PRINT A4 : feuille CSS print pour le PV (/decisions/[id]) et l'affiche de session (/decisions/[id]/vote) — titre, texte, qui vote, seuil, échéance, comment participer ; noir sur blanc, sans moods. La cartographie de clôture d'un dossier s'imprime comme PV. + +MOODS v2 (champ lexical du puits, 2 light + 2 dark) : Source (light bleu d'eau), Margelle (light sable/pierre), Nappe (dark bleu profond), Minuit (dark encre). Socle : borderless (background + box-shadow), arrondis 16/12/20/14, hover cards translateY(-3px) 0.12s, mobile-first clamp(), breakpoints 480/768/1024, touch ≥ 2.25 rem. useMood.ts conservé. + +## Hors scope v2 + +- Décision paramétrique COMPLÈTE : équation de réalité pluggable (viabilité auto-calculée type p0 non linéaire), éditeur de courbes 'curve' (Bézier, ParamKind réservé), impact sur données réelles importées du périmètre, rétro-manipulation résultat→contrainte — v2 livre curseurs bornés + répartition sum100 (un derived, clamp/renormalisation spécifiés) + médiane basse + faisceau + baseline + impact linear-share sur attribut déclaré ; le solveur et le registre importé en v3 +- Builder d'indicateurs de l'observatoire (indicateurs propres définis par le collectif) — construire l'éditeur avant d'avoir observé la pratique contredirait l'observatoire lui-même ; v2 = stats fixes + effets recherchés/constatés + consignations +- Vecteur topologique calculé (bassin de vie, commune pivot, géo) et règles d'appartenance WoT — v2 : listes nominatives + Circle.kind déclaratif ; enum de règle extensible sans migration +- Vote secret cryptographique et pseudonymat certifié @personne (deux identités découplées) — v2 : ballot 'secret' en secret d'affichage honnête + dépersonnalisation d'ordre d'affichage ; la crypto arrive avec la synchro et l'auth Duniter +- Économie du don outillée : allocation en DU effective, budget de gratitude, caisses fléchées, coefficients relatifs, rétribution des mandats pilotée par le collectif — v2 se limite à l'unité 'DU' descriptive dans resources (gabarit free-currency) ; l'intégration exige Ğ1/GrateWizard +- Grappes et fédération inter-collectifs opérationnelles — v2 pose lineage + le pattern documenté « une grappe = un collectif ordinaire dont les membres sont des mandataires d'autres collectifs ; la fédération = échange de bundles, jamais une table » ; jamais de parentCollectiveId +- Engagements fermes à seuil critique (crowdaction : recueillir, ne déclencher qu'au quota) — nouveau type d'objet et de cycle de vie, backlog v3 +- Cartographie thématique vectorielle (liaisons, interdépendances, symptômes/causes visualisés) — v2 : tags plats + champ symptomsVsCauses du brief ; la carte manipulable est un produit en soi +- Événements de brassage générés par l'outil et multi-postes simultanés pour ateliers physiques — v2 livre l'imprimable A4 + le mode atelier mono-poste (intermédiation humaine, doctrinalement acceptable) +- Délégation liquide / procuration — si elle arrive en v3, la nommer « mandater son vote » (jamais « déléguer » — le test anti-lexique l'attrapera) +- IA repositionnée sur la recherche des biais décisionnels (seul usage autorisé par la doctrine) — exige corpus réel + backend ; jamais de suggestion prescriptive +- Synchronisation backend / adaptateur FastAPI — contrat des stores et Bundle prêts (invariants privacy des re-votes et ballot secret notés au contrat) ; brancher l'API ne change aucun écran +- Auth Duniter challenge-réponse Ed25519 — identités locales v2, duniterAddress réservé +- Notifications sortantes (email, digest) et WebSocket temps réel — tout est in-app dans le Fil ; silence-vaut-accord réservé au réversible en conséquence, et l'Assent couvre le reste +- IPFS et ancrage on-chain — proofLevel 'local' uniquement, champs réservés jamais affichés +- Jugement majoritaire, catalogue 18 modalités × 9 nominations, ProcessTemplate opérationnels, exports juridiques signés — bibliothèque documentaire ; le PV markdown/A4 reste « registre de légitimité, jamais substitut légal » +- Passerelles hors-écran complètes (relais humains institués, restitutions événementielles) et horizon des institutions symétriques réelles — consignés dans la page-manifeste/README comme horizon nommé, pas comme dette + +## Plan d'implémentation + +1. 1. SOCLE, LEXIQUE ET MOTEURS — branche v2, shell vers layouts/, moods 4 ambiances, types/domain.ts complet (ci-dessus, avec Assent, frozen, choicePersonId), app/lexicon.ts (source unique + test vitest anti-lexique + 3 règles de ton) DÈS CE SPRINT, engine/ TS pur (threshold avec electionResult, nuanced, parametric avec crystallize, modeParams, settings avec replis, triage, state avec canTransition(decision,to,ctx), impact) avec vitest portant test_threshold/test_qualifier/test_nuanced/test_mode_params + specs parametric (médiane BASSE step-safe ; sum100 exactement-un-derived + clamp/renormalisation ; computeMyImpact linear-share ; detectBimodality ; dégénérescences 0-vote) + specs election (pluralité, blanc, quorum, tie) + specs state (Assent, affluence, matière, dossier) — le cas Forgeron W=7224 T=120 ⇒ 94 vert AVANT tout écran +2. 2. PERSISTANCE ET SEEDS — data/persistence.ts (IndexedDB, export/import, validation consent obligatoire, lineage à l'import), export_seed_bundle.py, bundles duniter-g1 + atelier-du-canal (paramétrique d'exemple calculable avec resources.amount, session frozen à cristalliser, consignation, dossier découpé, attributs, Assent démontré), tests de non-perte bloquants (33/59 clauses, 97/23/19/7224, 11 personnes, 3 mandats, protocoles), templates.ts 7 gabarits (blank en tête avec Consentement seul, large→nuancé partout sauf free-currency, requireEffects par gabarit) +3. 3. SHELL ET ONBOARDING — header (WorkspaceSelector + finalité A1, Cmd+K, MoodSwitcher, Décider), FAB, sceau 井 canonique, /creer 3 écrans (phrase du rêve, 7 gabarits, « Adopter notre Pacte », signature « Osons, nous verrons »), /donnees (export/import/reset, identité, attributs, mode atelier) +4. 4. LE GESTE — /decider complet (Q0 inline, 3 chips, ligne « Ce que ça engage » suggérée jamais bloquante, baselineNote suggéré, carte Le chemin sans code de règle, C'est urgent, dérogation asymétrique, alternatives permanentes réglage/consignation/binaire-avec-coût, suggestions R6+maturation, création structurée) + / Aujourd'hui (capture sticky, Fil sélecteurs purs incluant cartes À cristalliser / Dossier complet / accord explicite manquant, « À toi de décider » compteur nu, boutons inline) — MESURER : 2 gestes < 10 s, et la consignation aussi +5. 5. CYCLE DE VIE — /decisions (registre) + /decisions/[id] (timeline avec état figé, panneau Périmètre premier/second lieu auditable, Ça me concerne, objection de frontière suspensive, garde d'affluence non-ignorable + scopeKeptNote, bloc S'instruire condensé auto-repliable, fenêtres : silence-vaut-accord easy seulement + Assent + prolongation, gardes d'ouverture matière/ressources, chaînage, Remettre en question au protocole d'origine, PV + print A4, gravure honnête) +6. 6. LE VOTE — /decisions/[id]/vote : consentement (Assent stocké), nuancé (libellés définitifs, histogramme, commentaire 0-1), binaire hérité (jauge inertielle, phrase sujet-collectif, lien Atelier), PARAMÉTRIQUE (curseurs + clavier, statu quo, contrainte/calculé, clamp dynamique, faisceau, médiane basse expliquée, Pour moi, Explorer, frozen + geste Cristalliser du garant + quorum + bimodalité), ÉLECTION (désignation, blanc, pluralité, tie → départage humain), ballot secret d'affichage, liste arrêtée, re-vote à historique privé, mode atelier (recordedById), affiche A4, adoption ⇒ application automatique + micro-célébration 井 +7. 7. LES TEXTES — /textes + document vivant (clauses, versions/diff auteur replié, vue projetée, inertie câblée, Provenance/GenesisBlock, Pacte effectif : resolveSettings lu par le triage avec replis, A1 finalité, préambule boussoles, sous-titre, clause requireEffects en clair) + /textes/formules (FormulaEditor sur moteur unique + section médiane/cristallisation du Réglage collectif + binaire repositionné en outil d'exception) +8. 8. LES MANDATS — /mandats (compteurs bruts, AUCUNE jauge), fiche (Exercice du mandat, Feux de la rampe, rapports, révocation chaînée au protocole d'origine), wizard 3 étapes pré-rempli par R6 (règle d'élection affichée) +9. 9. L'OBSERVATOIRE ET LA BOUCLE APPRENANTE — /decisions/observatoire (stats + consigné/outillé, ressources par unité huile/eau, épreuves du réel, Révoquées — ce qu'on en a appris, élagage), l'épreuve du réel complète (tableau recherché/constaté, measurerIds saisissent), suggestions du Fil (récurrence⇒mandat, maturation⇒protocolisation, sunset, élagage) — le système se simplifie lui-même +10. 10. DÉCISION INCRÉMENTIELLE — découpage en micro-décisions (chainKind element), onglet Éléments, pondération des enjeux (Concern.priority), cartographie de clôture auto-générée, clôture du dossier par le geste du garant (framing→closed gardée) — livrée après le socle car elle compose des primitives déjà construites +11. 11. FINITION LIVRABLE — QA mobile+desktop zéro régression (480/768/1024, touch 2.25 rem), parcours complets sur les 2 seeds ET un collectif Page blanche (« Adopter notre Pacte » < 10 min avec un non-théoricien ; consigner→observer→protocoliser déroulé ; cristallisation et départage d'élection déroulés), test anti-lexique vert, a11y clavier (curseurs inclus)/contraste, print A4 vérifié, README + CONTRIBUTING + page-manifeste publique (sans vocabulaire de livraison, close sur « La ville change, le puits demeure »), npm run build zéro erreur, screenshots diff --git a/docs/dev/blueprint-v2-final.json b/docs/dev/blueprint-v2-final.json new file mode 100644 index 0000000..6682711 --- /dev/null +++ b/docs/dev/blueprint-v2-final.json @@ -0,0 +1,687 @@ +{ + "summary": "Confronter le blueprint v1 à la doctrine des livres + SejeteralO et produire le blueprint définitif vérifié", + "agentCount": 9, + "logs": [ + "4/4 critiques rendues — synthèse finale", + "Vérifications : reserves, reserves, reserves — 13 must-fix" + ], + "result": { + "final": { + "manifeste": "Personne n'a jamais vécu en démocratie — alors nous la rêvons de jour, et nous la commençons à notre échelle. libreDecision attrape chaque décision en une phrase et propose le chemin le plus court : décide seul et trace-le, décide sous mandat, écoute puis décide, ou décidez ensemble — jamais plus lourd que l'enjeu. Ici on ne délègue pas, on mandate, et on reprend ; on n'est pas consulté, on s'instruit, puis on se prononce ; on ne s'affronte pas en deux camps, on règle des curseurs — et la position de chacun reste visible dans le faisceau, la médiane n'écrase personne, la dernière main est toujours humaine. Les concernés en premier lieu ont voix par construction, chacun voit pourquoi ; lever la main « ça me concerne » est un droit, pas une demande. Chaque décision dit ce qu'elle engage, ce qu'elle cherche à changer, et repasse l'épreuve du réel : rien n'est définitif, toute décision en vigueur garde sa poignée, se tromper est une donnée qu'on consigne. Les règles du jeu elles-mêmes — le Pacte — s'écrivent au fil de l'eau, par ceux qu'elles gouvernent, sous les boussoles Autonomie, Équilibre, Liaison. Du trio d'amis à la toile Ğ1 : le même geste, dix secondes, et l'observatoire qui apprend de nous. La ville change, le puits demeure.", + "delta_v1": [ + { + "changement": "CollectiveSettings.protocolByRange.inertialBinary renommé 'large' (Id résolu par le Pacte, le nom ne présume plus la méthode). Gabarits blank/informal/association/cooperative/commune/symmetric : 'large' pointe le protocole nuancé 6 niveaux (nuancedMinParticipants/nuancedThresholdPct existants). SEULS free-currency et les seeds Ğ1 pointent le binaire inertiel. R5 >50 hors clause : défaut nuancé ; R3 (clause visée) garde clauseByInertia — héritage Ğ1 (33+59 clauses, D30M50B.1G.2) intact, à sa place.", + "raison_doctrinale": "« Le clivage binaire est un fléau contre la réflexion, un ennemi fatal : il la tue » ; « proscrire le binaire oui/non comme format par défaut » ; le binaire n'est légitime que « remis à sa place dans la boîte à outils » — jamais destination naturelle des grands corpus (le RIC disqualifié : « tir à la corde »)." + }, + { + "changement": "VoteMethod += 'parametric' (UI « Réglage collectif / décider au curseur »). Decision.paramSpec { params ≤7, constraint 'none'|'sum100', impactAttrKey? } ; ParamDef { key, label métier obligatoire, kind 'slider'|'share', min/max/step, unit?, baseline?, derived? } ; Vote.values?: number[] ; engine/parametric.ts (validateVote, resolveDerived, medianByElement, computeMyImpact, detectBimodality, crystallize). SPECS VERROUILLÉES (réparations) : médiane BASSE par élément (nombre pair ⇒ élément central inférieur — chaque valeur médiane est une valeur effectivement votée, donc conforme au step par construction, testée vitest) ; sum100 ⇒ EXACTEMENT une part 'derived' exigée à la création du paramSpec ; au vote, la géométrie clampe dynamiquement les parts votées pour que le derived reste dans ses bornes ET validateVote rejette sinon ; à la cristallisation, le derived n'est jamais agrégé : il est résolu depuis la médiane des parts votées, et s'il sort de ses bornes ⇒ clamp à la borne + renormalisation proportionnelle des parts non-derived (somme 100 restaurée, spec testée). Dégénérescences : 0 vote ⇒ baseline, jamais d'écran vide. Jamais dans le chemin 10 s : alternative « C'est un réglage » + suggérée quand la phrase contient nombre/%/montant.", + "raison_doctrinale": "« Raisonner curseur / table de mixage » est LE geste central des deux livres ; leçons SejeteralO « voter = régler une forme, pas choisir un camp », « médiane élément par élément » avec l'invariant « le résultat est lui-même une position que chacun aurait pu proposer » — invariant qui EXIGE la médiane basse et la résolution sous contrainte ; « robustesse numérique de série : dégénérescences spécifiées »." + }, + { + "changement": "RÉPARATION — Cristallisation = geste humain daté : la clôture paramétrique passe par VoteSession.status 'frozen' à closesAt (votes figés, médiane figée affichée) puis le GARANT (stewardIds, à défaut l'auteur) fait le geste « Cristalliser » → 'closed' + crystallizedById/crystallizedAt, la médiane devient position adoptée (ou rejected si quorum parametricMinParticipants non atteint — constaté au même geste). Relance au Fil du garant s'il tarde. Si detectBimodality positif, le geste affiche en évidence « Deux positions distinctes — cristalliser quand même / scinder / reformuler » : jamais de cristallisation automatique. Consent/nuancé/binaire gardent la clôture automatique (le moteur ne fait qu'y constater un seuil sur un contenu formulé par des humains) ; l'élection clôt automatiquement SAUF égalité (geste requis, cf. delta élection).", + "raison_doctrinale": "Tension du corpus explicitement tranchée : « l'agrégat éclaire une délibération, la cristallisation reste un geste humain distinct et daté » ; chez SejeteralO la publication est distincte de l'agrégation vivante ; un calcul qui produit un contenu adopté sans geste humain = « décision algorithmique », repoussoir absolu." + }, + { + "changement": "Matière : Decision.brief { context?, sources?, symptomsVsCauses?, effects: Effect[] } avec Effect { label, target?, measured? }. RÉPARATION — garde restreinte : ≥1 effet recherché exigé (state.ts, framing→voting / draft→voting) UNIQUEMENT pour les sessions collectives de poids binding/structural, gouverné par la clause de Pacte 'triage.requireEffects' ∈ 'none'|'structural'|'binding' (défaut 'binding' ; 'none' seedé dans blank ET informal) ; quand elle s'applique au structural, ≥1 effet doit porter une cible mesurable (target). Le consentement léger (weight light) et le trio n'y sont JAMAIS soumis ; le tunnel ne l'affiche jamais — la matière s'écrit en framing. L'épreuve du réel réutilise effects : tableau « recherché / cible / constaté » au-dessus des 3 boutons, mesures saisies par les measurerIds.", + "raison_doctrinale": "« Il n'est pas consulté, il est instruit » ; « aucun vote sans matière » — mais « jamais plus lourd que l'enjeu » (manifeste) : le collectif règle sa propre exigence (couche méta), et la boucle « acquisition collective des expériences » est fermée par le même champ." + }, + { + "changement": "RÉPARATION — Bloc « S'instruire » condensé : en salle de vote et fiche (framing/voting), un RÉSUMÉ COMPACT toujours visible avant les boutons (titre, effets recherchés, « Ce que ça engage ») avec versions+diff, avis/objections et provenance en DÉPLIABLES internes ; le bloc entier se replie automatiquement aux visites suivantes de la même personne (préférence locale par décision) — voter sur un titre nu reste impossible, re-voter reste léger.", + "raison_doctrinale": "« Aucun vote sans matière » concilié avec la leçon SejeteralO « agir d'abord, comprendre ensuite » : pas de mur de scroll mobile à chaque re-vote d'un vote révisable." + }, + { + "changement": "Ressources : Decision.resources { note, amount?, unit 'heures'|'€'|'DU'|string } + Decision.measurerIds à côté de stewardIds. Huile/eau : DU et € JAMAIS convertis ni additionnés (Observatoire : lignes par unité). RÉPARATION — localisation de la garde : la ligne « Ce que ça engage » est SUGGÉRÉE-dépliée dans le tunnel (toujours facultative, ne bloque jamais) ; l'exigence (weight != 'light' hors solo) vit UNIQUEMENT dans la garde d'ouverture de fenêtre/session de state.ts, même mécanique que brief.effects — le tunnel n'affiche jamais un champ obligatoire qui ne change pas le chemin.", + "raison_doctrinale": "« Chaque décision implique une allocation de ressources, donc une décision connexe incontournable » (MNP p.89) ; « des équipes pour réaliser et des équipes pour mesurer » ; l'huile et l'eau ne se mélangent jamais ; et la métrique produit « 2 gestes / 10 s » reste intacte pour le cas nominal." + }, + { + "changement": "Suppression de toute jauge de confiance sur les mandats. Bloc « Exercice du mandat » : faits comptés jamais synthétisés (« 12 décisions tracées · 1 objection · 2/2 rapports », listes cliquables). Le flux public devient « Feux de la rampe » (« toutes les décisions prises sous ce mandat — contestables »). Rendu seulement, zéro champ.", + "raison_doctrinale": "« On peut évaluer des choses ; mais les personnes ? Alerte rouge » ; « la confiance exclut le contrôle » — une jauge de CONFIANCE sur un pouvoir contrôlé est un oxymore ; « Feux de la rampe » est le mot doctrinal exact." + }, + { + "changement": "Protocol.ballot: 'open' | 'secret' (défaut 'open'), gouverné par le Pacte. Mode secret v2 : décomptes agrégés et commentaires anonymisés seuls, étiquette « secret d'affichage — démo » ; crypto réelle avec la synchro (v3, noté au contrat des stores).", + "raison_doctrinale": "« Modalité de vote secret vs ouvert comme paramètre de chaque décision » (leçon fouloscopie) — « l'outil doit savoir faire les deux, et le périmètre en décide » ; même honnêteté d'étiquetage que la gravure." + }, + { + "changement": "RÉPARATION — Historique des re-votes jamais opposable : la chaîne supersedesVoteId n'est JAMAIS affichée publiquement sous le nom d'un votant — visible du seul auteur du vote ; même en ballot 'open', seul le DERNIER vote actif d'une personne apparaît sous son nom, et le décompte agrégé est recalculé. Libellé : « Ton vote est remplaçable — ton historique n'est visible que de toi. » Invariant écrit au modèle + test.", + "raison_doctrinale": "Tension « Mémoire vive vs droit à l'erreur » tranchée par le corpus : « archiver les mandats, pardonner les positions ; l'historique des votes révisés n'est jamais opposable à une personne » — sinon contrôle des personnes entre elles, le glas doctrinal." + }, + { + "changement": "Gabarit 'blank' « Page blanche — observatoire d'abord » en tête de /creer : Pacte d'une seule clause (« nous consignons nos décisions ; le reste s'écrira au fil de l'eau ») + clause A1 finalité. RÉPARATION — cohérence settings : blank seede AU MINIMUM le protocole « Consentement » ; protocolByRange.consent est OBLIGATOIRE (invariant : tout collectif a un protocole de consentement, validé à l'import), les autres clés optionnelles ; resolveSettings spécifie le REPLI : toute clé non résolue retombe sur consent ; si consent introuvable (bundle corrompu), le triage route 'advice' avec bannière « Aucun protocole — crée-le ou décide sur avis » — jamais un crash. Les 5 gabarits existants réétiquetés « point de départ — tout est amendable par décision ».", + "raison_doctrinale": "« L'observatoire des décisions AVANT de protocoliser » ; contrat social « écrit au fil de l'eau, sans préfigurer » ; « une chorégraphie du don ne se télécharge pas en .ods » — et la robustesse spécifiée (« jamais d'écran vide ») fait partie de la leçon SejeteralO." + }, + { + "changement": "Gabarit 'symmetric' « Institution symétrique » : Pacte en transparence totale, TextDoc reference « Déclaration de symétrie », première décision « Déclarer notre symétrie », 2 mandats suggérés Ambassadeur et Négociateur. Données pures templates.ts.", + "raison_doctrinale": "« Tu ne veux pas — Ô Institution — tant pis, je crée une symétrie » (chapitre entier de MNP) ; le poids = le volume des personnes qui l'habitent ; représentants « ambassadeurs et négociateurs, jamais décideurs »." + }, + { + "changement": "DecisionRoute += 'record' + Decision.decidedHow. Alternative permanente « Déjà tranché — je le consigne » → adopted immédiat, zéro fenêtre (2 gestes). /decisions/memoire → /decisions/observatoire « L'Observatoire » ; ratio consigné/outillé = maturité ; maturation (famille R6, jamais bloquante) : ≥3 consignations partageant ≥2 tags ⇒ « Cette pratique est mûre — protocolise-la en quelques phrases » pré-remplissant une clause de Pacte.", + "raison_doctrinale": "« Consigner comment elles se prennent dans les faits, avant de protocoliser » puis « traduire en protocoles de quelques phrases quand c'est mûr » — le flux consigner→observer→protocoliser devient le chemin ordinaire ; l'inversion des noms (Mémoire/Observatoire) est corrigée." + }, + { + "changement": "chainKind += 'element' + Concern.priority?: 0|1|2|3. « Découper en micro-décisions » sur toute décision structural en framing ; onglet « Éléments » + « Cartographie de clôture » auto-générée. RÉPARATION — clôture du dossier : nouvelle transition framing→closed du parent, gardée par « tous les éléments dans un état terminal » ; jamais automatique : quand la garde est vraie, carte au Fil du garant « Le dossier est complet — clore et publier la cartographie de clôture » ; le geste clôt, la cartographie devient le PV du dossier. Libellé UI : « décision incrémentielle » réservé à CE mode.", + "raison_doctrinale": "La « décision incrémentielle » est le nom PROPOSÉ PAR L'AUTEUR (modèle Taïwan) : « chacun navigue dans les points qui le concernent, pondère ses préférences » ; « cartographie de clôture visible de tous » — et la clôture est un geste humain, cohérent avec la cristallisation." + }, + { + "changement": "Decision.baselineNote?: string (« Aujourd'hui : … ») suggéré routes advice/collective (jamais solo). Fiche : « Aujourd'hui → Proposé » ; ParamDef.baseline = marqueur « actuel » ; l'épreuve du réel relit l'écart. Les clauses gardent leur diff (qui EST leur baseline).", + "raison_doctrinale": "Leçon SejeteralO : « la baseline : toujours afficher l'existant — une décision se lit comme un écart au statu quo, mesurable »." + }, + { + "changement": "Person.attributes?: Record (déclarés, édités dans /donnees, jamais affichés à autrui ni agrégés) + ParamSpec.impactAttrKey. RÉPARATION — computeMyImpact 'linear-share' SPÉCIFIÉ : ne s'applique QUE si constraint='sum100' ET resources.amount présent ET impactAttrKey présent ; pour chaque part p (derived inclus) : montant(p) = resources.amount × p/100 ; ma quote-part(p) = montant(p) × attr(moi) / Σ attr(déclarants du corpus) ; résultat par curseur + total, calculé aux trois positions (la mienne / médiane / baseline). attr(moi) absent ou Σ=0 ⇒ null (jamais un chiffre inventé — la carte montre alors seulement l'écart collectif en valeurs de curseurs). L'exemple seedé « budget d'ateliers 1 200 € × 'heures/mois' » devient calculable. Étiquette « estimation sur données déclarées ».", + "raison_doctrinale": "Principe produit n°5 « qu'est-ce que ça change pour moi » ; SejeteralO « Mon prix au m³ » et son triptyque ma-position/médiane/référence — version local-first honnête, bornée, et désormais calculable ; le moteur sur données réelles importées reste v3." + }, + { + "changement": "Composant Faisceau : nuancé → histogramme des 6 niveaux sous la jauge + commentaires en liste — jamais un simple adopté/rejeté ; paramétrique → strip-plot par curseur (points translucides anonymes, médiane trait épais, baseline marqueur creux). detectBimodality ⇒ bannière non bloquante « Deux positions distinctes se dessinent sur X — scinder la question ou changer de référentiel ? » ouvrant le découpage ou une reformulation.", + "raison_doctrinale": "« La diversité reste visible : le faisceau » ; « la médiane peut masquer une distribution bimodale » ; le « changement de référentiel » doctrinal devient un geste produit." + }, + { + "changement": "Garde d'affluence dans state.ts : concernEscalateRatio atteint ⇒ la fenêtre/session NE PEUT PAS se clore sans traitement — élargir d'un cran (1 tap, re-triage journalisé) ou maintenir avec Decision.scopeKeptNote motivée publique, affichée en évidence.", + "raison_doctrinale": "La montée d'échelle est DÉCIDÉE (« on en change quand le peuple le décide ») — l'asymétrie de dérogation appliquée au périmètre, LE lieu doctrinal de la subsidiarité." + }, + { + "changement": "Circle.kind?: 'place'|'theme'|'team' — iconographie, groupement, ordre des suggestions seulement, jamais un droit. Hint non bloquant si 'team' > 12. Gabarits typent leurs cercles.", + "raison_doctrinale": "Les 3 vecteurs du périmètre : le topologique gagne une existence déclarative sans machinerie géo ; « au-delà de douze, les complications commencent »." + }, + { + "changement": "Collective.lineage?: { sourceSlug, exportedAt, sha256 } posé à l'import d'un bundle étranger (« essaimé de … »). PAS de parentCollectiveId ; pattern v3 documenté : une grappe = un collectif ordinaire dont les membres sont des mandataires d'autres collectifs.", + "raison_doctrinale": "La « scission féconde » exige la mémoire d'origine ; le corpus proscrit la fédération dans le modèle (« représentation, voix unique ») — composition pure." + }, + { + "changement": "Clause « A1 — Notre finalité » seedée dans chaque gabarit (texte sous vote permanent, défaut portant les boussoles), affichée sous le nom du collectif ; préambule de Pacte proposé et modifiable portant Autonomie–Équilibre–Liaison.", + "raison_doctrinale": "« Chaque groupe doit se donner une finalité » ; « boussoles proposées, jamais imposées » ; coexistence « sans être concurrents »." + }, + { + "changement": "Passerelles hors-écran : (1) CSS print A4 + « Imprimer » sur fiche (PV) et salle (affiche de session) ; (2) « agir en tant que » requalifié « mode atelier » via recordedById?: Id sur Vote/Advice/Objection/Assent (« saisi par X pour Y — atelier du ») ; (3) micro-copy « à trancher en présence ? » sur structural.", + "raison_doctrinale": "« Toute décision numérique doit être praticable sans écran » ; « intermédiation humaine pour les démunis du numérique » ; « plus le numérique prend de place, plus il faut en donner à la relation humaine »." + }, + { + "changement": "frontend/app/lexicon.ts : source UNIQUE des libellés français + test vitest anti-lexique (déléguer/délégation, dette, retard, en souffrance, corpus, snapshot, CONTRE, verdict, triage, oligarchie, peuple adulte…). Trois règles de ton : (1) tutoyer la personne dans ses gestes ; (2) actes collectifs à l'infinitif (« Adopter notre Pacte ») ; (3) sujet grammatical des phrases générées = le collectif ou la légitimité, jamais la formule/le système.", + "raison_doctrinale": "« L'auteur voudra retrouver ses mots exacts » ; « politiquement explicable » : c'est la légitimité qui exige, pas les maths — l'algorithme au service, jamais souverain." + }, + { + "changement": "Renommages UI (types inchangés) : carte /decider « Le chemin » (explanation seule, code R* au dépliable/PV/journal) ; « Concerné·e en premier lieu / en second lieu » (pastille pleine / main levée) ; chip « Concernés » ; panneau « Périmètre » ; « Mandats — les pouvoirs confiés » ; Fil « À toi de décider » compteur nu ; « Qui vote : 11 personnes — liste arrêtée le 12/08 » (W/T/M/B/G/C à l'Atelier seulement) ; échelle nuancée « Pas du tout · Plutôt non · Réservé · Plutôt oui · Oui · Tout à fait » ; revoyure « L'épreuve du réel » / « Le réel a-t-il suivi ? » / « Ça tient / À revoir / À révoquer » ; « C'est urgent » + badge « Décidé en urgence — le collectif ratifie » ; « Révoquées — ce qu'on en a appris » ; routes à la première personne ; sous-titre du Pacte « Notre contrat social — sacralisé, jamais immuable » ; fenêtre framing « N jours pour s'instruire et formuler des contre-propositions ».", + "raison_doctrinale": "« On ne délègue pas, on abandonne » ; « périmètre » est LE concept pivot ; le registre dette/peur est le couple de l'asservissement ; « le verdict n'est jamais un juge » ; « toute étiquette évacue la moitié de la population »." + }, + { + "changement": "Bouton « Explorer » en salle paramétrique : sandbox de la médiane, curseurs libres, dérivés + « Pour moi » recalculés en live, « Partir de la médiane / du statu quo / de ma position », « En faire ma position », étiquette « exploration — ne compte pas ». Rétro-manipulation résultat→contrainte : v3.", + "raison_doctrinale": "Leçon SejeteralO « manipulation de l'agrégat pour explorer » ; « déplacer, évaluer, ajuster, recommencer » ; abaisse le coût psychologique du premier vote." + }, + { + "changement": "Dépersonnalisation d'affichage : contenu et diff d'abord, auteur replié (visible au tap) dans le bloc formulation et la salle. Pas de mode anonyme réel en v2 mono-poste.", + "raison_doctrinale": "« On débat du contenu, pas de l'auteur » (@personne) — anti ad-hominem, anti-sauveur." + }, + { + "changement": "Désirabilité : tampon 井 incliné -10° « Décidé » (translateY 0.12s) + « C'est décidé — et révisable, comme tout ici » ; /creer se signe « Osons, nous verrons » ; manifeste public sans vocabulaire de livraison, clos sur « La ville change, le puits demeure » ; termes-diagnostics réservés à la page-manifeste.", + "raison_doctrinale": "« Rendre désirable plutôt que convaincre » exige que décider soit beau ; le couple plaisir+rêve." + }, + { + "changement": "RÉPARATION — Silence-vaut-accord résolu par l'entité Assent : « Ça me va » est stocké (Assent { decisionId, personId, recordedById? }, append-only, dans le Bundle). Règle unique des fenêtres d'objection : à échéance sans objection ouverte — reversibility 'easy' ⇒ adopted (silence vaut accord) ; costly/irreversible ⇒ adopted SEULEMENT si ≥1 Assent d'un concerné autre que l'auteur, sinon la fenêtre se PROLONGE d'un cran (windowEndsAt + objectionWindowHours, relance au Fil des concernés) — jamais d'adoption par pur silence hors easy. La contradiction v1 est levée, le comportement à échéance est défini.", + "raison_doctrinale": "« Il ne cède pas un consentement, il négocie — chaque virage » : hors du réversible, l'accord est un geste, pas une absence ; et le silence-vaut-accord réservé au réversible était déjà l'engagement du blueprint — désormais tenu mécaniquement." + }, + { + "changement": "RÉPARATION — Élection codable : Vote.choicePersonId?: Id (invariant : EXACTEMENT un de value / values / choicePersonId selon la méthode, validé par le store). Vote déposé sans choicePersonId = blanc (compte pour la participation, pas pour la désignation). engine/threshold.ts += electionResult(votes, corpus) : quorum optionnel electionMinParticipants (FormulaParams) sinon rejected ; désignation à la PLURALITÉ simple des derniers votes actifs ; ÉGALITÉ ⇒ résultat 'tie' avec exAequoIds — l'outil ne départage JAMAIS : la clôture propose une session de départage chaînée restreinte aux ex æquo (pré-remplie), ou le tirage au sort entre ex æquo si le protocole du Pacte l'a prévu d'avance. Salle élection : désignation par avatar, bouton « Vote blanc », règle affichée en une phrase.", + "raison_doctrinale": "L'élection « remise à sa place dans la boîte à outils » doit être réellement outillée pour les mandats ; « l'outil représente, agrège et éclaire — il ne décide jamais » : une égalité n'est pas tranchée par un algorithme, et le tirage au sort n'est légitime que décidé d'avance par le Pacte." + }, + { + "changement": "RÉPARATION — canTransition étendu : engine/state.ts expose canTransition(decision, to, ctx: { concerns: Concern[], settings: CollectiveSettings, session?: VoteSession, children?: Decision[], assents?: Assent[], objections?: Objection[] }) — les gardes d'affluence (|déclarés| vs |calculés|), de matière (requireEffects), de ressources, de frontière (windowSuspendedAt), d'Assent et de clôture de dossier (children terminaux) ont leurs entrées dans la signature ; le contexte est assemblé par le store, la fonction reste pure et testable.", + "raison_doctrinale": "Machine à états UNIQUE réellement implémentable : toute mutation passe par canTransition, et les gardes doctrinales (subsidiarité, instruction, consentement) sont du code testé, pas des intentions." + }, + { + "changement": "Verdict enrichi : parametricHint (nombre/%/montant détecté), alternatives permanentes « C'est un réglage — décidez au curseur », « Déjà tranché — je le consigne », binaire avec son coût (« deux camps, un perdant ») ; TriageContext.similarRecordedCount ; suggestion.kind += 'protocolize'.", + "raison_doctrinale": "« L'outil représente, agrège et éclaire — il ne décide jamais » : les modalités doctrinales sont proposées, jamais imposées." + } + ], + "modele_domaine": "// ─────────────────────────────────────────────────────────────\n// libreDecision v2 — noyau de domaine DÉFINITIF (frontend/app/types/domain.ts)\n// Local-first, prêt-synchro. Identifiants anglais, UI française via app/lexicon.ts.\n// INVARIANT 1 : LA DÉCISION EST L'OBJET PIVOT — mandats, clauses, révocations,\n// ratifications, éléments de dossier en sont des spécialisations chaînées.\n// INVARIANT 2 : l'unification est un fait d'architecture, JAMAIS un vocabulaire d'UI.\n// INVARIANT 3 : aucun agrégat/score sur une personne, nulle part — on évalue des choses.\n// INVARIANT 4 : aucun calcul ne produit un contenu adopté sans geste humain daté\n// (cristallisation paramétrique, départage d'élection, clôture de dossier).\n// ─────────────────────────────────────────────────────────────\n\ntype Id = string // UUIDv7 — triable par temps, prêt pour la synchro\ntype ISODate = string // ISO 8601 UTC\ntype Json = string | number | boolean | null | Json[] | { [k: string]: Json }\n\n// Toute entité : scoping collectif + horloge de synchro + soft-delete.\n// Prêt-synchro : jamais de suppression physique (archivedAt) ;\n// Vote/Advice/Objection/Assent append-only (merge trivial) ; le reste LWW par updatedAt.\ninterface Entity {\n id: Id\n collectiveId: Id\n createdAt: ISODate\n updatedAt: ISODate\n archivedAt?: ISODate\n}\n\n// ── Le tenant ────────────────────────────────────────────────\ntype CollectiveTemplate =\n | 'blank' // « Page blanche — observatoire d'abord » : Pacte 1 clause + A1, protocole Consentement SEUL\n | 'informal' | 'association' | 'cooperative' | 'commune'\n | 'free-currency' // héritage Ğ1 — SEUL gabarit où protocolByRange.large = binaire inertiel\n | 'symmetric' // « Institution symétrique » — le geste fondateur militant (MNP)\n\ninterface Collective {\n id: Id\n slug: string\n name: string\n color: string // hex accent\n icon: string // i-lucide-*\n template: CollectiveTemplate\n isTransparent: boolean\n pactDocId: Id // TextDoc role='pact' — les réglages EN document voté\n rootCircleId: Id\n lineage?: { // essaimage : posé auto à l'import d'un bundle d'un autre collectif\n sourceSlug: string; exportedAt: ISODate; sha256: string\n } // PAS de parentCollectiveId — la fédération n'est jamais une table\n createdAt: ISODate\n updatedAt: ISODate\n}\n\n// ── Les gens ─────────────────────────────────────────────────\ninterface Person extends Entity {\n displayName: string\n isMe: boolean // profil local (v2 mono-poste ; « mode atelier » pour la saisie en présence)\n attributes?: Record // attributs DÉCLARÉS par la personne (clés libres : 'heures/mois'…),\n // édités dans /donnees — servent UNIQUEMENT la carte « Pour moi » ;\n // jamais affichés à autrui, jamais agrégés (invariant 3)\n duniterAddress?: string // SS58 — réservé auth Duniter (hors scope v2)\n wotStatus?: 'member' | 'smith'\n}\n\ninterface Circle extends Entity {\n name: string\n purpose: string\n kind?: 'place' | 'theme' | 'team' // lieu / thème / équipe — iconographie, groupement des sélecteurs,\n // ordre des suggestions SEULEMENT, jamais un droit ;\n // hint non bloquant si 'team' > 12 (« au-delà de douze… »)\n memberIds: Id[] // v2 : liste nominative = SEULE règle d'appartenance (auditable)\n parentCircleId?: Id // emboîtement (couches d'oignon) — racine = rootCircle\n domains: string[] // tags — SUGGESTION/découverte uniquement, jamais un droit de vote\n}\n// Corpus d'une décision = union CALCULABLE : membres des cercles du scope\n// ∪ personnes nommées ∪ titulaires des mandats dont le domaine intersecte.\n// Chaque inclusion garde sa raison (Concern.reason) — auditable en un tap.\n\n// ── LA DÉCISION (pivot) ──────────────────────────────────────\ntype Reversibility = 'easy' | 'costly' | 'irreversible'\ntype Weight = 'light' | 'binding' | 'structural'\n\ntype DecisionRoute =\n | 'solo' // « Je décide » (trace facultative hors mandat)\n | 'mandate' // « Je décide, sous mandat » (trace obligatoire + fenêtre d'objection)\n | 'transmit' // « Je transmets » — le mandat d'un autre couvre\n | 'advice' // « J'écoute, puis je décide »\n | 'collective' // « Nous décidons » (modalité du protocole)\n | 'record' // « Déjà tranché — je le consigne » : l'observatoire en un geste, adopted immédiat\n\ntype DecisionStatus =\n | 'draft' | 'advice' | 'objection' | 'framing' | 'voting'\n | 'adopted' | 'rejected' | 'revoked' | 'transmitted' | 'closed'\n\ntype TriageRule = 'R-U' | 'R0a' | 'R0b' | 'R0c' | 'R2' | 'R3' | 'R4' | 'R5' | 'R6'\n\n// La matière à réflexion — « il n'est pas consulté, il est instruit »\ninterface Effect {\n label: string // effet recherché, une ligne\n target?: string // cible mesurable optionnelle (« ≤ 400 € », « +5 membres »)\n measured?: { note: string; at: ISODate; byId: Id } // constaté — saisi par les measurerIds à la revoyure\n}\ninterface Brief {\n context?: string\n sources?: { label: string; url: string }[]\n symptomsVsCauses?: string // qualification doctrinale du sujet\n effects: Effect[] // GARDE (state.ts, ouverture de session collective UNIQUEMENT) :\n // exigé si weight ∈ requireEffects du Pacte ('binding' couvre binding+structural) ;\n // structural sous garde ⇒ ≥1 effet avec target. JAMAIS pour weight 'light',\n // JAMAIS dans le tunnel, JAMAIS en solo/record.\n}\n\n// La décision connexe de ressources — « chaque décision implique une allocation »\ninterface Resources {\n note: string // « Ce que ça engage » en une phrase\n amount?: number\n unit?: 'heures' | '€' | 'DU' | string // huile/eau : unités JAMAIS converties ni additionnées entre elles\n}\n\n// Le réglage collectif — « raisonner curseur / table de mixage »\ntype ParamKind = 'slider' | 'share' // 'curve' réservé v3 (SejeteralO complet)\ninterface ParamDef {\n key: string\n label: string // libellé métier OBLIGATOIRE — jamais de a, b, c bruts\n kind: ParamKind\n min: number; max: number; step: number\n unit?: string\n baseline?: number // statu quo — marqueur creux toujours affiché\n derived?: boolean // résolu par la contrainte, jamais voté — étiquette « calculé »\n}\ninterface ParamSpec {\n params: ParamDef[] // ≤ 7 (leçon SejeteralO : petit espace manipulable)\n constraint: 'none' | 'sum100' // sum100 : les 'share' totalisent 100 ; EXACTEMENT UNE part 'derived'\n // exigée (validé à la création du spec) — la variable d'absorption ;\n // résolue linéairement (100 − Σ autres)\n impactAttrKey?: string // clé de Person.attributes → carte « Pour moi » ('linear-share')\n}\n\ninterface Decision extends Entity {\n authorId: Id\n title: string // LA phrase de capture — seul champ obligatoire\n body?: string // progressive disclosure\n brief?: Brief // matière — cf. garde d'ouverture ci-dessus\n baselineNote?: string // « Aujourd'hui : … » — suggéré routes advice/collective, jamais solo\n resources?: Resources // SUGGÉRÉ-déplié au tunnel (jamais bloquant) ; EXIGÉ à la garde\n // d'ouverture de fenêtre/session (state.ts) si weight != 'light' hors solo\n tags: string[] // découverte + récurrence — JAMAIS un corpus\n\n // — verdict de triage (pré-rempli par le moteur, ajustable) —\n reversibility: Reversibility\n weight: Weight\n urgent: boolean\n scope: {\n selfOnly: boolean\n circleIds: Id[] // plancher : élargir libre, JAMAIS restreindre sous le calcul\n personIds: Id[]\n }\n route: DecisionRoute\n triageRule: TriageRule // relégué au dépliable « pourquoi ? », PV, journal — jamais en carte\n routeOverridden: boolean\n overrideNote?: string // OBLIGATOIRE si allègement (asymétrie de dérogation)\n scopeKeptNote?: string // affluence atteinte + périmètre maintenu ⇒ note motivée PUBLIQUE obligatoire\n\n // — spécialisations chaînées (le pliage génératif) —\n underMandateId?: Id // décidée SOUS ce mandat → feux de la rampe du mandat\n createsMandate?: MandateDraft\n amendsClauseId?: Id\n protocolId?: Id // résolu par le Pacte, ajustable en alourdissant\n paramSpec?: ParamSpec // si modalité paramétrique (« Réglage collectif »)\n parentDecisionId?: Id\n chainKind?: 'ratification' | 'revision' | 'revocation' | 'element'\n // 'element' : micro-décision d'un dossier découpé (« décision incrémentielle »).\n // Parent d'éléments : transition framing→closed gardée par\n // « tous les enfants element en état terminal » — GESTE du garant\n // (carte au Fil « Le dossier est complet — clore »), jamais automatique ;\n // la cartographie de clôture devient le PV du dossier.\n decidedHow?: string // route 'record' : « comment ça s'est décidé », une phrase\n\n // — temps & fenêtres —\n status: DecisionStatus\n windowEndsAt?: ISODate\n windowSuspendedAt?: ISODate // objection de frontière → compte à rebours SUSPENDU (visible)\n decidedAt?: ISODate\n review?: Review // obligatoire si irreversible || structural\n sunsetAt?: ISODate\n stewardIds: Id[] // garants — RÉALISENT (et cristallisent le paramétrique, clôturent le dossier)\n measurerIds: Id[] // mesureurs — CONSTATENT (saisissent Effect.measured) : la paire doctrinale\n\n // — preuve & visibilité —\n visibility: 'private' | 'scope' | 'collective' // anti-flicage : trace maîtrisée hors mandat\n engraving?: Engraving\n}\n\ninterface Review {\n dueAt: ISODate\n verdict?: 'confirmed' | 'revise' | 'revoke' // UI : « Ça tient / À revoir / À révoquer »\n note?: string\n decidedAt?: ISODate\n}\n\ninterface Engraving {\n sha256: string\n engravedAt: ISODate\n proofLevel: 'local' // v2 : UNIQUEMENT 'local' — « empreinte locale — démo »\n ipfsCid?: string // réservé v3 — jamais affiché tant que non réel\n chainRef?: string\n}\n\n// ── Inclusion & fenêtres ─────────────────────────────────────\ninterface Concern extends Entity {\n decisionId: Id\n personId: Id\n origin: 'computed' | 'declared' // UI : « concerné·e en premier lieu » / « en second lieu »\n reason: string // « membre du cercle Forgerons » — affichée au tap\n declaredNote?: string\n beforeSnapshot: boolean // avant liste arrêtée ⇒ vote ; après ⇒ voix consultative\n priority?: 0 | 1 | 2 | 3 // « pondère tes enjeux » — par élément d'un dossier ;\n // nourrit la cartographie de clôture, jamais le droit de vote\n}\n\ninterface Assent extends Entity { // « Ça me va » STOCKÉ — l'accord explicite des fenêtres d'objection\n decisionId: Id\n personId: Id\n recordedById?: Id // mode atelier\n}\n// RÈGLE DES FENÊTRES D'OBJECTION (state.ts) : à échéance sans objection ouverte —\n// easy ⇒ adopted (silence vaut accord) ; costly/irreversible ⇒ adopted SEULEMENT si\n// ≥1 Assent d'un concerné ≠ auteur, sinon PROLONGATION d'un cran (+objectionWindowHours,\n// relance au Fil) — jamais d'adoption par pur silence hors easy.\n\ninterface Objection extends Entity {\n decisionId: Id\n personId: Id\n kind: 'content' | 'boundary' // boundary SUSPEND la fenêtre — la frontière prime sur le fond\n argument: string\n status: 'open' | 'withdrawn' | 'integrated' | 'escalated'\n resolutionNote?: string\n recordedById?: Id // mode atelier : « saisi par X pour Y — atelier du »\n}\n\ninterface Advice extends Entity {\n decisionId: Id\n personId: Id\n position: 'favorable' | 'reserved' | 'unfavorable'\n note?: string\n recordedById?: Id // mode atelier\n}\n\n// ── Le pouvoir ───────────────────────────────────────────────\ninterface Mandate extends Entity {\n title: string\n holderId: Id\n originDecisionId: Id // le mandat EST né d'une décision — cliquable\n domain: { circleIds: Id[]; tags: string[] }\n startsAt: ISODate\n endsAt: ISODate // toujours borné\n electorCircleId: Id // corpus d'élection = corpus de révocation\n nominationMethod: 'ratified-self' | 'election-no-candidate' | 'nuanced-vote' | 'consent' | 'draw' | 'rotation'\n reports: MandateReport[]\n status: 'proposed' | 'active' | 'expired' | 'revoked'\n // AUCUN agrégat/jauge : la fiche affiche des faits comptés (« Exercice du mandat »),\n // jamais synthétisés ; le flux public = « Feux de la rampe »\n}\ninterface MandateReport { dueAt: ISODate; deliveredAt?: ISODate; content?: string }\ninterface MandateDraft {\n title: string\n domainCircleIds: Id[]\n domainTags: string[]\n durationDays: number\n reportEveryDays?: number\n}\n\n// ── Les textes ───────────────────────────────────────────────\ninterface TextDoc extends Entity {\n slug: string\n title: string\n role: 'pact' | 'reference'\n description: string // Pacte seedé : « Notre contrat social — sacralisé, jamais immuable »\n provenance?: Provenance\n}\ninterface Provenance {\n sources: { title: string; url: string; date?: string; version?: string }[]\n voteRecord?: {\n url: string; modeParams: string; period: string\n result: { for: number; against: number; invalid?: number; wotSize: number; thresholdRequired: number; status: string }\n }\n contributors?: string[]\n notes?: string // typos des textes votés : SIGNALÉES, jamais corrigées sans vote\n}\n\ntype InertiaPreset = 'low' | 'standard' | 'high' | 'max'\n// Câblage RÉEL : low={M:50,G:0.1} standard={M:50,G:0.2} high={M:60,G:0.4} max={M:66,G:0.6} (B=0.1, C=0)\n\ninterface Clause extends Entity {\n docId: Id\n section: string\n position: number\n code: string\n title: string\n inertia: InertiaPreset\n currentVersionId?: Id\n settingKey?: string // clauses du Pacte : ex. 'triage.smallGroupMax'\n}\n\ninterface ClauseVersion extends Entity {\n clauseId: Id\n decisionId: Id // TOUTE version naît d'une décision (y compris fondatrice seedée)\n versionLabel: string\n content: string\n settingValue?: Json\n status: 'current' | 'proposed' | 'superseded' | 'rejected'\n adoptedAt?: ISODate\n}\n\n// ── Modalités & vote ─────────────────────────────────────────\ntype VoteMethod = 'consent' | 'binary' | 'nuanced' | 'parametric' | 'election'\n// UI : consentement · pour/contre (outil de dernier recours) · nuancé ·\n// « Réglage collectif » (au curseur) · élection\n\ninterface Protocol extends Entity {\n name: string\n method: VoteMethod\n description: string\n durationDays: number\n ballot: 'open' | 'secret' // décidé par le Pacte — défaut 'open' (feux de la rampe).\n // v2 mono-poste : « secret d'affichage — démo » (décomptes agrégés\n // et commentaires anonymisés seuls) ; crypto réelle avec la synchro (v3)\n formula: FormulaParams\n modeParams: string // DSL DÉRIVÉ affiché : 'D30M50B.1G.2S.1' — jamais édité à la main\n pactClauseId?: Id // référencé par le Pacte ⇒ le modifier = amender le Pacte\n}\ninterface FormulaParams {\n majorityPct: number // M\n baseExponent: number // B (défaut 0.1)\n gradientExponent: number // G — l'inertie\n constantBase: number // C (défaut 0)\n smithExponent?: number\n techcommExponent?: number\n nuancedMinParticipants?: number\n nuancedThresholdPct?: number\n parametricMinParticipants?: number // quorum du réglage collectif — non atteint ⇒ rejected au geste\n electionMinParticipants?: number // quorum de l'élection — non atteint ⇒ rejected\n tieBreak?: 'runoff' | 'draw' // départage d'élection DÉCIDÉ D'AVANCE par le Pacte\n // (défaut 'runoff' : session de départage chaînée entre ex æquo ;\n // 'draw' : tirage au sort entre ex æquo — jamais choisi par le moteur)\n}\n\ninterface VoteSession extends Entity {\n decisionId: Id\n protocolId: Id\n corpusPersonIds: Id[] // SNAPSHOT à l'ouverture — UI : « liste arrêtée le … »\n corpusSize: number // W — plus jamais 0 (lettre réservée à l'Atelier)\n opensAt: ISODate\n closesAt: ISODate\n status: 'open' | 'frozen' | 'closed'\n // 'frozen' : PARAMÉTRIQUE uniquement — à closesAt les votes sont figés, la médiane figée est affichée,\n // et la session ATTEND le geste humain du garant (relance au Fil s'il tarde).\n crystallizedById?: Id // le geste « Cristalliser » : qui, quand — daté et signé\n crystallizedAt?: ISODate\n outcome?: 'adopted' | 'rejected' | 'tie'\n // 'tie' : élection à égalité — l'outil ne départage JAMAIS : session de départage chaînée\n // (restreinte aux ex æquo) ou tirage au sort SI le protocole l'a prévu (tieBreak).\n // Consent/nuancé/binaire : open→closed automatique à closesAt (le moteur constate un seuil\n // sur un contenu formulé par des humains). Paramétrique : open→frozen→closed (geste).\n // INVARIANT : tallies, histogrammes ET médianes TOUJOURS recalculés depuis Vote[] — jamais dénormalisés.\n}\n\ninterface Vote extends Entity {\n sessionId: Id\n voterId: Id\n value?: 'for' | 'against' | 0 | 1 | 2 | 3 | 4 | 5\n // nuancé UI : 0 Pas du tout · 1 Plutôt non · 2 Réservé · 3 Plutôt oui · 4 Oui · 5 Tout à fait\n values?: number[] // paramétrique — ordre de paramSpec.params HORS derived\n choicePersonId?: Id // élection — désignation ; vote déposé sans choicePersonId = BLANC\n // (compte pour la participation, pas pour la désignation)\n // INVARIANT : EXACTEMENT UN de value / values / choicePersonId selon la méthode (validé par le store) —\n // sauf blanc d'élection (aucun des trois, méthode 'election' seulement)\n comment?: string // OBLIGATOIRE si négatif (against, 0, 1)\n supersedesVoteId?: Id // re-vote audité — PRIVACY : la chaîne n'est JAMAIS affichée\n // publiquement sous le nom d'un votant ; visible du SEUL auteur ;\n // même en ballot 'open', seul le DERNIER vote actif apparaît sous un nom,\n // le décompte agrégé est recalculé (« pardonner les positions »)\n recordedById?: Id // mode atelier\n}\n\n// ── Réglages résolus du Pacte (fonction pure — PAS une table settings) ──\ninterface CollectiveSettings { // resolveSettings(pactClauses, versions)\n triage: {\n smallGroupMax: number // défaut 5\n collectiveMin: number // défaut 50\n consentMax: number // défaut 7\n objectionWindowHours: number // défaut 48\n adviceWindowHours: number // défaut 72\n framingDays: number // défaut 14 — « s'instruire et formuler des contre-propositions »\n concernEscalateRatio: number // défaut 0.5 — atteint ⇒ traitement OBLIGATOIRE (garde state.ts)\n recurrenceThreshold: number // défaut 3\n reviewDelayDays: number // défaut 90\n requireEffects: 'none' | 'structural' | 'binding'\n // clé 'triage.requireEffects' — gouverne la garde matière des sessions COLLECTIVES :\n // 'binding' = binding ET structural (défaut association/cooperative/commune/free-currency/symmetric) ;\n // 'structural' = structural seul ; 'none' seedé dans blank et informal.\n // Sous garde, structural ⇒ ≥1 effet avec cible mesurable (target).\n }\n protocolByRange: {\n consent: Id // OBLIGATOIRE — invariant : tout collectif a un protocole Consentement\n // (tout gabarit le seede, l'import le valide)\n nuanced?: Id\n large?: Id // ex-inertialBinary : le nom ne présume plus la méthode.\n // Gabarits standard → nuancé ; free-currency → binaire inertiel\n parametric?: Id // « Réglage collectif »\n election?: Id\n clauseByInertia?: Record // l'héritage Ğ1 vit ici, intact\n }\n // REPLI SPÉCIFIÉ (resolveSettings) : toute clé optionnelle non résolue retombe sur consent ;\n // consent introuvable (bundle corrompu) ⇒ le triage route 'advice' avec bannière\n // « Aucun protocole — crée-le ou décide sur avis » — JAMAIS un crash.\n}\n\n// ── Moteurs purs (frontend/app/engine/ — implémentation UNIQUE, vitest) ──\n// threshold.ts : wotThreshold(W,T,{M,B,G,C}) — port exact de threshold.py (ceil) ;\n// + smithThreshold, techcommThreshold, nuancedResult (histogramme 6 niveaux exposé) ;\n// + electionResult(votes, corpus, formula) → { winnerId } | { outcome:'tie', exAequoIds }\n// | { outcome:'rejected' } — pluralité simple des derniers votes actifs, blancs comptés\n// en participation, quorum electionMinParticipants, JAMAIS de départage moteur.\n// SPEC EXÉCUTABLE : cas Forgeron W=7224 T=120 M50 B.1 G.2 ⇒ 94.\n// parametric.ts : validateVote(spec, values) — bornes/step/clamp dynamique sum100, sanitize NaN/inf ;\n// resolveDerived(spec, values) — sum100 linéaire (100 − Σ autres), étiquette « calculé » ;\n// medianByElement(votes) — MÉDIANE BASSE par curseur (pair ⇒ élément central inférieur :\n// chaque valeur médiane est une valeur effectivement votée ⇒ step respecté par\n// construction — « une position que chacun aurait pu proposer », testé) ;\n// crystallize(spec, votes) — médiane basse des parts votées PUIS resolveDerived ;\n// derived hors bornes ⇒ clamp à la borne + renormalisation proportionnelle des parts\n// non-derived (somme 100 restaurée) — spec testée ;\n// computeMyImpact(spec, resources, values, myAttr, corpusAttrs) — 'linear-share' :\n// UNIQUEMENT si constraint='sum100' && resources.amount && impactAttrKey ;\n// montant(p) = amount × p/100 ; maPart(p) = montant(p) × myAttr / Σ corpusAttrs ;\n// par curseur + total, aux trois positions (mienne/médiane/baseline) ;\n// myAttr absent ou Σ=0 ⇒ null — JAMAIS un chiffre inventé ;\n// detectBimodality(values[]) — plus grand écart entre valeurs triées vs dispersion,\n// jamais bloquant. Dégénérescences : 0 vote ⇒ baseline, jamais d'écran vide.\n// triage.ts : triage(input, ctx, settings) → Verdict (règles ci-dessous)\n// modeParams.ts : parse/format du DSL\n// settings.ts : resolveSettings(clauses, versions) → CollectiveSettings (défauts + replis spécifiés)\n// state.ts : canTransition(decision, to, ctx: { concerns: Concern[], settings: CollectiveSettings,\n// session?: VoteSession, children?: Decision[], assents?: Assent[],\n// objections?: Objection[] }) — machine à états UNIQUE, gardes :\n// windowSuspendedAt (frontière) ; affluence (ratio ⇒ élargir ou scopeKeptNote) ;\n// matière (requireEffects) ; ressources (weight != light hors solo) ;\n// Assent (fenêtres non-easy) ; dossier (framing→closed si children terminaux, geste) ;\n// cristallisation (paramétrique : closed exige crystallizedById).\n// impact.ts : computeConcerned(scope, circles, mandates) → { personId, reason }[]\n// app/lexicon.ts : source UNIQUE des libellés français + test vitest anti-lexique\n// (déléguer, délégation, dette, retard, en souffrance, corpus, snapshot,\n// CONTRE, verdict, triage, oligarchie, peuple adulte…).\n// Règles de ton : (1) tutoyer la personne dans ses gestes ;\n// (2) actes collectifs à l'infinitif (« Adopter notre Pacte ») ;\n// (3) sujet des phrases générées = personnes/collectif, JAMAIS la formule.\n\ninterface TriageInput {\n title: string; tags: string[]\n scope: Decision['scope']\n reversibility: Reversibility\n weight: Weight\n urgent: boolean\n amendsClauseId?: Id\n}\ninterface TriageContext { // assemblé par le store, consommé pur — testable sans I/O\n myActiveMandates: Mandate[]\n otherActiveMandates: Mandate[]\n matchingClauses: Clause[] // index Q0 « déjà décidé ? » — le MÊME index que Cmd+K\n similarRecentCount: number // décisions adoptées partageant ≥2 tags sur 90 j\n similarRecordedCount: number // consignations 'record' partageant ≥2 tags — maturation\n computedConcernedIds: Id[]\n}\ninterface Verdict {\n route: DecisionRoute\n rule: TriageRule\n explanation: string // UNE phrase française — affichée SEULE, sans code de règle\n protocolId?: Id\n windowHours?: number\n framingDays?: number\n reviewRequired: boolean\n engravingSuggested: boolean\n conservatoryChain?: boolean\n parametricHint?: boolean // nombre/%/montant détecté ⇒ « C'est un réglage » mis en avant\n alternatives: { route: DecisionRoute; label: string; cost: string }[]\n // toujours présentes : « C'est un réglage — décidez au curseur » (parametric) ;\n // « Déjà tranché — je le consigne » (record) ; le binaire avec son coût (« deux camps, un perdant »)\n suggestion?: { kind: 'claim-mandate' | 'create-rule' | 'protocolize'; prefill: Json }\n}\n\n// ── Persistance locale & synchro future ──────────────────────\ninterface Bundle { // export/import ET seeds — le MÊME chemin de code\n schemaVersion: 2\n exportedAt: ISODate\n collective: Collective\n people: Person[]; circles: Circle[]\n decisions: Decision[]; concerns: Concern[]; objections: Objection[]; advices: Advice[]\n assents: Assent[]\n mandates: Mandate[]\n docs: TextDoc[]; clauses: Clause[]; versions: ClauseVersion[]\n protocols: Protocol[]; sessions: VoteSession[]; votes: Vote[]\n}\n// data/persistence.ts : IndexedDB (idb-keyval), clé `ld2:`, débounce 500 ms.\n// importBundle() valide protocolByRange.consent + pose Collective.lineage si bundle étranger.\n// Prêt-synchro : UUIDv7, updatedAt (LWW), archivedAt, Vote/Advice/Objection/Assent append-only.\n// Contrat des stores Pinia inchangé — l'adaptateur FastAPI futur ne change AUCUN écran.\n// NOTES contrat : ballot 'secret' devra devenir cryptographique côté serveur (v3) ;\n// la privacy des chaînes supersedesVoteId (visibles du seul auteur) est un invariant du contrat.", + "cycle_de_vie": "ÉMERGENCE — la capture : une phrase dans « Qu'est-ce que tu décides ? » (Fil ou FAB). Crée Decision{status:'draft'}. Dix secondes visées, aucun autre champ obligatoire. Alternative permanente : « Déjà tranché — je le consigne » (route 'record') → adopted immédiat avec decidedHow, zéro fenêtre — l'observatoire en un geste, 2 gestes comme le solo.\n\nTRIAGE — le chemin : triage() pur recalcule à chaque geste ; valider fait passer draft → l'état de départ de la route. Toujours expliqué (une phrase française, jamais de code), toujours contournable (alourdir libre 1 tap ; alléger = note obligatoire + fenêtre d'objection imposée — asymétrie journalisée). La ligne « Ce que ça engage » se DÉPLIE en suggestion si weight binding/structural hors solo mais ne bloque JAMAIS le tunnel ; « Aujourd'hui : … » suggéré sur advice/collective.\n\nFORMULATION (route collective, poids binding/structural) : status 'framing' — « N jours pour s'instruire et formuler des contre-propositions » (framingDays, défaut 14). La matière (brief) s'édite ICI ; contre-propositions bornées (ClauseVersions ou variantes de body avec diff, contenu d'abord, auteur replié). Sur toute décision structural en framing : « Découper en micro-décisions » — chaque élément part en /decider?parent=&chain=element avec son propre triage/état/modalité ; les concernés pondèrent leurs enjeux (Concern.priority). GARDES D'OUVERTURE (framing→voting ou draft→voting, state.ts) : (a) matière — ≥1 effet recherché exigé si weight ∈ requireEffects du Pacte ('binding' couvre binding+structural ; 'none' dans blank/informal) ; structural sous garde ⇒ ≥1 effet mesurable (target) ; (b) ressources — resources.note exigé si weight != 'light' hors solo. Les deux gardes vivent à l'ouverture, jamais au tunnel.\n\nDÉLIBÉRATION — selon la route :\n- 'solo' selfOnly : draft → adopted immédiat (2 gestes). Trace facultative, visibility 'private' par défaut.\n- 'record' : draft → adopted immédiat, decidedHow posé, trace pure.\n- 'mandate' et solo-avec-tiers-dérogatoire : draft → objection (fenêtre 48 h, concernés notifiés au Fil, « Ça me va / J'objecte » — le « Ça me va » crée un Assent). À échéance sans objection ouverte : easy ⇒ adopted (silence vaut accord) ; costly/irreversible ⇒ adopted SEULEMENT si ≥1 Assent d'un concerné ≠ auteur, sinon la fenêtre se PROLONGE d'un cran (+objectionWindowHours, relance au Fil) — jamais d'adoption par pur silence hors easy. Objection 'content' maintenue → escalade framing/voting. Objection 'boundary' → windowSuspendedAt, compte à rebours SUSPENDU.\n- 'advice' : draft → advice (72 h) ; avis déposés ; l'auteur décide → adopted (avis annexés). Objection formelle → escalade collective/consent.\n- 'collective' : framing? → voting : VoteSession créée, liste arrêtée (union calculée + déclarés avant arrêt), W fixé. Modalités : consentement (zéro objection maintenue) ; nuancé (nuancedResult, histogramme visible en cours) ; binaire inertiel (votesFor ≥ wotThreshold) ; PARAMÉTRIQUE : curseurs nommés + statu quo + « calculé » + faisceau + médiane vivante + « Pour moi » + Explorer — values[] révisable jusqu'à closesAt (supersedesVoteId, historique visible du seul auteur) ; élection (désignation par avatar, blanc possible).\n CLÔTURE : consent/nuancé/binaire → automatique à closesAt (le moteur constate un seuil sur un contenu formulé par des humains). PARAMÉTRIQUE → à closesAt la session passe 'frozen' (votes figés, médiane figée affichée) puis le GARANT cristallise (« Cristalliser » → 'closed', crystallizedById/At, médiane basse = position adoptée ; quorum parametricMinParticipants non atteint ⇒ rejected constaté au même geste ; relance au Fil s'il tarde ; bimodalité ⇒ le geste affiche « Deux positions distinctes — cristalliser quand même / scinder / reformuler », jamais d'automatisme). ÉLECTION → automatique (pluralité, quorum electionMinParticipants) SAUF égalité : outcome 'tie', l'outil ne départage jamais — session de départage chaînée entre ex æquo, ou tirage au sort si le Pacte l'a prévu (tieBreak).\n\nGARDE D'AFFLUENCE (transversale, state.ts) : si |déclarés| ≥ concernEscalateRatio × |calculés|, la fenêtre/session NE PEUT PAS se clore sans traitement — élargir d'un cran (1 tap, re-triage journalisé) OU scopeKeptNote motivée publique.\n\nADOPTION : decidedAt posé. Micro-célébration : tampon 井 « Décidé » + « C'est décidé — et révisable, comme tout ici ». amendsClauseId → ClauseVersion 'current' automatique. createsMandate → Mandate 'active'. structural/irreversible → review.dueAt + gravure proposée. sunsetAt optionnel. Mode secret : décomptes agrégés seuls (« secret d'affichage — démo »).\n\nRÉALISATION / ÉVALUATION : stewardIds réalisent ; measurerIds constatent (Effect.measured). À review.dueAt : « L'ÉPREUVE DU RÉEL » — « Le réel a-t-il suivi ? », tableau recherché/cible/constaté AU-DESSUS des 3 boutons « Ça tient / À revoir / À révoquer » ; écart à baselineNote relu ; ressources réexaminées. 'confirmed' : review close (one-shot → 'closed') ; 'revise'/'revoke' : décision enfant chaînée sous protocole et inertie D'ORIGINE.\n\nDOSSIER DÉCOUPÉ : quand tous les éléments sont en état terminal, carte au Fil du garant « Le dossier est complet — clore et publier la cartographie de clôture » ; le geste fait framing→closed du parent, la cartographie devient le PV du dossier. Jamais automatique.\n\nRÉVISION / RÉVOCATION : toute décision adoptée porte « Remettre en question » — /decider pré-rempli, protocole et inertie d'origine affichés. Enfant 'revocation' adoptée → parent 'revoked' (« Révoquées — ce qu'on en a appris »).\n\nURGENCE : « C'est urgent » — si non irréversible, voie la plus légère MAINTENANT (badge « Décidé en urgence — le collectif ratifie », fenêtre ×2) + ratification chaînée automatique. Irréversible ⇒ refusé : « Irréversible : l'urgence ne peut pas contourner le collectif ».\n\nTRANSMISSION : 'transmit' → 'transmitted' (terminal pour l'auteur) ; le mandataire reprend ou l'auteur conteste le mandat.\n\nMACHINE À ÉTATS UNIQUE (engine/state.ts) : draft→{advice,objection,framing,voting,adopted,transmitted} ; advice→{adopted,voting} ; objection→{adopted,framing,voting} ; framing→{voting,closed} (closed : dossier complet, geste) ; voting→{adopted,rejected} ; adopted→{revoked,closed} ; + gardes windowSuspendedAt, affluence, matière, ressources, Assent, cristallisation. Toute mutation passe par canTransition(decision, to, ctx).", + "triage_algorithme": "MOTEUR : engine/triage.ts — fonction PURE triage(input, ctx, settings) → Verdict. Tous les seuils viennent des clauses du Pacte (resolveSettings, replis spécifiés → consent) — le triage est gouverné par ceux qu'il gouverne. « Triage » et « verdict » ne sortent JAMAIS du code : l'UI dit « Le chemin ». La carte affiche l'explanation SEULE (code R* au dépliable « pourquoi ? », au PV et au journal de dérogation). Sujet grammatical : toujours le collectif ou la légitimité, jamais la formule.\n\nENTRÉES EXPLICITES (3 chips max) : 1. Concernés → scope (pré-calculé tags×domains, corrigeable, plancher non restrictible). 2. Réversible ? → easy/costly/irreversible. 3. Ça engage quoi ? → light/binding/structural — si binding/structural hors solo, la ligne « Ce que ça engage » (resources.note + montant/unité) se DÉPLIE en suggestion, jamais bloquante (l'exigence vit à la garde d'ouverture de state.ts). ENTRÉES INFÉRÉES : mandats couvrants, clauses qui matchent (index Q0 = Cmd+K), récurrence, consignations similaires, « C'est urgent », clause visée, nombre/%/montant dans la phrase ⇒ parametricHint.\n\nRÈGLES, DANS L'ORDRE — première qui matche (libellés français FINAUX) :\n- R-U URGENCE (overlay) : urgent && !irreversible ⇒ voie la plus légère MAINTENANT (badge « Décidé en urgence — le collectif ratifie », fenêtre ×2) + ratification chaînée auto. urgent && irreversible ⇒ REFUS : « Irréversible : l'urgence ne peut pas contourner le collectif. »\n- R0a DÉJÀ DÉCIDÉ : clause en vigueur couvre ⇒ « C'est déjà décidé (E9, Acte Forgeron) — agis. » avec lien, OU « Conteste la règle » ⇒ R3 avec l'inertie de la clause.\n- R0b MON MANDAT : ⇒ 'mandate' : « Ton mandat Modération couvre — décide, c'est tracé. » Trace obligatoire aux feux de la rampe + fenêtre d'objection 48 h.\n- R0c MANDAT D'UN AUTRE : ⇒ 'transmit' : « Le mandat de Cgeek couvre — transmets-lui. » Alternative : « Conteste ce mandat ».\n- R2 MOI SEUL : selfOnly ⇒ 'solo' : « Personne d'autre n'est concerné — décide. » Zéro fenêtre ; si irréversible, revoyure pré-cochée retirable (sur soi l'outil ne s'interpose pas).\n- R3 CLAUSE VISÉE : amendsClauseId ⇒ 'collective', protocole = clauseByInertia[clause.inertia] (repli → consent si absent) : « Tu proposes une version de E9 — son inertie s'applique : vote de ceux qu'elle gouverne. » L'héritage Ğ1 (binaire inertiel des clauses) survit ICI, intact.\n- R4 PETIT CERCLE RÉVERSIBLE : easy && |concernés| ≤ smallGroupMax(5) ⇒ 'advice' : « Réversible et 3 personnes concernées — demande leur avis puis décide. » 72 h ; objection formelle ⇒ escalade R5/consent.\n- R5 COLLECTIF (défaut de l'escalade) : sinon ⇒ 'collective', modalité par taille : ≤ consentMax(7) → consentement (« Vous êtes 6 — un tour d'accord suffit : sans objection, c'est adopté. ») ; ≤ collectiveMin(50) → nuancé (« Vous êtes 23 — vote nuancé : chacun se prononce en nuances, pas en camps. ») ; > collectiveMin → protocole protocolByRange.large résolu par le Pacte (repli → consent) — gabarits standard : « Vous êtes plus de 50 — vote nuancé, la modalité que votre Pacte a choisie. » ; free-currency/Ğ1 : binaire inertiel hérité (« … la formule d'inertie que votre communauté a adoptée »). structural ⇒ + framing + matière selon requireEffects + revoyure + gravure suggérée + inertie haute ; irreversible ⇒ revoyure obligatoire. Si parametricHint : la bascule « régler plutôt que trancher » proposée sous le chemin.\n- R6 RÉCURRENCE & MATURATION (overlay, jamais bloquant) : similarRecentCount ≥ 3 ⇒ « 3e fois — réclame un mandat » (createsMandate pré-rempli) ou « crée une règle » ; similarRecordedCount ≥ 3 (≥2 tags partagés) ⇒ « Cette pratique est mûre — protocolise-la en quelques phrases » (pré-remplit une clause de Pacte depuis les traces réelles).\n\nALTERNATIVES PERMANENTES (« Je choisis autrement ») : « C'est un réglage (montant, taux, répartition) — décidez au curseur » (parametric, mise en avant si parametricHint) ; « Déjà tranché — je le consigne » (record) ; le binaire pour/contre avec son coût affiché (« deux camps, un perdant ») ; chaque alternative avec son coût.\n\nDÉROGATION ASYMÉTRIQUE : ALOURDIR libre — 1 tap journalisé. ALLÉGER = overrideNote obligatoire + fenêtre d'objection 48 h imposée, affiché en évidence. MÊME ASYMÉTRIE AU PÉRIMÈTRE : affluence atteinte ⇒ élargir (1 tap) ou scopeKeptNote motivée publique — jamais d'évaporation silencieuse. Le chemin n'est jamais un juge : il recommande, explique, et laisse faire.\n\nMÉTRIQUE PRODUIT : chemin nominal solo = phrase + Entrée + bouton = 2 gestes, < 10 secondes ; le tunnel n'affiche JAMAIS un champ obligatoire qui ne change pas le chemin (les gardes de matière et de ressources vivent à l'ouverture de fenêtre/session, pas au tunnel) ; brief et curseurs vivent APRÈS le tunnel.", + "modalites": "CINQ MODALITÉS, UNE BOÎTE À OUTILS HIÉRARCHISÉE PAR LA DOCTRINE (chaque protocole est un Protocol nommé, référencé par une clause du Pacte — le modifier = amender le Pacte ; Protocol.ballot open/secret décidé par le Pacte, défaut open « feux de la rampe », secret v2 = « secret d'affichage — démo » ; dans TOUTES les modalités, l'historique de re-vote supersedesVoteId n'est visible que de son auteur — seul le dernier vote actif apparaît sous un nom) :\n\n1. CONSENTEMENT (« Ça me va / J'objecte ») — proposé par R5 pour ≤ consentMax(7), par les escalades d'objection, et REPLI universel de protocolByRange (tout collectif en a un — invariant). Zéro vote : le « Ça me va » est stocké (Assent), tour d'objections argumentées, zéro objection maintenue = adopté. Hors du réversible, ≥1 Assent est requis à l'échéance (sinon prolongation) — l'accord est un geste, pas une absence.\n\n2. NUANCÉ 6 NIVEAUX — proposé par R5 pour 8..collectiveMin(50) ET défaut des grands corpus (protocolByRange.large des gabarits standard). Libellés : 0 « Pas du tout » · 1 « Plutôt non » · 2 « Réservé » · 3 « Plutôt oui » · 4 « Oui » · 5 « Tout à fait » — jamais « CONTRE ». Commentaire OBLIGATOIRE sur 0-1. Histogramme des 6 niveaux visible en cours et à la clôture + commentaires en liste — jamais un simple adopté/rejeté. Valeurs 0-5 et résultats Ğ1 intacts. Clôture automatique à closesAt.\n\n3. PARAMÉTRIQUE — « Réglage collectif » / décider au curseur. JAMAIS routé d'office : proposé dans « Je choisis autrement », mis en avant si parametricHint. paramSpec : ≤7 curseurs à libellés métier ('slider' borné ou 'share' répartition), contrainte 'sum100' avec EXACTEMENT une part 'derived' (variable d'absorption, résolue 100 − Σ autres) — étiquettes « contrainte / calculé ». Salle : curseurs + statu quo + bloc calculé non éditable + faisceau translucide + médiane trait épais + carte « Pour moi » (linear-share spécifié : quote-parts de resources.amount au prorata de l'attribut déclaré — sans attribut, jamais de chiffre inventé) + « Explorer » (sandbox « exploration — ne compte pas »). Vote values[] révisable jusqu'à la clôture. Agrégation NOMMÉE : « Le collectif retient la médiane de chaque curseur — robuste aux extrêmes, et c'est une position que chacun aurait pu proposer. » (médiane BASSE : l'invariant est vrai par construction, step respecté). À closesAt : session 'frozen' — puis CRISTALLISATION = GESTE HUMAIN DATÉ du garant (« Cristalliser », crystallizedById/At ; quorum parametricMinParticipants sinon rejected ; relance au Fil ; bimodalité affichée en évidence au moment du geste : « cristalliser quand même / scinder / reformuler » — jamais d'automatisme). Pour les DOSSIERS complexes : le découpage en micro-décisions (chainKind 'element', « décision incrémentielle ») combine plusieurs modalités élément par élément, avec pondération (Concern.priority) et cartographie de clôture publiée par le geste de clôture du garant.\n\n4. ÉLECTION — reléguée aux seules problématiques d'échelle et de mandats : la nomination EST une décision createsMandate (6 méthodes : auto-désignation ratifiée, élection sans candidat, nuancé, consentement, tirage au sort, rotation). Jamais proposée pour une question de fond. CODÉE : Vote.choicePersonId désigne une personne du corpus ; vote déposé sans désignation = BLANC (participation, pas désignation) ; clôture automatique à closesAt : quorum electionMinParticipants sinon rejected, désignation à la PLURALITÉ simple des derniers votes actifs ; ÉGALITÉ ⇒ outcome 'tie' — l'outil ne départage JAMAIS : session de départage chaînée entre ex æquo (pré-remplie), ou tirage au sort entre ex æquo UNIQUEMENT si le protocole du Pacte l'a prévu d'avance (tieBreak:'draw'). Règle affichée en une phrase dans la salle.\n\n5. BINAIRE INERTIEL (pour/contre + Seuil = C + B^W + (M + (1−M)(1−(T/W)^G))·max(0,T−C)) — HÉRITAGE REMIS À SA PLACE : proposé par R3 pour l'amendement des clauses Ğ1 (clauseByInertia — les 59 Forgeron votées ainsi, D30M50B.1G.2 intact) et par R5 >50 UNIQUEMENT dans free-currency. Partout ailleurs : disponible dans « Je choisis autrement » avec son coût (« deux camps, un perdant ») ; description seedée : « l'outil de dernier recours des très grands corps — remis à sa place dans la boîte à outils ». L'Atelier des formules reste son école (seul lieu des lettres W/T/M/B/G/C et de KaTeX).\n\nRÉSOLUTION : protocolByRange { consent (obligatoire), nuanced?, large?, parametric?, election?, clauseByInertia? } — Ids résolus par le Pacte, repli spécifié → consent, ajustables en alourdissant. L'adoption applique automatiquement (clause current, mandat créé) — zéro bouton d'acceptation manuel ; tallies/médianes toujours recalculés depuis Vote[].", + "perimetres_inclusion": "LE MOT INSTALLÉ : « Périmètre » = qui est concerné par CETTE décision (panneau de la fiche, frise du chemin) ; « Cercle » = groupe durable de personnes (entité Circle, listes nominatives auditables). Les 3 vecteurs doctrinaux : ÉCHELLE D'ENJEU portée par weight/urgent/reversibility ; THÉMATIQUE par tags/domains (suggestion, jamais un droit) ; TOPOLOGIQUE déclaratif via Circle.kind 'place'|'theme'|'team' (iconographie, groupement, ordre des suggestions — jamais un droit ; hint non bloquant à 12 membres pour les 'team'). Le renommage réserve la place des vecteurs calculés (géo/WoT, v3) sans casser un libellé.\n\nDEUX PORTES, UNE LISTE, CHAQUE ENTRÉE SOURCÉE (Concern horodaté) :\n- CONCERNÉS EN PREMIER LIEU (origin 'computed', pastille pleine) : à la validation du chemin, engine/impact.ts calcule l'union CALCULABLE (membres des cercles du scope ∪ personnes nommées ∪ titulaires des mandats intersectants). Chaque inclusion porte sa raison en clair — auditable en un tap, jamais « la machine t'a désigné ». PLANCHER ANTI-EXCLUSION : élargir libre, JAMAIS restreindre sous le calcul.\n- CONCERNÉS EN SECOND LIEU (origin 'declared', main levée) : bouton « Ça me concerne » sur toute décision visible, SANS justification — un droit, pas une demande ; motif optionnel public. Déclaré AVANT la liste arrêtée ⇒ vote ; APRÈS ⇒ voix consultative — règle affichée en compte à rebours.\n\nOBJECTION DE FRONTIÈRE (suspensive — la contestation des frontières prime sur le fond) : « J'ai été oublié / Untel manque » ⇒ windowSuspendedAt, compte à rebours SUSPENDU (pastille pause, « compte à rebours suspendu — frontière contestée »), priorité maximale au Fil de l'auteur, jusqu'à intégration ou réponse motivée.\n\nMONTÉE D'ÉCHELLE — DÉCIDÉE, JAMAIS ÉVAPORÉE : si |déclarés| ≥ concernEscalateRatio(0.5) × |calculés|, bannière « Le périmètre déborde — élargis d'un cran » ET garde dans state.ts : la fenêtre/session ne peut pas se clore sans traitement — élargissement au cercle parent (couches d'oignon, 1 tap, re-triage journalisé — chemin nominal), ou maintien avec scopeKeptNote motivée PUBLIQUE, affichée en évidence comme un allègement. L'asymétrie de dérogation appliquée au lieu doctrinal de la subsidiarité.\n\nGRADIENT D'ENGAGEMENT (affiché sur la fiche) : observateur (lecture) < concerné (avis, objection, « Ça me va ») < lié (vote — liste arrêtée) < garant/mesureur (stewardIds réalisent et cristallisent, measurerIds constatent).\n\nPONDÉRATION PERSONNELLE DES ENJEUX : sur un dossier découpé, chaque concerné pose Concern.priority 0-3 (« pondère tes enjeux ») depuis le Fil ou la fiche — nourrit la hiérarchie des enjeux de la cartographie de clôture, jamais le droit de vote.\n\nANTI-BRIGADING PAR CONSTRUCTION : déclarations publiques + liste arrêtée + entrée tardive consultative + escalade non-ignorable — et la formule inertielle absorbe l'afflux (gonfler W sans que T suive fait MONTER le seuil). ANTI-FLICAGE (ligne rouge) : hors mandat, trace facultative et visibility choisie ('private' par défaut en solo) ; sous mandat, trace obligatoire visible du cercle électeur — la redevabilité s'applique au pouvoir, jamais aux personnes ; Person.attributes jamais affichés à autrui ni agrégés ; l'historique des re-votes n'est jamais opposable à une personne (visible du seul auteur).\n\nFRACTALITÉ : Collective.lineage (« essaimé de … ») mémorise l'essaimage par bundle ; AUCUNE structure fédérale dans le modèle (choix doctrinal documenté) — pattern v3 : une grappe = un collectif ordinaire dont les membres sont des mandataires d'autres collectifs, la fédération = échange de bundles/PV, jamais une table.", + "instruction_evaluation": "LE PRINCIPE : « Il n'est pas consulté, il est instruit » — pas de vote collectif engageant sans matière, et la même matière sert l'évaluation : la boucle d'« acquisition collective des expériences » est fermée par construction, sans jamais devenir plus lourde que l'enjeu.\n\nLA MATIÈRE (Decision.brief) : contexte, sources {label,url}, symptômes vs causes (la qualification doctrinale du sujet), et effects — les EFFETS RECHERCHÉS { label une ligne, target cible mesurable optionnelle, measured constaté }. GARDE DÉFINITIVE (state.ts, ouverture de session collective UNIQUEMENT — framing→voting / draft→voting) : ≥1 effet recherché exigé si weight ∈ clause de Pacte 'triage.requireEffects' ('binding' = binding et structural, défaut des gabarits structurés ; 'structural' ; 'none' seedé dans blank ET informal — le collectif règle sa propre exigence) ; sous garde, structural ⇒ ≥1 effet avec cible mesurable. Le weight 'light' (le trio, « on part le 12 ») n'y est JAMAIS soumis ; le chemin solo/record ne voit JAMAIS ces champs — 2 gestes / 10 s préservés ; la matière s'écrit en framing, pas dans le tunnel.\n\nLE BLOC « S'INSTRUIRE » (condensé — réparation intégrée) : en tête de la salle de vote et de la fiche (framing/voting) — un RÉSUMÉ COMPACT toujours visible avant les boutons : titre, effets recherchés, « Ce que ça engage » ; en DÉPLIABLES internes : body, versions + diff (contenu d'abord, auteur replié), avis et objections déposés, provenance (cas Ğ1 : voteRecord). Le bloc entier se replie automatiquement aux visites suivantes de la même personne (préférence locale par décision) — voter sur un titre nu reste structurellement impossible en collectif, re-voter reste léger (« agir d'abord, comprendre ensuite »). La phrase de fenêtre : « 14 jours pour s'instruire et formuler des contre-propositions ».\n\nLE STATU QUO (double ancre SejeteralO) : Decision.baselineNote (« Aujourd'hui : … ») suggéré routes advice/collective — en-tête de fiche « Aujourd'hui → Proposé » ; ParamDef.baseline = marqueur « actuel » sur chaque curseur + colonne de la carte « Pour moi » ; les clauses gardent leur diff (qui EST leur baseline). Une décision se lit comme un écart au statu quo, mesurable.\n\nLA DÉCISION CONNEXE (Decision.resources) : « Ce que ça engage » — note + montant/unité ('heures'|'€'|'DU'|libre). SUGGÉRÉE-dépliée au tunnel (jamais bloquante), EXIGÉE à la garde d'ouverture de fenêtre/session si weight != 'light' hors solo (même mécanique que la matière). Affichée fiche + résumé S'instruire + PV, réexaminée à l'épreuve du réel. Huile/eau : DU et € jamais convertis ni additionnés — agrégats de l'Observatoire par unité, lignes séparées. La paire doctrinale : stewardIds RÉALISENT (et cristallisent), measurerIds MESURENT. resources.amount est aussi le multiplicande de la carte « Pour moi » (linear-share).\n\nL'ÉPREUVE DU RÉEL (Review) : à review.dueAt — « Le réel a-t-il suivi ? », tableau effet recherché / cible / constaté (Effect.measured saisi par les measurerIds) AU-DESSUS des 3 boutons « Ça tient / À revoir / À révoquer » ; écart à la baseline relu ; ressources confrontées. Jamais un jugement dans le vide : « évaluer les expériences avec la curiosité du chercheur », le droit de se tromper est une donnée.\n\nL'OBSERVATOIRE (/decisions/observatoire) : comment nous décidons, DANS LES FAITS. Stats d'autonomie (ratio seul/mandat/avis/collectif/consigné, temps médians, participation, dérogations alourdies/allégées/périmètres maintenus, ressources engagées par unité) ; épreuves du réel rendues et dues ; « Révoquées — ce qu'on en a appris » ; élagage suggéré (protocole jamais invoqué, mandat sans trace). LA CONSIGNATION (route 'record') : « Déjà tranché — je le consigne » + decidedHow — consigner AVANT de protocoliser ; ratio consigné/outillé = maturité ; MATURATION (famille R6, jamais bloquante) : ≥3 consignations partageant ≥2 tags ⇒ « Cette pratique est mûre — protocolise-la en quelques phrases » pré-remplissant une clause de Pacte depuis les traces réelles. Le flux consigner→observer→protocoliser est le chemin ordinaire du produit, symétrique de R6 : le contrat social s'écrit au fil de l'eau, depuis la pratique.", + "navigation": "SHELL : layouts/default.vue. Header : logo-stamp (icône puits, rotation -10°) + « libreDecision » ; WorkspaceSelector (collectifs locaux + finalité A1 en une ligne + « Créer un collectif ») ; recherche globale Cmd+K (index unique clauses/décisions/mandats = le MÊME index que Q0) ; MoodSwitcher ; bouton « Décider » (desktop). FAB « Décider » 56 px mobile, au-dessus du sceau. Sceau hexagramme #48 Tsing 井 fixe bas-droite (géométrie SVG canonique). layouts/bare.vue pour /creer.\n\nSIDEBAR — 4 ENTRÉES, pas une de plus :\n1. Aujourd'hui (/) — le Fil + la capture. À 3 personnes, c'est tout l'outil.\n2. Décisions (/decisions) — le registre + l'Observatoire.\n3. Textes (/textes) — les documents sous vote permanent, Pacte épinglé.\n4. Mandats (/mandats) — les pouvoirs confiés.\n\nDISSOLUTIONS CONSERVÉES : protocoles dans le Pacte + étape modalité du chemin ; Atelier des formules = /textes/formules (lié depuis CHAQUE seuil, seule maison des lettres W/T/M/B/G/C) ; Sanctuaire = badge 井 + fiche de preuve + filtre ; pas de page réglages (les réglages SONT le Pacte) ; préférences locales = /donnees (mood, export/import, identité, attributs personnels, mode atelier).\n\nROUTES : / — Aujourd'hui · /decider — Le chemin (pré-remplissable ?clause= ?parent=&chain= ?mandat=) · /decisions — registre · /decisions/observatoire — L'Observatoire · /decisions/[id] — fiche décision (URL permanente) · /decisions/[id]/vote — salle de vote · /textes — bibliothèque · /textes/formules — Atelier des formules · /textes/[slug] — document vivant · /mandats — liste · /mandats/nouveau — wizard · /mandats/[id] — fiche mandat · /creer — onboarding (bare) · /donnees — données locales.\n\nCOUCHES CODE : app/types/domain.ts (noyau) ; app/lexicon.ts (source unique des libellés FR + test anti-lexique) ; app/engine/ (threshold avec electionResult, parametric avec crystallize, triage, modeParams, settings, state avec ctx, impact — purs, vitest) ; app/data/ (persistence.ts IndexedDB + seeds/*.bundle.json + templates.ts 7 gabarits) ; app/stores/ (contrat commun prêt adaptateur FastAPI — invariants privacy inclus) ; app/composables/ (useMood, useSettings, useFeed, useSearch) ; app/components/ par domaine (chemin/, decisions/, votes/ dont Faisceau et Curseurs, texts/, mandates/, circles/, common/). Le Fil est un SÉLECTEUR PUR — zéro table notification (il dérive : fenêtres, votes, épreuves dues, rapports dus, frontières, affluences, cristallisations en attente, dossiers complets, suggestions R6/maturation). Budget 400 lignes max par page. CSS print A4 (PV + affiche de session).", + "ecrans": [ + { + "route": "/creer", + "nom": "Créer un collectif", + "role": "Onboarding en 3 écrans — le premier usage EST le tutoriel ; s'ouvre sur le rêve", + "contenu": "Ouverture : « Une démocratie qui n'a jamais existé — à votre échelle. » Écran 1 : nom, couleur, mood. Écran 2 : gabarits en 7 cartes qui TRADUISENT — « Page blanche — observatoire d'abord » EN TÊTE (Pacte : clause « nous consignons nos décisions ; le reste s'écrira au fil de l'eau » + A1 finalité, protocole Consentement SEUL installé, routes solo/consignation/avis/consentement) ; puis Informel / Association / Coopérative / Commune / Communauté monnaie libre réétiquetés « point de départ — tout est amendable par décision » ; et « Institution symétrique » (Déclaration de symétrie, mandats Ambassadeur/Négociateur suggérés, première décision « Déclarer notre symétrie »). Chaque gabarit installe Pacte pré-rempli (préambule Autonomie–Équilibre–Liaison proposé et modifiable, clause A1, clauses settingKey dont requireEffects) + protocoles (au minimum Consentement — invariant) + cercle racine typé. Langage concret (« qui doit être d'accord ? »). Écran 3 : moi + membres initiaux. Imports « Explorer Duniter Ğ1 » / « Explorer l'Atelier du Canal » par le MÊME chemin qu'un import utilisateur. Atterrissage sur / avec la première décision proposée : « Adopter notre Pacte » (collective, consentement). Signature finale « Osons, nous verrons ». Réussite < 10 minutes, testée avec des non-théoriciens." + }, + { + "route": "/", + "nom": "Aujourd'hui", + "role": "L'écran quotidien unique : capture + Fil personnel — la boîte de réception de la démocratie", + "contenu": "Barre de capture sticky « Qu'est-ce que tu décides ? » (Entrée → /decider). Header « À toi de décider » avec compteur nu (jamais de rouge — interdits : dette, retard, en souffrance). Fil trié par échéance, sections vides masquées : fenêtres d'objection (« Ça me va / J'objecte » inline — le Ça me va crée un Assent ; compte à rebours ; pastille pause si frontière contestée ; mention « il manque un accord explicite » si fenêtre prolongée) ; avis demandés (3 boutons inline) ; votes ouverts (jauge miniature, lien salle) ; À CRISTALLISER (garant d'un réglage collectif figé : « Les votes sont figés — cristallise la médiane », relance persistante) ; DOSSIER COMPLET (garant : « Le dossier est complet — clore et publier la cartographie de clôture ») ; égalité d'élection à départager (session chaînée proposée) ; épreuves du réel dues (« Ça tient / À revoir / À révoquer » inline) ; rapports de mandat dus ; objections de frontière à traiter (priorité haute) ; périmètre débordant (élargir / maintenir motivé) ; pondération d'enjeux demandée ; suggestions de la boucle apprenante (« 3e fois — réclame ce mandat », « Cette pratique est mûre — protocolise-la », « ce protocole n'a jamais servi — élague »). Ensuite l'activité du collectif (selon visibility). Tout est sélecteur Pinia pur. Mobile-first absolu." + }, + { + "route": "/decider", + "nom": "Le chemin (tunnel)", + "role": "Une phrase → un chemin exécutable en un tap — 10 s pour le routinier, 3 min pour le stratégique", + "contenu": "Phrase en haut (pré-remplie par capture ou query clause/parent/chain/mandat). Résultats Q0 « déjà décidé ? » inline dès la frappe. 3 chips : Concernés (avatars pré-calculés, plancher non restrictible), Réversible ?, Ça engage quoi ? — si binding/structural hors solo, la ligne « Ce que ça engage » se DÉPLIE en suggestion (note + montant/unité, jamais € et DU additionnés) mais ne bloque JAMAIS la validation (l'exigence vit à l'ouverture de fenêtre/session) ; « Aujourd'hui : … » suggéré sur avis/collectif. Toggle « C'est urgent » discret. Carte LE CHEMIN live : grande icône de route, l'explication en une phrase SEULE (aucun code R*), frise le périmètre / combien de temps / quel seuil, revoyure et gravure pré-cochées si exigées, UN bouton principal. « Je choisis autrement » toujours visible : alternatives avec coût, dont « C'est un réglage — décidez au curseur » (mise en avant si nombre/%/montant détecté), « Déjà tranché — je le consigne » (→ adopted immédiat + « comment ça s'est décidé »), binaire avec « deux camps, un perdant ». Alourdir 1 tap journalisé ; alléger = note + fenêtre imposée. « Pourquoi ? » dépliable (le code de règle y vit). Suggestions R6/maturation sous le chemin. Valider crée la Decision structurée dans le bon état." + }, + { + "route": "/decisions", + "nom": "Registre des décisions", + "role": "Toutes les décisions par état — SectionLayout conservé", + "contenu": "Pills d'état en français courant (préparation / avis / objection / formulation / vote / figé / en vigueur / clos / rejeté / révoqué) aux couleurs du cycle. Filtres : cercle (groupés par kind lieu/thème/équipe), tag, « me concerne », poids, gravées 井, consignées. Formes courtes des routes en pills (seul / sous mandat / transmis / sur avis / ensemble / consigné). Cartes : titre, icône de route, poids, échéance, jauge miniature, badge 井, badge « Décidé en urgence — le collectif ratifie ». Lien vers l'Observatoire. Zéro création ici — on crée par la capture." + }, + { + "route": "/decisions/[id]", + "nom": "Fiche décision", + "role": "URL permanente de l'objet pivot — progressive disclosure par état", + "contenu": "En-tête : titre, « Aujourd'hui → Proposé » si baselineNote, route en français, poids, badges (urgence, allègement avec note, périmètre maintenu avec scopeKeptNote en évidence, 井). Timeline verticale du cycle. Panneau PÉRIMÈTRE : concernés en premier lieu (pastille pleine, tap → raison) et en second lieu (main levée, note publique), « Ça me concerne », compte à rebours de la liste arrêtée, bannière d'affluence NON-IGNORABLE (élargir 1 tap / maintenir motivé), objection de frontière (pastille pause). Bloc S'INSTRUIRE (framing/voting) : résumé compact toujours visible (titre, effets, ce que ça engage) + dépliables (body, versions/diff auteur replié, avis/objections, provenance) — auto-replié aux visites suivantes. Selon l'état : bloc fenêtre (boutons inline, silence-vaut-accord affiché seulement si réversible, sinon « un accord explicite est requis » avec compteur d'Assents) ; bloc formulation (« N jours pour s'instruire… », « Découper en micro-décisions » sur structural) ; onglet ÉLÉMENTS si dossier (filtre « me concernent », états, échéances, pondération 0-3) + CARTOGRAPHIE DE CLÔTURE auto-générée + carte « Le dossier est complet — clore » quand tous terminaux (geste du garant) ; bloc session (lien salle, résultat, « Qui vote : N personnes — liste arrêtée le … », bandeau « Votes figés — en attente de cristallisation » si frozen) ; bloc vigueur (garants, mesureurs, ressources, sunset, épreuve du réel due : tableau recherché/constaté au-dessus des 3 boutons) ; chaînage cliquable. Poignée « Remettre en question » sur tout adopté (protocole et inertie D'ORIGINE affichés). Actions : Imprimer (PV A4), graver (« empreinte locale — démo »), micro-copy « à trancher en présence ? » sur structural. Budget 400 lignes." + }, + { + "route": "/decisions/[id]/vote", + "nom": "Salle de vote", + "role": "Le geste démocratique central — instruit, puis prononcé", + "contenu": "Bloc S'INSTRUIRE condensé en tête : résumé compact toujours visible avant les boutons (titre, effets recherchés, ce que ça engage) + dépliables (texte/diff, avis et objections, provenance) — replié automatiquement aux visites suivantes. « Qui vote : 11 personnes — liste arrêtée le 12/08 » (avatars). Selon la méthode : CONSENTEMENT (« Ça me va » stocké / « J'objecte » argumentée) ; NUANCÉ 6 niveaux (« Pas du tout » → « Tout à fait », commentaire obligatoire inline sur 0-1, HISTOGRAMME sous la jauge) ; BINAIRE hérité (jauge inertielle : arc animé, seuil qui descend quand la participation monte, phrase sujet-collectif : « 120 votants sur 7 224 : à si faible participation, il faut presque l'unanimité — 94 pour. Plus de monde vote, plus le seuil descend. », lien « comprendre ce seuil » → Atelier pré-réglé) ; PARAMÉTRIQUE « Réglage collectif » : un curseur par paramètre (libellé métier, marqueur statu quo creux, alternative clavier + champ numérique), bloc « calculé » non éditable (étiquettes contrainte/calculé), FAISCEAU strip-plot (points translucides anonymes, médiane basse trait épais), carte « POUR MOI » (ma position / médiane / statu quo — quote-parts de « ce que ça engage » au prorata de mon attribut déclaré, « estimation sur données déclarées », jamais de chiffre inventé), « EXPLORER » (sandbox « exploration — ne compte pas », partir de la médiane/du statu quo/de ma position, « En faire ma position »), phrase d'agrégation « Le collectif retient la médiane de chaque curseur — robuste aux extrêmes, et c'est une position que chacun aurait pu proposer », bannière bimodalité non bloquante ; à closesAt bandeau « Votes figés — le garant cristallise » puis bouton « CRISTALLISER » (garant seul, quorum vérifié, bimodalité rappelée : « cristalliser quand même / scinder / reformuler ») ; ÉLECTION (désignation par avatar, bouton « Vote blanc », règle en une phrase : « La personne la plus désignée est nommée ; en cas d'égalité, vous départagez — jamais l'outil » ; égalité ⇒ session de départage chaînée ou tirage au sort si le Pacte l'a prévu). Mode secret : décomptes agrégés et commentaires anonymisés seuls, « secret d'affichage — démo ». Mon vote remplaçable — « ton historique n'est visible que de toi » (chaîne supersedes jamais publique, dernier vote seul affiché sous un nom en ballot open). Bouton « Imprimer » : affiche de session A4. Clôture : distribution complète + arguments — jamais un simple adopté/rejeté ; adoption ⇒ application automatique + tampon 井 « C'est décidé — et révisable, comme tout ici »." + }, + { + "route": "/decisions/observatoire", + "nom": "L'Observatoire", + "role": "Comment nous décidons, dans les faits — le miroir et l'apprentissage", + "contenu": "Titre « L'Observatoire », sous-titre « comment nous décidons, dans les faits ». Stats d'autonomie : ratio seul/mandat/avis/collectif/CONSIGNÉ (consigné/outillé = maturité), temps médians par route, taux d'objection, participation, dérogations (alourdies/allégées/périmètres maintenus), ressources engagées PAR UNITÉ (heures / € / DU — lignes séparées, jamais additionnées). Épreuves du réel : dues, rendues (tenues/à revoir/révoquées). « Révoquées — ce qu'on en a appris » : chaque révoquée avec sa chaîne — curiosité du chercheur, pas de lugubre. Consignations : liste des « déjà tranché » avec decidedHow ; maturation : ≥3 consignations partageant ≥2 tags ⇒ carte « Cette pratique est mûre — protocolise-la en quelques phrases » (pré-remplit une clause de Pacte). Élagage : protocole jamais invoqué, mandat sans trace 6 mois → revue suggérée. Décisions jamais revues : relance puis archivage proposé. Filtres personne/cercle/tag/période. Graphiques sobres (skill dataviz)." + }, + { + "route": "/textes", + "nom": "Textes", + "role": "La bibliothèque des documents sous vote permanent", + "contenu": "Pacte épinglé en tête : badge « les règles du jeu » + sous-titre « Notre contrat social — sacralisé, jamais immuable ». Cartes documents : titre, rôle, clauses, sessions en cours (mini-jauges), provenance Ğ1 (Certification 33 clauses, Forgeron 59 v2.0.0-fr, Comité Technique). « Déclaration de symétrie » pour les collectifs symmetric. Lien Atelier des formules. Filtre gravées." + }, + { + "route": "/textes/[slug]", + "nom": "Document vivant", + "role": "Clauses versionnées, boucle vote⇒texte fermée", + "contenu": "Sections repliables + scroll-spy. Chaque clause : code, titre, pastille d'inertie CÂBLÉE au protocole d'amendement, mini-jauge sur session réelle, statut. Tap → détail : version courante, propositions avec diff (contenu d'abord, auteur replié), décision fondatrice et amendements chaînés. « Proposer une version » → /decider?clause= (R3, inertie de la clause). Vue projetée conservée. InertiaSlider pédagogique → Atelier pré-réglé. Provenance/GenesisBlock (sources, voteRecord 97/23/19, notes — typos signalées jamais corrigées). Spécifique Pacte : clauses settingKey en clair (« Petit groupe : jusqu'à 5 personnes », « Matière exigée : décisions engageantes et structurantes »), effet IMMÉDIAT à l'adoption (resolveSettings), clause d'inertie protégée par l'inertie maximale ; clause A1 — Notre finalité en tête ; préambule Autonomie–Équilibre–Liaison." + }, + { + "route": "/textes/formules", + "nom": "Atelier des formules", + "role": "La pédagogie du seuil — seule maison des lettres", + "contenu": "FormulaEditor : sliders M/B/G/C (+Smith/ComTech), KaTeX de la formule inertielle, table de participation, presets d'inertie comparés. Branché sur engine/threshold.ts UNIQUE. Accessible depuis chaque seuil affiché, pré-réglé (« pourquoi 94 ? »). Cas Forgeron 2026 en un clic (W=7224, T=120 ⇒ 94). Le binaire présenté comme outil d'exception (« l'outil de dernier recours des très grands corps »). Section « Réglage collectif » : la médiane basse par curseur expliquée (robuste aux extrêmes, une position que chacun aurait pu proposer), mini-démo interactive, et la règle de cristallisation (« l'agrégat éclaire, le geste cristallise »)." + }, + { + "route": "/mandats", + "nom": "Mandats", + "role": "La carte des pouvoirs confiés : qui peut décider quoi, jusqu'à quand", + "contenu": "Cartes par statut (proposé/actif/expiré/révoqué) : titulaire, domaine (cercles + tags), échéance, prochain rapport dû, COMPTEURS BRUTS (« 12 décisions tracées · 1 objection · 2/2 rapports ») — AUCUNE jauge, aucun agrégat. Bouton « Réclamer un mandat » → wizard (souvent pré-rempli par R6)." + }, + { + "route": "/mandats/[id]", + "nom": "Fiche mandat", + "role": "Le pouvoir confié, visible et reprenable", + "contenu": "Titulaire, origine (la décision créatrice — cliquable), domaine explicite, période, cercle électeur. Bloc « EXERCICE DU MANDAT » : faits comptés jamais synthétisés — n décisions tracées, objections ouvertes/intégrées/escaladées (liste cliquable), rapports rendus/dus. Bloc « FEUX DE LA RAMPE » (sous-titre « toutes les décisions prises sous ce mandat — contestables ») : flux des décisions sous mandat, chacune avec sa fenêtre d'objection. Rapports de redevabilité. « Demander la révocation » en un geste : /decider chaîné (revocation) au protocole de nomination D'ORIGINE, cercle électeur comme périmètre — jamais improvisée, jamais plus coûteuse que l'élection." + }, + { + "route": "/mandats/nouveau", + "nom": "Wizard mandat", + "role": "Le meilleur flux v1, conservé et branché sur le pivot", + "contenu": "3 étapes : (1) rôle et domaine (titre, cercles, tags, mission en une phrase) ; (2) modalité de nomination parmi 6 (auto-désignation ratifiée, élection sans candidat — avec sa règle de clôture affichée : pluralité, blanc, départage par vous, jamais par l'outil —, nuancé, consentement, tirage au sort, rotation) avec pros/cons pédagogiques ; (3) durée bornée + cadence de rapports + cercle électeur. Sortie : une Decision createsMandate routée collective — la nomination EST une décision." + }, + { + "route": "/donnees", + "nom": "Données locales", + "role": "Local-first assumé : tes données, ton fichier, ta machine", + "contenu": "Export bundle JSON (schemaVersion 2), import avec validation (protocole Consentement requis, collisions) + pose de lineage (« essaimé de … »), réinitialisation par collectif. Seeds (Duniter Ğ1, Atelier du Canal). Identité locale (displayName, isMe) + MES ATTRIBUTS (Person.attributes, clés libres : 'heures/mois'… — « servent uniquement ta carte Pour moi, jamais montrés aux autres »). MODE ATELIER (ex-« agir en tant que ») : saisie assistée en présence — chaque saisie porte « saisi par X pour Y — atelier du » ; l'étiquette démo reste pour les personnages seedés. Niveaux de preuve honnêtes (empreinte locale / IPFS / chaîne — jamais affichés tant que non réels). Note synchro : « un serveur pourra se brancher plus tard, tes données ne changent pas de forme »." + } + ], + "vocabulaire_ui": [ + { + "terme": "Le chemin", + "remplace": "Le Verdict", + "contexte": "Carte centrale de /decider et titre du tunnel — le type TS Verdict reste en code ; « verdict » et « triage » ne sortent jamais du code" + }, + { + "terme": "Concerné·e en premier lieu", + "remplace": "calculé (glyphe calculator)", + "contexte": "Popover/panneau Périmètre — pastille pleine ; « la machine t'a désigné » est banni" + }, + { + "terme": "Concerné·e en second lieu — s'est déclaré·e", + "remplace": "déclaré", + "contexte": "Popover/panneau — la main levée reste ; « Ça me concerne » inchangé" + }, + { + "terme": "Concernés", + "remplace": "Touchés", + "contexte": "Chip du tunnel /decider" + }, + { + "terme": "Périmètre", + "remplace": "Cercles (panneau) / « qui est convoqué »", + "contexte": "Panneau de la fiche + frise du chemin ; « Cercle » = groupe durable seulement ; « scope » reste code" + }, + { + "terme": "Mandats — les pouvoirs confiés", + "remplace": "les pouvoirs délégués", + "contexte": "Sidebar + rôle de /mandats/[id] ; tout dérivé de « déléguer » banni de l'UI (test anti-lexique)" + }, + { + "terme": "À toi de décider", + "remplace": "dette décisionnelle", + "contexte": "Header du Fil, compteur nu — interdits : dette, retard, en souffrance ; « dette décisionnelle » survit en métrique interne dev" + }, + { + "terme": "Exercice du mandat", + "remplace": "Jauge de confiance", + "contexte": "Fiche et cartes mandat — faits comptés, jamais synthétisés ; « confiance » banni des agrégats" + }, + { + "terme": "Feux de la rampe", + "remplace": "Observatoire (bloc fiche mandat)", + "contexte": "Flux public des décisions prises sous un mandat — contestables" + }, + { + "terme": "L'Observatoire", + "remplace": "Mémoire", + "contexte": "/decisions/observatoire — « comment nous décidons, dans les faits »" + }, + { + "terme": "Qui vote : 11 personnes — liste arrêtée le 12/08", + "remplace": "corpus visible : W, T / snapshot", + "contexte": "Salle et fiche ; W/T/M/B/G/C vivent uniquement à l'Atelier des formules" + }, + { + "terme": "Pas du tout · Plutôt non · Réservé · Plutôt oui · Oui · Tout à fait", + "remplace": "0=CONTRE … 5=TOUT À FAIT", + "contexte": "Échelle nuancée — valeurs 0-5 et commentaire obligatoire 0-1 inchangés, résultats Ğ1 intacts" + }, + { + "terme": "L'épreuve du réel / « Le réel a-t-il suivi ? »", + "remplace": "Verdict de réalité", + "contexte": "Bloc revoyure — fiche et Fil" + }, + { + "terme": "Ça tient / À revoir / À révoquer", + "remplace": "confirmée / réviser / révoquer", + "contexte": "3 boutons de l'épreuve du réel (Review.verdict inchangé en code)" + }, + { + "terme": "C'est urgent / « Décidé en urgence — le collectif ratifie »", + "remplace": "décision conservatoire", + "contexte": "Toggle du tunnel + badge ; « conservatoire » survit dans le PV et la doc" + }, + { + "terme": "Réglage collectif / décider au curseur", + "remplace": "—(nouveau : VoteMethod 'parametric')", + "contexte": "Protocole et mode de la salle — « raisonner curseur / table de mixage »" + }, + { + "terme": "Cristalliser", + "remplace": "—(nouveau : geste du garant)", + "contexte": "Clôture paramétrique : « Les votes sont figés — cristallise la médiane » ; « l'agrégat éclaire, le geste cristallise » — jamais automatique" + }, + { + "terme": "Votes figés — en attente de cristallisation", + "remplace": "—(nouveau : status 'frozen')", + "contexte": "Bandeau de la salle/fiche entre closesAt et le geste du garant" + }, + { + "terme": "Décision incrémentielle", + "remplace": "—(nouveau : chainKind 'element')", + "contexte": "Découpage d'un dossier en micro-décisions — le nom de l'auteur réservé à CE mode (Taïwan)" + }, + { + "terme": "Cartographie de clôture", + "remplace": "—(nouveau)", + "contexte": "Section auto-générée du dossier découpé ; publiée par le geste « clore » du garant, devient le PV du dossier" + }, + { + "terme": "contrainte / calculé", + "remplace": "—(nouveau)", + "contexte": "Étiquettes du paramétrique : cadre en lecture seule vs part dérivée — le votant sait ce qu'il contrôle" + }, + { + "terme": "Pour moi", + "remplace": "—(nouveau)", + "contexte": "Carte d'impact (ma position / médiane / statu quo) — « estimation sur données déclarées », jamais de chiffre inventé" + }, + { + "terme": "S'instruire", + "remplace": "—(nouveau, regroupe l'existant)", + "contexte": "Bloc de tête salle/fiche : résumé compact toujours visible + dépliables, auto-replié aux visites suivantes" + }, + { + "terme": "Ce que ça engage", + "remplace": "—(nouveau : resources)", + "contexte": "Ligne suggérée du tunnel + bloc fiche/PV — la décision connexe de ressources ; exigée à l'ouverture, jamais au tunnel" + }, + { + "terme": "Déjà tranché — je le consigne", + "remplace": "—(nouveau : route 'record')", + "contexte": "Alternative permanente du tunnel — l'observatoire en un geste" + }, + { + "terme": "Aujourd'hui : … / Aujourd'hui → Proposé", + "remplace": "—(nouveau : baselineNote)", + "contexte": "Statu quo en en-tête de fiche ; marqueur « actuel » sur chaque curseur" + }, + { + "terme": "Je décide · Je décide, sous mandat · Je transmets · J'écoute, puis je décide · Nous décidons", + "remplace": "Solo / Mandat / Transmettre / Avis / Collectif", + "contexte": "Libellés des routes (lexicon.ts) — le chemin est un geste, pas une catégorie ; formes courtes en pills de filtres" + }, + { + "terme": "Ton vote est remplaçable — ton historique n'est visible que de toi", + "remplace": "re-vote audité — l'historique reste (indéterminé)", + "contexte": "Salle de vote : la chaîne supersedes n'est jamais publique sous un nom ; seul le dernier vote actif apparaît en ballot open" + }, + { + "terme": "Vote blanc", + "remplace": "—(nouveau : élection)", + "contexte": "Élection : vote déposé sans désignation — compte pour la participation, pas pour la désignation" + }, + { + "terme": "En cas d'égalité, vous départagez — jamais l'outil", + "remplace": "—(nouveau : outcome 'tie')", + "contexte": "Règle d'élection affichée en salle ; départage = session chaînée entre ex æquo ou tirage au sort si le Pacte l'a prévu" + }, + { + "terme": "Il manque un accord explicite", + "remplace": "—(nouveau : Assent)", + "contexte": "Fenêtre d'objection non réversible prolongée : ≥1 « Ça me va » requis — le silence ne vaut accord que pour le réversible" + }, + { + "terme": "pour/contre — « l'outil de dernier recours des très grands corps »", + "remplace": "vote binaire (neutre)", + "contexte": "Description seedée du protocole binaire + Atelier ; libellé Ğ1 : « héritage Toile de Confiance »" + }, + { + "terme": "Adopter notre Pacte", + "remplace": "Adoptez votre Pacte", + "contexte": "Première décision proposée — actes collectifs à l'infinitif (règle de ton n°2)" + }, + { + "terme": "Révoquées — ce qu'on en a appris", + "remplace": "Cimetière des révoquées", + "contexte": "Section de l'Observatoire — droit à l'erreur, curiosité du chercheur" + }, + { + "terme": "mode atelier — « saisi par X pour Y — atelier du »", + "remplace": "agir en tant que (pur démo)", + "contexte": "/donnees — saisie assistée en présence, passerelle hors-écran" + }, + { + "terme": "exploration — ne compte pas", + "remplace": "—(nouveau)", + "contexte": "Étiquette du sandbox Explorer de la salle paramétrique" + }, + { + "terme": "essaimé de …", + "remplace": "—(nouveau : lineage)", + "contexte": "Métadonnée d'affichage d'un collectif né d'un bundle d'un autre" + }, + { + "terme": "compte à rebours suspendu — frontière contestée", + "remplace": "(conservé v1 — il est juste)", + "contexte": "Pastille pause de l'objection de frontière" + } + ], + "seeds": "EXTRACTION — script backend/scripts/export_seed_bundle.py (venv existant) : importe seed.py (33 items Certification, 59 Forgeron, GENESIS, VOTER_NAMES ×11, mandates ×3, protocoles) et émet frontend/app/data/seeds/duniter-g1.bundle.json au schéma Bundle v2 (avec assents:[]). TEST DE NON-PERTE (vitest, bloquant) : 33+59 clauses, presets d'inertie conservés, provenance.voteRecord == {for:97, against:23, invalid:19, wotSize:7224, thresholdRequired:97, modeParams:'D30M50B.1G.2S.1'}, 11 personnes dont 5 smiths, 3 mandats, protocoles complets. Diff exigé AVANT dépréciation de seed.py. Typos des textes votés : Provenance.notes, JAMAIS corrigées.\n\nMAPPING Ğ1 (v1 conservé, amendé) : Collective{template:'free-currency', isTransparent:true} ; 11 Person (5 smiths) ; Circles typés kind — « Toile de confiance » (racine), « Forgerons » (team), « Comité Technique » (team) ; TextDocs Certification (33 clauses, presets câblés), Forgeron (59 clauses v2.0.0-fr, voteRecord complet), Comité Technique ; pacte-duniter-g1 avec clauses settingKey (smallGroupMax=5, collectiveMin=50, consentMax=7, fenêtres 48/72, framingDays=14, concernEscalateRatio=0.5, recurrenceThreshold=3, reviewDelayDays=90, requireEffects='binding', clause A1 « Notre finalité » adaptée à la toile), clause d'inertie en preset 'max', description du Pacte « Notre contrat social — sacralisé, jamais immuable ». protocolByRange : consent → « Consentement » (D7, OBLIGATOIRE partout), large → « Vote WoT standard » (binaire inertiel D30M50B.1G.2, description « l'outil de dernier recours des très grands corps — héritage Toile de Confiance ») — L'HÉRITAGE À SA PLACE ; clauseByInertia mappé sur les 4 presets ; + « Réglage collectif » (parametric, D14, parametricMinParticipants), « Vote nuancé » (D30), « Vote forgeron Smith » (S.1), « Vote ComTech » (T.1), « Élection sans candidat » (D14, electionMinParticipants, tieBreak:'runoff') — tous ballot:'open'. Chaque clause courante reçoit sa DÉCISION FONDATRICE. 3 Mandates (ComTech Cgeek active + rapport dû ; Administrateur Forgerons proposed EN VOTE nuancé ; Modération Forum active). Décisions historiques : Runtime Upgrade 1100 (adopted, structural, gravée, revoyure posée + brief.effects rétro-documentés), Évolution licence v0.4.0 (framing avec versions).\n\nDEUXIÈME COLLECTIF — atelier-du-canal.bundle.json (test permanent de marque blanche) : association fictive 12 membres, Pacte à seuils DIFFÉRENTS (smallGroupMax=4, fenêtres courtes, requireEffects='binding'), protocolByRange.large → NUANCÉ (la démonstration anti-binaire), Règlement intérieur 8 clauses, cercles typés Bureau (team) / Ateliers (theme) / Tous (racine), mandat Trésorerie (rapport dû), Person.attributes seedés ('heures/mois'). Décisions à TOUS les états : draft, advice en fenêtre, objection SUSPENDUE par frontière, objection non-easy avec 1 Assent posé (démonstration « accord explicite »), framing avec 2 versions et diff, voting nuancé en cours (histogramme vivant), adopted avec épreuve du réel due (brief.effects + measurerIds + resources posés — boucle complète), revoked (« ce qu'on en a appris »), conservatoire + ratification chaînée, UNE CONSIGNATION 'record' (« Le marché du samedi est reconduit — décidé au café, comme d'habitude ») et UN DOSSIER DÉCOUPÉ (2 éléments dont 1 terminal, priorités posées, cartographie de clôture en construction) ; et LE PARAMÉTRIQUE D'EXEMPLE : « Répartir le budget d'ateliers 2026 » en voting — resources { note:'budget annuel ateliers', amount:1200, unit:'€' }, paramSpec sum100 avec 3 parts nommées + EXACTEMENT 1 part 'derived' « Réserve (calculé) », baselines posées, impactAttrKey 'heures/mois', 5-6 votes seedés → faisceau, médiane basse et « Pour moi » calculables et vivants à l'ouverture ; une SECONDE session paramétrique 'frozen' (closesAt passé) avec la carte « À cristalliser » au Fil du garant — la cristallisation-geste démontrée. Tout contenu Ğ1 en dur dans le code est structurellement impossible.\n\nGABARITS (/creer, app/data/templates.ts) — SEPT : blank « Page blanche — observatoire d'abord » (Pacte : clause consignation + A1, protocole Consentement SEUL — l'invariant protocolByRange.consent satisfait, toutes les autres clés en repli → consent, requireEffects='none') ; informal (requireEffects='none') / association / cooperative / commune réétiquetés « point de départ — tout est amendable par décision », protocolByRange.large → nuancé partout, requireEffects='binding' ; free-currency (mapping Ğ1, large → binaire inertiel, unité DU par défaut dans resources) ; symmetric « Institution symétrique » (Pacte transparence totale, TextDoc « Déclaration de symétrie », première décision « Déclarer notre symétrie », mandats suggérés Ambassadeur et Négociateur). Chaque gabarit : préambule Autonomie–Équilibre–Liaison proposé et modifiable + clause A1 (défaut : boussoles vitalité/émancipation) + cercles typés kind + protocole Consentement au minimum. Les seeds s'importent par le MÊME chemin que l'import utilisateur ; lineage posé si bundle d'origine étrangère ; l'import valide l'invariant consent.", + "design_directives": "ICONOGRAPHIE DES ROUTES (vocabulaire visuel fixe) : solo = i-lucide-zap ; sous mandat = i-lucide-key-round ; transmettre = i-lucide-send ; avis = i-lucide-message-circle ; collectif = i-lucide-users ; consignation = i-lucide-notebook-pen ; urgence = i-lucide-siren ; déjà décidé = i-lucide-book-open-check ; récurrence→mandat = i-lucide-repeat ; réglage collectif = i-lucide-sliders-horizontal ; cristallisation = i-lucide-stamp (le geste). Carte Le chemin : icon-container 14 px, nom de route en 700 À LA PREMIÈRE PERSONNE, l'explication en une phrase dessous (jamais de code R*), frise périmètre/durée/seuil, bouton principal pill 20 px plein accent, alternatives en pills fantômes. Teintes déclinées des custom properties du mood.\n\nPÉRIMÈTRE : stacks d'avatars (initiales, teinte déterministe de l'id), débord -8 px, « +N ». Premier lieu = pastille pleine en coin (glyphe calculator SUPPRIMÉ) ; second lieu = main levée + fond légèrement teinté. Tap → popover chaîne d'inclusion. Plancher verrouillé visuellement (pas de croix de retrait sur les premiers-lieu). Cercles emboîtés en chips imbriquées ; icône par kind (lieu/thème/équipe).\n\nCYCLE DE VIE : timeline verticale à pastilles pleines reliées (pas de bordure) — draft neutre, avis/objection ambre + compte à rebours, vote bleu + mini-jauge, FIGÉ bleu-gris avec icône tampon en attente (« votes figés — en attente de cristallisation »), en vigueur vert, épreuve du réel violette, révoqué terre cuite, clos gris, rejeté barré. SUSPENSION : pastille pause + fond strié + « compte à rebours suspendu — frontière contestée ». Fenêtre prolongée (Assent manquant) : ambre soutenu + « il manque un accord explicite ». Un seul mapping état→couleur.\n\nJAUGE INERTIELLE (signature, héritage) : arc 270°, seuil qui DESCEND quand la participation monte, chiffres clairs, phrase générée sujet-collectif. KaTeX réservé à l'Atelier.\n\nNUANCÉ : 6 segments en gradient sémantique construit depuis la palette du mood (skill dataviz, jamais rouge/vert crus) ; HISTOGRAMME des 6 niveaux sous la jauge (la distribution, pas seulement le résultat) ; commentaire obligatoire inline sous 0-1.\n\nCURSEURS (Réglage collectif) : poignées tactiles ≥ 2.25 rem AVEC alternative clavier + champ numérique (correction SejeteralO) ; libellé métier au-dessus, unité, valeur courante en 700 ; marqueur statu quo = losange creux sur le rail ; part « calculé » = rail estompé non saisissable + étiquette « calculé » ; étiquette « contrainte » sur le cadre ; clamp dynamique visible (le rail se raccourcit quand la réserve dérivée approche de ses bornes). FAISCEAU : strip-plot par curseur — chaque position = point translucide anonyme (opacity ~0.25), médiane = trait épais accent, baseline = marqueur creux ; « transparence des agrégats, discrétion des personnes ». Carte « Pour moi » : fond légèrement teinté accent, trois colonnes ma position / médiane / statu quo, mention « estimation sur données déclarées ». Sandbox Explorer : bandeau « exploration — ne compte pas », curseurs en teinte neutre. CRISTALLISATION : bouton-tampon réservé au garant, confirmation avec rappel de bimodalité le cas échéant — le geste est solennel mais léger (un tap + confirmation), jamais une cérémonie.\n\nMICRO-CÉLÉBRATION D'ADOPTION : le tampon 井 incliné -10° s'appose « Décidé » (translateY du système, 0.12s ease, aucune fanfare) + « C'est décidé — et révisable, comme tout ici ».\n\nGRAVURE ET PREUVE : badge 井 tampon incliné sur objets gravés ; fiche de preuve sha256 mono tronqué copiable + « empreinte locale — démo ». Sceau hexagramme #48 Tsing : géométrie SVG canonique (viewBox 112×105, 6 traits, trait 1 en bas = dernier rect, opacity ~0.28, drop-shadow CSS), fixe bas-droite, jamais recouvert ; FAB au-dessus avec décalage.\n\nPRINT A4 : feuille CSS print pour le PV (/decisions/[id]) et l'affiche de session (/decisions/[id]/vote) — titre, texte, qui vote, seuil, échéance, comment participer ; noir sur blanc, sans moods. La cartographie de clôture d'un dossier s'imprime comme PV.\n\nMOODS v2 (champ lexical du puits, 2 light + 2 dark) : Source (light bleu d'eau), Margelle (light sable/pierre), Nappe (dark bleu profond), Minuit (dark encre). Socle : borderless (background + box-shadow), arrondis 16/12/20/14, hover cards translateY(-3px) 0.12s, mobile-first clamp(), breakpoints 480/768/1024, touch ≥ 2.25 rem. useMood.ts conservé.", + "hors_scope_v2": [ + "Décision paramétrique COMPLÈTE : équation de réalité pluggable (viabilité auto-calculée type p0 non linéaire), éditeur de courbes 'curve' (Bézier, ParamKind réservé), impact sur données réelles importées du périmètre, rétro-manipulation résultat→contrainte — v2 livre curseurs bornés + répartition sum100 (un derived, clamp/renormalisation spécifiés) + médiane basse + faisceau + baseline + impact linear-share sur attribut déclaré ; le solveur et le registre importé en v3", + "Builder d'indicateurs de l'observatoire (indicateurs propres définis par le collectif) — construire l'éditeur avant d'avoir observé la pratique contredirait l'observatoire lui-même ; v2 = stats fixes + effets recherchés/constatés + consignations", + "Vecteur topologique calculé (bassin de vie, commune pivot, géo) et règles d'appartenance WoT — v2 : listes nominatives + Circle.kind déclaratif ; enum de règle extensible sans migration", + "Vote secret cryptographique et pseudonymat certifié @personne (deux identités découplées) — v2 : ballot 'secret' en secret d'affichage honnête + dépersonnalisation d'ordre d'affichage ; la crypto arrive avec la synchro et l'auth Duniter", + "Économie du don outillée : allocation en DU effective, budget de gratitude, caisses fléchées, coefficients relatifs, rétribution des mandats pilotée par le collectif — v2 se limite à l'unité 'DU' descriptive dans resources (gabarit free-currency) ; l'intégration exige Ğ1/GrateWizard", + "Grappes et fédération inter-collectifs opérationnelles — v2 pose lineage + le pattern documenté « une grappe = un collectif ordinaire dont les membres sont des mandataires d'autres collectifs ; la fédération = échange de bundles, jamais une table » ; jamais de parentCollectiveId", + "Engagements fermes à seuil critique (crowdaction : recueillir, ne déclencher qu'au quota) — nouveau type d'objet et de cycle de vie, backlog v3", + "Cartographie thématique vectorielle (liaisons, interdépendances, symptômes/causes visualisés) — v2 : tags plats + champ symptomsVsCauses du brief ; la carte manipulable est un produit en soi", + "Événements de brassage générés par l'outil et multi-postes simultanés pour ateliers physiques — v2 livre l'imprimable A4 + le mode atelier mono-poste (intermédiation humaine, doctrinalement acceptable)", + "Délégation liquide / procuration — si elle arrive en v3, la nommer « mandater son vote » (jamais « déléguer » — le test anti-lexique l'attrapera)", + "IA repositionnée sur la recherche des biais décisionnels (seul usage autorisé par la doctrine) — exige corpus réel + backend ; jamais de suggestion prescriptive", + "Synchronisation backend / adaptateur FastAPI — contrat des stores et Bundle prêts (invariants privacy des re-votes et ballot secret notés au contrat) ; brancher l'API ne change aucun écran", + "Auth Duniter challenge-réponse Ed25519 — identités locales v2, duniterAddress réservé", + "Notifications sortantes (email, digest) et WebSocket temps réel — tout est in-app dans le Fil ; silence-vaut-accord réservé au réversible en conséquence, et l'Assent couvre le reste", + "IPFS et ancrage on-chain — proofLevel 'local' uniquement, champs réservés jamais affichés", + "Jugement majoritaire, catalogue 18 modalités × 9 nominations, ProcessTemplate opérationnels, exports juridiques signés — bibliothèque documentaire ; le PV markdown/A4 reste « registre de légitimité, jamais substitut légal »", + "Passerelles hors-écran complètes (relais humains institués, restitutions événementielles) et horizon des institutions symétriques réelles — consignés dans la page-manifeste/README comme horizon nommé, pas comme dette" + ], + "plan_implementation": [ + "1. SOCLE, LEXIQUE ET MOTEURS — branche v2, shell vers layouts/, moods 4 ambiances, types/domain.ts complet (ci-dessus, avec Assent, frozen, choicePersonId), app/lexicon.ts (source unique + test vitest anti-lexique + 3 règles de ton) DÈS CE SPRINT, engine/ TS pur (threshold avec electionResult, nuanced, parametric avec crystallize, modeParams, settings avec replis, triage, state avec canTransition(decision,to,ctx), impact) avec vitest portant test_threshold/test_qualifier/test_nuanced/test_mode_params + specs parametric (médiane BASSE step-safe ; sum100 exactement-un-derived + clamp/renormalisation ; computeMyImpact linear-share ; detectBimodality ; dégénérescences 0-vote) + specs election (pluralité, blanc, quorum, tie) + specs state (Assent, affluence, matière, dossier) — le cas Forgeron W=7224 T=120 ⇒ 94 vert AVANT tout écran", + "2. PERSISTANCE ET SEEDS — data/persistence.ts (IndexedDB, export/import, validation consent obligatoire, lineage à l'import), export_seed_bundle.py, bundles duniter-g1 + atelier-du-canal (paramétrique d'exemple calculable avec resources.amount, session frozen à cristalliser, consignation, dossier découpé, attributs, Assent démontré), tests de non-perte bloquants (33/59 clauses, 97/23/19/7224, 11 personnes, 3 mandats, protocoles), templates.ts 7 gabarits (blank en tête avec Consentement seul, large→nuancé partout sauf free-currency, requireEffects par gabarit)", + "3. SHELL ET ONBOARDING — header (WorkspaceSelector + finalité A1, Cmd+K, MoodSwitcher, Décider), FAB, sceau 井 canonique, /creer 3 écrans (phrase du rêve, 7 gabarits, « Adopter notre Pacte », signature « Osons, nous verrons »), /donnees (export/import/reset, identité, attributs, mode atelier)", + "4. LE GESTE — /decider complet (Q0 inline, 3 chips, ligne « Ce que ça engage » suggérée jamais bloquante, baselineNote suggéré, carte Le chemin sans code de règle, C'est urgent, dérogation asymétrique, alternatives permanentes réglage/consignation/binaire-avec-coût, suggestions R6+maturation, création structurée) + / Aujourd'hui (capture sticky, Fil sélecteurs purs incluant cartes À cristalliser / Dossier complet / accord explicite manquant, « À toi de décider » compteur nu, boutons inline) — MESURER : 2 gestes < 10 s, et la consignation aussi", + "5. CYCLE DE VIE — /decisions (registre) + /decisions/[id] (timeline avec état figé, panneau Périmètre premier/second lieu auditable, Ça me concerne, objection de frontière suspensive, garde d'affluence non-ignorable + scopeKeptNote, bloc S'instruire condensé auto-repliable, fenêtres : silence-vaut-accord easy seulement + Assent + prolongation, gardes d'ouverture matière/ressources, chaînage, Remettre en question au protocole d'origine, PV + print A4, gravure honnête)", + "6. LE VOTE — /decisions/[id]/vote : consentement (Assent stocké), nuancé (libellés définitifs, histogramme, commentaire 0-1), binaire hérité (jauge inertielle, phrase sujet-collectif, lien Atelier), PARAMÉTRIQUE (curseurs + clavier, statu quo, contrainte/calculé, clamp dynamique, faisceau, médiane basse expliquée, Pour moi, Explorer, frozen + geste Cristalliser du garant + quorum + bimodalité), ÉLECTION (désignation, blanc, pluralité, tie → départage humain), ballot secret d'affichage, liste arrêtée, re-vote à historique privé, mode atelier (recordedById), affiche A4, adoption ⇒ application automatique + micro-célébration 井", + "7. LES TEXTES — /textes + document vivant (clauses, versions/diff auteur replié, vue projetée, inertie câblée, Provenance/GenesisBlock, Pacte effectif : resolveSettings lu par le triage avec replis, A1 finalité, préambule boussoles, sous-titre, clause requireEffects en clair) + /textes/formules (FormulaEditor sur moteur unique + section médiane/cristallisation du Réglage collectif + binaire repositionné en outil d'exception)", + "8. LES MANDATS — /mandats (compteurs bruts, AUCUNE jauge), fiche (Exercice du mandat, Feux de la rampe, rapports, révocation chaînée au protocole d'origine), wizard 3 étapes pré-rempli par R6 (règle d'élection affichée)", + "9. L'OBSERVATOIRE ET LA BOUCLE APPRENANTE — /decisions/observatoire (stats + consigné/outillé, ressources par unité huile/eau, épreuves du réel, Révoquées — ce qu'on en a appris, élagage), l'épreuve du réel complète (tableau recherché/constaté, measurerIds saisissent), suggestions du Fil (récurrence⇒mandat, maturation⇒protocolisation, sunset, élagage) — le système se simplifie lui-même", + "10. DÉCISION INCRÉMENTIELLE — découpage en micro-décisions (chainKind element), onglet Éléments, pondération des enjeux (Concern.priority), cartographie de clôture auto-générée, clôture du dossier par le geste du garant (framing→closed gardée) — livrée après le socle car elle compose des primitives déjà construites", + "11. FINITION LIVRABLE — QA mobile+desktop zéro régression (480/768/1024, touch 2.25 rem), parcours complets sur les 2 seeds ET un collectif Page blanche (« Adopter notre Pacte » < 10 min avec un non-théoricien ; consigner→observer→protocoliser déroulé ; cristallisation et départage d'élection déroulés), test anti-lexique vert, a11y clavier (curseurs inclus)/contraste, print A4 vérifié, README + CONTRIBUTING + page-manifeste publique (sans vocabulaire de livraison, close sur « La ville change, le puits demeure »), npm run build zéro erreur, screenshots" + ] + }, + "verdicts": [ + { + "verdict": "reserves", + "must_fix": [ + "Cristallisation paramétrique : en faire un geste humain daté — à closesAt les votes sont figés mais c'est le garant (stewardIds) qui cristallise la médiane en position adoptée (relance au Fil s'il tarde), et la cristallisation automatique est au minimum bloquée quand detectBimodality est positif. Le corpus tranche explicitement (tension « Agrégat mathématique vs jugement construit ») : « l'agrégat éclaire une délibération, la cristallisation reste un geste humain distinct et daté » ; chez SejeteralO la publication est un geste distinct de l'agrégation vivante. L'automatisme actuel (« Clôture à closesAt ou geste du garant ») est le seul endroit du blueprint où un calcul produit un contenu adopté sans geste humain — exactement la « décision algorithmique », repoussoir absolu de la doctrine.", + "Historique des re-votes : spécifier que la chaîne supersedesVoteId n'est JAMAIS affichée publiquement sous le nom d'un votant — visible du seul auteur (et du décompte agrégé recalculé), même en ballot 'open'. Le corpus tranche (tension « Mémoire vive vs droit à l'erreur ») : « archiver les mandats, pardonner les positions ; l'historique des votes révisés n'est jamais opposable à une personne » — or « Mon vote remplaçable (historique audité) » et « re-vote audité — l'historique reste » laissent la visibilité publique indéterminée, ce qui ouvre un contrôle des personnes entre elles (le glas doctrinal)." + ], + "remarques": [ + "Binaire réellement remis à sa place : protocolByRange.large → nuancé dans tous les gabarits standard, binaire inertiel confiné à free-currency et clauseByInertia (R3), coût affiché « deux camps, un perdant », description seedée « outil de dernier recours », l'Atelier comme seule école des lettres. L'héritage Ğ1 (33+59 clauses, D30M50B.1G.2) survit à sa place — conforme à la contrainte du commanditaire ET à « remis à sa place dans la boîte à outils ».", + "Personnes jamais notées : vérifié partout — jauge de confiance supprimée (l'oxymore confiance/contrôle correctement identifié), « Exercice du mandat » en faits comptés jamais synthétisés (le contrôle porte sur le pouvoir, pas la personne), invariant 3 du modèle, Person.attributes jamais affichés ni agrégés, tirage/rotation disponibles. Conforme au cloisonnement exigé par le corpus.", + "Instruction : le point le plus fort du blueprint — bloc « S'instruire » non repliable avant les boutons, garde d'ouverture ≥1 effet recherché (state.ts), symptômes/causes, un même champ Effect pour instruction ET épreuve du réel : « il n'est pas consulté, il est instruit » et le mandat fondateur « acquisition collective des expériences » sont structurels, la boucle est fermée par construction.", + "Observatoire avant protocole : gabarit blank en tête de /creer, route 'record' à 2 gestes, maturation ≥3 consignations ⇒ protocolisation pré-remplie, renommage /decisions/observatoire — le flux consigner→observer→protocoliser est le chemin ordinaire, pas un discours. L'inversion de noms v1 (Mémoire) est corrigée dans le sens doctrinal exact.", + "Médiane/faisceau/impact : réels, pas décoratifs — engine/parametric.ts spécifié avec tests (medianByElement dont le résultat est une position valide, resolveDerived sum100, dégénérescences 0-vote), agrégateur NOMMÉ et expliqué en langage courant (le défaut SejeteralO explicitement corrigé), faisceau strip-plot anonyme + médiane épaisse + baseline, « Pour moi » borné aux données déclarées (« jamais un chiffre inventé ») — honnêteté locale-first exemplaire.", + "À préciser (une phrase suffit) : le cadre paramétrique lui-même (paramSpec — bornes, contrainte, baseline, impactAttrKey) se pose et se conteste en framing comme toute formulation. Tension corpus « Ancrage vs influence » : « le cadre lui-même (qui publie la référence, qui fixe les contraintes) doit être décidable par la couche méta » — structurellement couvert par le framing, mais jamais écrit.", + "Incohérence d'implémentation à anticiper : le test anti-lexique bannit « CONTRE » alors que la salle binaire héritée affiche « pour/contre » — prévoir l'exemption contextuelle du protocole binaire dans le test, sinon il sera contourné dès le sprint 6.", + "Le « degré d'inefficacité collectivement accepté » (mécanisme nommé du corpus) n'apparaît pas comme tel : il est de fait réglé par les clauses de fenêtres/framingDays du Pacte — une clause commentée portant ce nom serait une fidélité lexicale à coût nul.", + "Grand corpus structural sans nombre détecté : le chemin par défaut reste le nuancé simple là où la doctrine appellerait le découpage incrémentiel (« à grand volume, cela devient la décision incrémentielle ») — défendable car l'outil ne prescrit jamais et le bouton « Découper en micro-décisions » existe en framing, mais à surveiller à l'usage : envisager une suggestion non bloquante de découpage quand structural ET corpus > collectiveMin.", + "« Feux de la rampe » est restreint au flux des décisions sous mandat : c'est l'usage le plus exact (actes des mandatés) mais plus étroit que le concept doctrinal de transparence radicale générale — acceptable, le reste de la transparence existant sous d'autres noms (registre, Observatoire, isTransparent)." + ] + }, + { + "verdict": "reserves", + "must_fix": [ + "Restreindre la garde d'ouverture « ≥1 effet recherché » aux sessions collectives de poids binding/structural, ou la gouverner par une clause de Pacte (comme requireEffectsForStructural) désactivée dans les gabarits blank/informal : telle qu'écrite (« exigé à l'OUVERTURE de TOUTE session collective », state.ts draft→voting), elle impose un champ de matière rituel au consentement léger du trio (« on part le 12 » → « effet recherché : avoir une date »), en contradiction frontale avec le manifeste « jamais plus lourd que l'enjeu » et avec la carte blank « consentement simple ».", + "Préciser que la ligne « Ce que ça engage » (resources) est suggérée-dépliée dans le tunnel mais exigée SEULEMENT à la garde d'ouverture de fenêtre/session dans state.ts (même mécanique que brief.effects, dont le blueprint dit explicitement « la matière s'écrit en framing, pas dans le tunnel ») : le modèle dit « exigé si weight != 'light' hors solo » sans localiser la garde, et un champ texte obligatoire au tunnel qui ne change pas le chemin viole la métrique produit énoncée deux paragraphes plus loin (« le tunnel n'affiche JAMAIS un champ qui ne change pas le chemin ») — c'est le cas nominal de la trésorière (binding hors solo) qui prend la lourdeur.", + "Condenser le bloc « S'instruire » de la salle de vote : résumé compact toujours visible avant les boutons (titre, effets, ce que ça engage) avec versions/diff, avis/objections et provenance en dépliables internes, et replié automatiquement aux visites suivantes de la même personne — « voter sur un titre nu impossible » est préservé, mais « NON repliable avant les boutons » tel quel impose à la citoyenne de 40 000 un mur de scroll mobile à CHAQUE re-vote d'un vote pourtant révisable, à rebours de la leçon SejeteralO listée au corpus (« agir d'abord, comprendre ensuite »)." + ], + "remarques": [ + "Le chemin nominal tient réellement : solo = phrase + Entrée + bouton (2 gestes), consignation 'record' = 2 gestes aussi, compteur nu, alternatives à coût affiché, sections vides masquées — la capture d'une décision banale est effectivement plus simple que de ne pas utiliser l'outil (et Q0/Cmd+K rend la trace retrouvable). Les trois must_fix sont les trois seuls endroits où ce contrat fuit.", + "baselineNote « suggéré dans le tunnel » pour advice/collective viole doucement la même règle (champ qui ne change pas le chemin) : le déplacer en framing/fiche ou l'aligner sur le traitement retenu pour resources.", + "Trou de spec vivabilité : à échéance d'une fenêtre d'objection sur reversibility 'costly' sans AUCUNE réponse (silence ≠ accord), le blueprint ne dit pas ce qui advient — la décision ne doit pas pourrir sans issue ; spécifier relance, prolongation ou escalade automatique (la v1 disait « au moins un Ça me va par cercle touché », le v2 a perdu la suite).", + "Gabarit blank : « aucun protocole installé » mais routes annoncées incluant « consentement simple » — seeder un protocole consentement minimal ou reformuler la carte, sinon la première montée en collectif casse.", + "Quorum paramétrique non atteint ⇒ 'rejected' sec : dans un collectif de 12, offrir « prolonger la fenêtre » comme alternative au rejet automatique (décidable via le protocole du Pacte).", + "Onboarding mono-poste : « Adopter notre Pacte » (collective/consentement) exige de fait le mode atelier dès la première session — le prévoir explicitement dans le flux /creer (consentement guidé en mode atelier, ou consignation du Pacte fondateur) pour tenir la promesse < 10 min avec un non-théoricien.", + "Garde d'affluence à très petit effectif : 2 concernés calculés + 1 main levée (ratio 0.5) bloque la clôture — vivable car élargir = 1 tap, mais s'assurer que le traitement est offert au garant/steward et pas au seul auteur (absence = blocage).", + "Ampleur du delta (24 changements, 11 chantiers) pour « une seule refonte livrable » : la vivabilité du projet repose sur l'isolation déjà prévue des modules paramétrique et incrémentiel (étapes 6 et 10 après le socle) — la maintenir strictement, et accepter de dégrader Explorer/bimodalité/Pour-moi en v2.1 plutôt que d'entamer le socle des 2 gestes.", + "Le mode 'secret d'affichage — démo' est honnêtement étiqueté et à coût quasi nul : conforme Ockham, rien à corriger, mais veiller à ce qu'il n'apparaisse jamais comme choix par défaut dans les gabarits (défaut 'open' bien posé)." + ] + }, + { + "verdict": "reserves", + "must_fix": [ + "Élection non codable : Vote n'a aucun champ pour désigner une personne (value 'for'|'against'|0-5, values number[]) alors que VoteMethod 'election', la salle « ÉLECTION (désignation) » et le protocole seedé « Élection sans candidat » sont en scope v2 — ajouter Vote.choicePersonId?: Id (exactement un de value/values/choicePersonId selon la méthode) et spécifier la règle de clôture (pluralité, égalités, abstention).", + "Contrainte sum100 dégénérée non spécifiée : définir resolveDerived quand 100 − Σ autres sort des bornes du paramètre 'derived' (au vote : validateVote rejette ou la géométrie des curseurs l'empêche ; à la cristallisation : la médiane des parts ne conserve pas la somme → définir clamp + renormalisation ou médiane sous contrainte), et imposer EXACTEMENT une part 'derived' quand constraint='sum100' (avec zéro derived, l'agrégat viole la contrainte sans variable d'absorption).", + "Convention de médiane non fixée : avec un nombre pair de votes, la moyenne des deux valeurs centrales peut violer ParamDef.step, ce qui casse l'invariant affiché « la médiane est une position que chacun aurait pu proposer » — spécifier dans medianByElement la médiane basse (ou le snap au step) et l'écrire dans les tests vitest.", + "computeMyImpact 'linear-share' sans formule : définir précisément le calcul (quels params concernés, rôle de Resources.amount comme multiplicande, rôle de Person.attributes[impactAttrKey], résultat par curseur ou global) — l'exemple seedé « budget d'ateliers × 'heures/mois' » n'est pas calculable en l'état.", + "canTransition(decision, to) n'a pas ses entrées : la garde d'affluence exige les Concern[] (|déclarés| vs |calculés|) et la garde matière exige CollectiveSettings (requireEffectsForStructural), absents de la signature — l'étendre en canTransition(decision, to, ctx: { concerns, settings, session? }).", + "Contradiction silence-vaut-accord : pour la fenêtre d'objection non-'easy', « silence vaut accord UNIQUEMENT si easy » contredit « à échéance sans objection ouverte → adopted » deux phrases plus loin — définir le comportement à échéance sans accord explicite (ex. exiger ≥1 « Ça me va » et nommer l'entité qui le stocke : Vote 'for' ? Advice ? nouveau champ ?).", + "Clôture du dossier découpé impossible : le parent d'une chaîne 'element' reste en framing dont la seule sortie est framing→voting — définir sa transition terminale (ex. framing→closed quand tous les éléments sont dans un état terminal, la cartographie de clôture devenant le PV du dossier).", + "Gabarit 'blank' incompatible avec CollectiveSettings : « aucun protocole installé » mais protocolByRange type des Id non-optionnels (consent/nuanced/large/parametric/election) et la route consentement est annoncée — seeder au minimum le protocole Consentement dans blank et spécifier le fallback de triage/resolveSettings quand une clé de protocolByRange est non résolue (proposer consent ou bloquer avec suggestion de créer le protocole, jamais un crash)." + ], + "remarques": [ + "Vérifié positivement : cas Forgeron W=7224 T=120 ⇒ 94 exact par la formule ; seeds Ğ1 sans perte (33+59 clauses, voteRecord 97/23/19/7224 en Provenance verbatim, clauseByInertia intact via R3, binaire inertiel conservé pour free-currency, 11 personnes/5 smiths/3 mandats) ; le renommage inertialBinary→large est purement référentiel, zéro migration de données.", + "Modalité consentement hors invariant : « tallies toujours recalculés depuis Vote[] » ne couvre pas le consentement dont la clôture dépend des Objection — préciser le stockage du « Ça me va » (Vote{value:'for'} recommandé) pour que le moteur unique de clôture ait des entrées homogènes.", + "detectBimodality : la constante de déclenchement (ratio plus grand écart / dispersion) n'est pas fixée — la poser dans la spec vitest, sinon la bannière est non testable.", + "Vote.values est positionnel sur paramSpec.params : geler paramSpec à l'entrée en 'voting' (ou clé les valeurs par ParamDef.key) pour éviter la corruption si le spec est édité en framing après premiers votes exploratoires.", + "Test anti-lexique « CONTRE » : la salle binaire héritée affiche « pour/contre » — scoper la règle (casse stricte CONTRE majuscule, ou exclure les libellés du protocole binaire) sinon le test est rouge par construction.", + "États non spécifiés : l'état cible de sunsetAt (closed ?) et le prédicat « acte one-shot → closed » (aucun champ ne distingue l'acte one-shot — choix manuel à l'épreuve du réel ?) restent à trancher.", + "Suspension de frontière : préciser si windowEndsAt est décalé de la durée suspendue à la levée de windowSuspendedAt (sinon une suspension longue consomme la fenêtre).", + "Nomination 'draw' et 'rotation' : le mécanisme d'exécution (source d'aléa, ordre de rotation, qui déclenche) n'est pas spécifié alors que le wizard les propose.", + "DSL modeParams : la grammaire affichée (D30M50B.1G.2S.1) n'a pas de lettre pour parametricMinParticipants ni nuancedThresholdPct — étendre le format ou documenter que ces params vivent hors DSL.", + "UX de la garde matière : pour une décision collective légère en consentement (pas de framing), l'endroit où saisir le ≥1 effet recherché exigé à l'ouverture n'est décrit nulle part — prévoir un prompt inline à la validation du chemin.", + "Budget 400 lignes : la salle de vote cumule 5 modalités + secret d'affichage + print A4 + Explorer — imposer dès le plan un composant par modalité dans components/votes/ sinon le budget est structurellement intenable.", + "Test de non-perte seeds : la formulation mélange voteRecord.modeParams (champ frère de result) et les champs de result — écrire l'assertion sur la structure exacte de Provenance pour éviter un test qui passe sur une structure fausse." + ] + } + ] + }, + "workflowProgress": [ + { + "type": "workflow_phase", + "index": 1, + "title": "Confrontation" + }, + { + "type": "workflow_phase", + "index": 2, + "title": "Synthèse finale" + }, + { + "type": "workflow_phase", + "index": 3, + "title": "Vérification" + }, + { + "type": "workflow_agent", + "index": 1, + "label": "critique:fidelite", + "phaseIndex": 1, + "phaseTitle": "Confrontation", + "agentId": "a19d69446c7f5120d", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786415645101, + "lastProgressAt": 1786415645101, + "cached": true, + "resultPreview": "{\"constats\":[{\"probleme\":\"R5 fait du « vote binaire à formule inertielle » la modalité PAR DÉFAUT de tout corpus > 50 (triage_algorithme R5, CollectiveSettings.protocolByRange.inertialBinary), alors que la doctrine proscrit le clivage binaire comme format par défaut (« Le clivage binaire est un fléau contre la réflexion, un ennemi fatal : il la tue » ; « proscrire le binaire oui/non… le numérique …", + "promptPreview": "CONTEXTE : libreDecision v2, refonte de zéro. Intention de Yvv : que n'importe quel collectif, de 3 personnes à une population entière, aille le plus loin possible dans l'auto-décision — « la capacité d'une démocratie qui n'a jamais existé ». Contraintes : Nuxt 4 SPA local-first (IndexedDB, export/import), UI française, design borderless/moods, une seule refonte livrable, les contextes Ğ1 existant…" + }, + { + "type": "workflow_agent", + "index": 2, + "label": "critique:mecanismes", + "phaseIndex": 1, + "phaseTitle": "Confrontation", + "agentId": "a2e6b8c7b3f37e1bd", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786415645101, + "lastProgressAt": 1786415645101, + "cached": true, + "resultPreview": "{\"constats\":[{\"probleme\":\"Le modèle ne connaît que 4 VoteMethod (consent/binary/nuanced/election) : aucune décision à paramètres continus, alors que la doctrine en fait le format central — « raisonner curseur / table de mixage » (MNP p.68, p.159-161), mécanisme corpus « Décision paramétrique continue avec viabilité auto-calculée », leçon SejeteralO « voter = régler une forme, pas choisir un camp »…", + "promptPreview": "CONTEXTE : libreDecision v2, refonte de zéro. Intention de Yvv : que n'importe quel collectif, de 3 personnes à une population entière, aille le plus loin possible dans l'auto-décision — « la capacité d'une démocratie qui n'a jamais existé ». Contraintes : Nuxt 4 SPA local-first (IndexedDB, export/import), UI française, design borderless/moods, une seule refonte livrable, les contextes Ğ1 existant…" + }, + { + "type": "workflow_agent", + "index": 3, + "label": "critique:vocabulaire", + "phaseIndex": 1, + "phaseTitle": "Confrontation", + "agentId": "a18cd8ad29fa87cf1", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786415645101, + "lastProgressAt": 1786415645101, + "cached": true, + "resultPreview": "{\"constats\":[{\"probleme\":\"La navigation dit « Mandats (/mandats) — les pouvoirs délégués » et la fiche mandat « Le pouvoir délégué, visible et reprenable ». C'est la contradiction doctrinale la plus frontale possible : « On procède par délégation de pouvoir, mais on ne délègue pas, on abandonne » ; « Le peuple adulte ne délègue plus, il mandate » (Mon nom est personne, métaphore du peuple adulte ;…", + "promptPreview": "CONTEXTE : libreDecision v2, refonte de zéro. Intention de Yvv : que n'importe quel collectif, de 3 personnes à une population entière, aille le plus loin possible dans l'auto-décision — « la capacité d'une démocratie qui n'a jamais existé ». Contraintes : Nuxt 4 SPA local-first (IndexedDB, export/import), UI française, design borderless/moods, une seule refonte livrable, les contextes Ğ1 existant…" + }, + { + "type": "workflow_agent", + "index": 4, + "label": "critique:echelle", + "phaseIndex": 1, + "phaseTitle": "Confrontation", + "agentId": "af88662473ae950a8", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786415645101, + "lastProgressAt": 1786415645101, + "cached": true, + "resultPreview": "{\"constats\":[{\"probleme\":\"La décision connexe d'allocation de ressources est totalement absente du modèle. Doctrine : « chaque décision implique une allocation de ressources, donc une décision connexe incontournable » (MNP p.89, p.269 ; mécanisme « Décision → ressources liées » du corpus ; « pas de démocratie sans son économie »). La Decision v2 a des stewardIds mais aucune trace de ce qu'elle eng…", + "promptPreview": "CONTEXTE : libreDecision v2, refonte de zéro. Intention de Yvv : que n'importe quel collectif, de 3 personnes à une population entière, aille le plus loin possible dans l'auto-décision — « la capacité d'une démocratie qui n'a jamais existé ». Contraintes : Nuxt 4 SPA local-first (IndexedDB, export/import), UI française, design borderless/moods, une seule refonte livrable, les contextes Ğ1 existant…" + }, + { + "type": "workflow_agent", + "index": 5, + "label": "architecte:final", + "phaseIndex": 2, + "phaseTitle": "Synthèse finale", + "agentId": "a562685026aef906c", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786415645102, + "lastProgressAt": 1786415645102, + "cached": true, + "resultPreview": "{\"manifeste\":\"Personne n'a jamais vécu en démocratie — alors nous la rêvons de jour, et nous la commençons à notre échelle. libreDecision attrape chaque décision en une phrase et propose le chemin le plus court : décide seul et trace-le, décide sous mandat, écoute puis décide, ou décidez ensemble — jamais plus lourd que l'enjeu. Ici on ne délègue pas, on mandate, et on reprend ; on n'est pas consu…", + "promptPreview": "CONTEXTE : libreDecision v2, refonte de zéro. Intention de Yvv : que n'importe quel collectif, de 3 personnes à une population entière, aille le plus loin possible dans l'auto-décision — « la capacité d'une démocratie qui n'a jamais existé ». Contraintes : Nuxt 4 SPA local-first (IndexedDB, export/import), UI française, design borderless/moods, une seule refonte livrable, les contextes Ğ1 existant…" + }, + { + "type": "workflow_agent", + "index": 6, + "label": "verif:1", + "phaseIndex": 3, + "phaseTitle": "Vérification", + "agentId": "ae6356968d165be73", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786415645134, + "lastProgressAt": 1786415645134, + "cached": true, + "resultPreview": "{\"verdict\":\"reserves\",\"must_fix\":[\"Cristallisation paramétrique : en faire un geste humain daté — à closesAt les votes sont figés mais c'est le garant (stewardIds) qui cristallise la médiane en position adoptée (relance au Fil s'il tarde), et la cristallisation automatique est au minimum bloquée quand detectBimodality est positif. Le corpus tranche explicitement (tension « Agrégat mathématique vs …", + "promptPreview": "CONTEXTE : libreDecision v2, refonte de zéro. Intention de Yvv : que n'importe quel collectif, de 3 personnes à une population entière, aille le plus loin possible dans l'auto-décision — « la capacité d'une démocratie qui n'a jamais existé ». Contraintes : Nuxt 4 SPA local-first (IndexedDB, export/import), UI française, design borderless/moods, une seule refonte livrable, les contextes Ğ1 existant…" + }, + { + "type": "workflow_agent", + "index": 7, + "label": "verif:2", + "phaseIndex": 3, + "phaseTitle": "Vérification", + "agentId": "a6ade8d5af85bb487", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786415645135, + "lastProgressAt": 1786415645135, + "cached": true, + "resultPreview": "{\"verdict\":\"reserves\",\"must_fix\":[\"Restreindre la garde d'ouverture « ≥1 effet recherché » aux sessions collectives de poids binding/structural, ou la gouverner par une clause de Pacte (comme requireEffectsForStructural) désactivée dans les gabarits blank/informal : telle qu'écrite (« exigé à l'OUVERTURE de TOUTE session collective », state.ts draft→voting), elle impose un champ de matière rituel …", + "promptPreview": "CONTEXTE : libreDecision v2, refonte de zéro. Intention de Yvv : que n'importe quel collectif, de 3 personnes à une population entière, aille le plus loin possible dans l'auto-décision — « la capacité d'une démocratie qui n'a jamais existé ». Contraintes : Nuxt 4 SPA local-first (IndexedDB, export/import), UI française, design borderless/moods, une seule refonte livrable, les contextes Ğ1 existant…" + }, + { + "type": "workflow_agent", + "index": 8, + "label": "verif:3", + "phaseIndex": 3, + "phaseTitle": "Vérification", + "agentId": "a77c9bbc9a86d7c09", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786415645161, + "queuedAt": 1786415645136, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "reserves", + "promptPreview": "CONTEXTE : libreDecision v2, refonte de zéro. Intention de Yvv : que n'importe quel collectif, de 3 personnes à une population entière, aille le plus loin possible dans l'auto-décision — « la capacité d'une démocratie qui n'a jamais existé ». Contraintes : Nuxt 4 SPA local-first (IndexedDB, export/import), UI française, design borderless/moods, une seule refonte livrable, les contextes Ğ1 existant…", + "lastProgressAt": 1786416127824, + "tokens": 182790, + "toolCalls": 10, + "durationMs": 482662, + "resultPreview": "{\"verdict\":\"reserves\",\"must_fix\":[\"Élection non codable : Vote n'a aucun champ pour désigner une personne (value 'for'|'against'|0-5, values number[]) alors que VoteMethod 'election', la salle « ÉLECTION (désignation) » et le protocole seedé « Élection sans candidat » sont en scope v2 — ajouter Vote.choicePersonId?: Id (exactement un de value/values/choicePersonId selon la méthode) et spécifier la…" + }, + { + "type": "workflow_agent", + "index": 9, + "label": "architecte:final", + "phaseIndex": 2, + "phaseTitle": "Synthèse finale", + "agentId": "a064f0d5e30b0bae0", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786416127851, + "queuedAt": 1786416127827, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "Personne n'a jamais vécu en démocratie — alors nous la rêvo…", + "promptPreview": "CONTEXTE : libreDecision v2, refonte de zéro. Intention de Yvv : que n'importe quel collectif, de 3 personnes à une population entière, aille le plus loin possible dans l'auto-décision — « la capacité d'une démocratie qui n'a jamais existé ». Contraintes : Nuxt 4 SPA local-first (IndexedDB, export/import), UI française, design borderless/moods, une seule refonte livrable, les contextes Ğ1 existant…", + "lastProgressAt": 1786419283153, + "tokens": 212204, + "toolCalls": 9, + "durationMs": 3155302, + "resultPreview": "{\"manifeste\":\"Personne n'a jamais vécu en démocratie — alors nous la rêvons de jour, et nous la commençons à notre échelle. libreDecision attrape chaque décision en une phrase et propose le chemin le plus court : décide seul et trace-le, décide sous mandat, écoute puis décide, ou décidez ensemble — jamais plus lourd que l'enjeu. Ici on ne délègue pas, on mandate, et on reprend ; on n'est pas consu…" + } + ], + "totalTokens": 394994, + "totalToolCalls": 19 +} \ No newline at end of file diff --git a/docs/dev/blueprint-v2-panel.json b/docs/dev/blueprint-v2-panel.json new file mode 100644 index 0000000..967468f --- /dev/null +++ b/docs/dev/blueprint-v2-panel.json @@ -0,0 +1,511 @@ +{ + "summary": "Cartographier la maquette existante puis panel de visions produit pour la refonte v2", + "agentCount": 12, + "logs": [ + "Cartographie terminée — lancement du panel de visions", + "4/4 visions produites — jugement" + ], + "result": { + "blueprint": { + "nom_produit": "libreDecision", + "manifeste": "Tout le monde décide, tout le temps. libreDecision attrape chaque décision en une phrase et répond en dix secondes : décide seul et trace-le, décide sous mandat, demande leur avis, ou porte-la au collectif — avec le bon cercle, la bonne modalité, le bon seuil, jamais plus lourd que l'enjeu. Les personnes touchées sont incluses par calcul, chacune voit pourquoi, et n'importe qui peut lever la main « ça me concerne » sans se justifier. Rien n'est définitif : toute décision en vigueur garde sa poignée de révision, avec l'inertie de son poids — révoquer un choix léger est un geste, toucher aux règles du jeu exige la quasi-unanimité. Les règles elles-mêmes sont un texte sous vote permanent, le Pacte : le collectif règle son propre curseur entre informel et constitutionnel, en décidant. Du trio d'amis à la toile Ğ1 et ses 7 224 membres, le même geste, le même objet, la même formule. La ville change, le puits demeure : la démocratie d'exercice quotidien, livrable à toute structure.", + "modele_domaine": "// ─────────────────────────────────────────────────────────────\n// libreDecision v2 — noyau de domaine (frontend/app/types/domain.ts)\n// Local-first, prêt-synchro. Identifiants anglais, UI française.\n// INVARIANT : LA DÉCISION EST L'OBJET PIVOT — mandats, clauses,\n// révocations, ratifications en sont des spécialisations chaînées.\n// L'unification est un fait d'architecture, JAMAIS un vocabulaire d'UI.\n// ─────────────────────────────────────────────────────────────\n\ntype Id = string // UUIDv7 — triable par temps, prêt pour la synchro\ntype ISODate = string // ISO 8601 UTC\ntype Json = string | number | boolean | null | Json[] | { [k: string]: Json }\n\n// Toute entité : scoping collectif + horloge de synchro + soft-delete.\n// Règles prêt-synchro : jamais de suppression physique (archivedAt) ;\n// Vote/Advice/Objection append-only (merge trivial) ; le reste LWW par updatedAt.\ninterface Entity {\n id: Id\n collectiveId: Id\n createdAt: ISODate\n updatedAt: ISODate\n archivedAt?: ISODate\n}\n\n// ── Le tenant ────────────────────────────────────────────────\ntype CollectiveTemplate = 'informal' | 'association' | 'cooperative' | 'commune' | 'free-currency'\n\ninterface Collective {\n id: Id\n slug: string\n name: string\n color: string // hex accent\n icon: string // i-lucide-*\n template: CollectiveTemplate // gabarit d'origine — installe le Pacte pré-rempli\n isTransparent: boolean // décisions visibles par défaut de tous les membres\n pactDocId: Id // le TextDoc role='pact' — les réglages EN document voté\n rootCircleId: Id // cercle « tout le collectif »\n createdAt: ISODate\n updatedAt: ISODate\n}\n\n// ── Les gens ─────────────────────────────────────────────────\ninterface Person extends Entity {\n displayName: string\n isMe: boolean // profil local (v2 mono-poste ; mode démo = « agir en tant que »)\n duniterAddress?: string // SS58 — réservé auth Duniter (hors scope v2)\n wotStatus?: 'member' | 'smith' // critères multi-corps Ğ1\n}\n\ninterface Circle extends Entity {\n name: string\n purpose: string // une phrase\n memberIds: Id[] // v2 : liste nominative = SEULE règle d'appartenance (calculable, auditable)\n parentCircleId?: Id // emboîtement (Ostrom 8) — racine = rootCircle\n domains: string[] // tags de domaine — SUGGESTION/découverte uniquement, jamais un droit de vote\n}\n// Corpus d'une décision = union CALCULABLE : membres des cercles du scope\n// ∪ personnes nommées ∪ titulaires des mandats dont le domaine intersecte.\n// Chaque inclusion garde sa raison (Concern.reason) — auditable en un tap.\n\n// ── LA DÉCISION (pivot) ──────────────────────────────────────\ntype Reversibility = 'easy' | 'costly' | 'irreversible'\ntype Weight = 'light' | 'binding' | 'structural' // léger / engageant / structurant\n\ntype DecisionRoute =\n | 'solo' // je décide (trace facultative hors mandat)\n | 'mandate' // je décide sous mandat (trace obligatoire + fenêtre d'objection)\n | 'transmit' // le mandat d'un autre couvre → transmission au titulaire\n | 'advice' // avis des touchés, puis je décide (advice process)\n | 'collective' // session collective (modalité du protocole)\n\ntype DecisionStatus =\n | 'draft' // capturée, verdict non validé\n | 'advice' // fenêtre d'avis ouverte\n | 'objection' // fenêtre d'objection ouverte (mandate, conservatoire, allègement dérogatoire)\n | 'framing' // formulation collective : contre-propositions bornées\n | 'voting' // session ouverte (consent | binary | nuanced | election)\n | 'adopted' // en vigueur (couvre la réalisation ; revoyure via review)\n | 'rejected'\n | 'revoked' // révoquée par une décision enfant adoptée\n | 'transmitted' // remise au mandataire (terminal pour l'auteur, chaînable)\n | 'closed' // acte one-shot exécuté et revu — archivé\n\ntype TriageRule = 'R-U' | 'R0a' | 'R0b' | 'R0c' | 'R2' | 'R3' | 'R4' | 'R5' | 'R6'\n\ninterface Decision extends Entity {\n authorId: Id\n title: string // LA phrase de capture — seul champ obligatoire\n body?: string // n'apparaît que si la route l'exige (progressive disclosure)\n tags: string[] // découverte + détection de récurrence — JAMAIS un corpus\n\n // — verdict de triage (pré-rempli par le moteur, ajustable) —\n reversibility: Reversibility\n weight: Weight\n urgent: boolean\n scope: {\n selfOnly: boolean\n circleIds: Id[] // cercles touchés — explicites (suggérés par tags, confirmés par l'auteur)\n personIds: Id[] // personnes nommées en plus\n }\n route: DecisionRoute\n triageRule: TriageRule // la règle qui a tiré — affichée en français sur la fiche\n routeOverridden: boolean // dérogation journalisée\n overrideNote?: string // OBLIGATOIRE si allègement (asymétrie de dérogation)\n\n // — spécialisations chaînées (le pliage génératif) —\n underMandateId?: Id // décidée SOUS ce mandat → observatoire du mandat\n createsMandate?: MandateDraft // adoptée ⇒ crée le Mandate (réclamer un pouvoir)\n amendsClauseId?: Id // adoptée ⇒ applique la ClauseVersion liée (boucle fermée)\n protocolId?: Id // modalité si collective (résolue par le Pacte, ajustable en alourdissant)\n parentDecisionId?: Id // chaînage\n chainKind?: 'ratification' | 'revision' | 'revocation'\n\n // — temps & fenêtres —\n status: DecisionStatus\n windowEndsAt?: ISODate // fin de fenêtre avis/objection\n windowSuspendedAt?: ISODate // objection de frontière → compte à rebours SUSPENDU (visible)\n decidedAt?: ISODate\n review?: Review // obligatoire si irreversible || structural\n sunsetAt?: ISODate // clause de caducité — la réversibilité s'ingénie au lieu d'alourdir\n stewardIds: Id[] // garants de la réalisation (haut du gradient d'engagement)\n\n // — preuve & visibilité —\n visibility: 'private' | 'scope' | 'collective' // anti-flicage : trace maîtrisée hors mandat\n engraving?: Engraving\n}\n\ninterface Review {\n dueAt: ISODate\n verdict?: 'confirmed' | 'revise' | 'revoke' // verdict de réalité — 3 boutons\n note?: string\n decidedAt?: ISODate\n}\n\ninterface Engraving {\n sha256: string // hash canonique du PV JSON de la décision\n engravedAt: ISODate\n proofLevel: 'local' // v2 : UNIQUEMENT 'local' — libellé « empreinte locale — démo »\n ipfsCid?: string // réservé v3 ('ipfs') — jamais affiché tant que non réel\n chainRef?: string // réservé v3 ('chain')\n}\n\n// ── Inclusion ────────────────────────────────────────────────\ninterface Concern extends Entity {\n decisionId: Id\n personId: Id\n origin: 'computed' | 'declared' // calculé | « Ça me concerne »\n reason: string // calculé : « membre du cercle Budget » — affichée au tap\n declaredNote?: string // volontaire : motif optionnel, public\n beforeSnapshot: boolean // déclaré avant snapshot ⇒ vote ; après ⇒ voix consultative\n}\n\ninterface Objection extends Entity {\n decisionId: Id\n personId: Id\n kind: 'content' | 'boundary' // fond | « j'ai été oublié / untel manque » — boundary SUSPEND la fenêtre\n argument: string // toujours motivée\n status: 'open' | 'withdrawn' | 'integrated' | 'escalated'\n resolutionNote?: string\n}\n\ninterface Advice extends Entity { // avis (advice process)\n decisionId: Id\n personId: Id\n position: 'favorable' | 'reserved' | 'unfavorable'\n note?: string\n}\n\n// ── Le pouvoir ───────────────────────────────────────────────\ninterface Mandate extends Entity {\n title: string\n holderId: Id\n originDecisionId: Id // le mandat EST né d'une décision — cliquable\n domain: {\n circleIds: Id[] // couverture CALCULABLE — l'entrée R0b/R0c du triage\n tags: string[] // lisibilité + suggestion (jamais seule base du match)\n }\n startsAt: ISODate\n endsAt: ISODate // toujours borné — pas de mandat perpétuel implicite\n electorCircleId: Id // corpus d'élection = corpus de révocation\n nominationMethod: 'ratified-self' | 'election-no-candidate' | 'nuanced-vote' | 'consent' | 'draw' | 'rotation'\n reports: MandateReport[] // redevabilité\n status: 'proposed' | 'active' | 'expired' | 'revoked'\n}\ninterface MandateReport { dueAt: ISODate; deliveredAt?: ISODate; content?: string }\ninterface MandateDraft {\n title: string\n domainCircleIds: Id[]\n domainTags: string[]\n durationDays: number\n reportEveryDays?: number\n}\n\n// ── Les textes ───────────────────────────────────────────────\ninterface TextDoc extends Entity {\n slug: string\n title: string\n role: 'pact' | 'reference' // pact = réglages du collectif ; reference = engagements, licence…\n description: string\n provenance?: Provenance // ex-genesis_json, normalisé\n}\ninterface Provenance {\n sources: { title: string; url: string; date?: string; version?: string }[]\n voteRecord?: { // ex. Forgeron : 97/23/19, W=7224, D30M50B.1G.2S.1\n url: string; modeParams: string; period: string\n result: { for: number; against: number; invalid?: number; wotSize: number; thresholdRequired: number; status: string }\n }\n contributors?: string[]\n notes?: string // typos des textes votés : SIGNALÉES ici, jamais corrigées sans vote\n}\n\ntype InertiaPreset = 'low' | 'standard' | 'high' | 'max'\n// Câblage RÉEL preset → formule (fini l'inertie décorative) :\n// low={M:50,G:0.1} standard={M:50,G:0.2} high={M:60,G:0.4} max={M:66,G:0.6} (B=0.1, C=0 partout)\n\ninterface Clause extends Entity {\n docId: Id\n section: string // ex. « E. Certification »\n position: number\n code: string // ex. « E9 »\n title: string\n inertia: InertiaPreset // détermine le protocole d'amendement — enfin effectif\n currentVersionId?: Id\n settingKey?: string // clauses du Pacte : clé machine, ex. 'triage.smallGroupMax'\n}\n\ninterface ClauseVersion extends Entity {\n clauseId: Id\n decisionId: Id // TOUTE version naît d'une décision (y compris fondatrice seedée)\n versionLabel: string // '2.0.0-fr'\n content: string // markdown\n settingValue?: Json // valeur machine si clause de Pacte\n status: 'current' | 'proposed' | 'superseded' | 'rejected'\n adoptedAt?: ISODate\n}\n\n// ── Modalités & vote ─────────────────────────────────────────\ntype VoteMethod = 'consent' | 'binary' | 'nuanced' | 'election'\n\ninterface Protocol extends Entity {\n name: string // ex. « Vote WoT standard »\n method: VoteMethod\n description: string\n durationDays: number\n formula: FormulaParams // SOURCE DE VÉRITÉ structurée (FormulaConfig v1 fusionnée)\n modeParams: string // DSL DÉRIVÉ affiché : 'D30M50B.1G.2S.1' — jamais édité à la main\n pactClauseId?: Id // référencé par le Pacte ⇒ méta-gouverné : le modifier = amender le Pacte\n}\ninterface FormulaParams {\n majorityPct: number // M\n baseExponent: number // B (défaut 0.1)\n gradientExponent: number // G — l'inertie\n constantBase: number // C (défaut 0)\n smithExponent?: number // critère additionnel sous-toile : ceil(smithCount^exp)\n techcommExponent?: number\n nuancedMinParticipants?: number\n nuancedThresholdPct?: number\n}\n\ninterface VoteSession extends Entity {\n decisionId: Id\n protocolId: Id\n corpusPersonIds: Id[] // SNAPSHOT à l'ouverture — l'inclusion tardive ne renverse pas un vote\n corpusSize: number // W — plus jamais 0\n opensAt: ISODate\n closesAt: ISODate\n status: 'open' | 'closed'\n outcome?: 'adopted' | 'rejected'\n // INVARIANT : tallies TOUJOURS recalculés depuis Vote[] — jamais stockés dénormalisés\n}\n\ninterface Vote extends Entity {\n sessionId: Id\n voterId: Id\n value: 'for' | 'against' | 0 | 1 | 2 | 3 | 4 | 5 // binaire | nuancé (0=CONTRE … 5=TOUT À FAIT)\n comment?: string // OBLIGATOIRE si négatif (against, 0, 1)\n supersedesVoteId?: Id // re-vote audité — l'historique reste\n}\n\n// ── Réglages résolus du Pacte (fonction pure — PAS une table settings) ──\ninterface CollectiveSettings { // resolveSettings(pactClauses, versions) — lu par triage et écrans\n triage: {\n smallGroupMax: number // défaut 5 — clé 'triage.smallGroupMax'\n collectiveMin: number // défaut 50 — bascule vers la formule inertielle\n consentMax: number // défaut 7 — ≤7 consentement ; 8..collectiveMin nuancé\n objectionWindowHours: number // défaut 48\n adviceWindowHours: number // défaut 72\n framingDays: number // défaut 14 — contre-propositions bornées (structurant)\n concernEscalateRatio: number // défaut 0.5 — signal d'affluence\n recurrenceThreshold: number // défaut 3 — suggestion de mandat\n reviewDelayDays: number // défaut 90\n }\n protocolByRange: { // protocoles nommés, référencés par des clauses du Pacte\n consent: Id; nuanced: Id; inertialBinary: Id; election: Id\n clauseByInertia: Record // protocole d'amendement par preset d'inertie\n }\n}\n\n// ── Moteurs purs (frontend/app/engine/ — implémentation UNIQUE, testée vitest) ──\n// threshold.ts : wotThreshold(W, T, {M,B,G,C}) → int — port exact de backend/app/engine/threshold.py (ceil)\n// + smithThreshold, techcommThreshold, nuancedResult (ports de smith/techcomm/nuanced)\n// SPEC EXÉCUTABLE : cas Forgeron W=7224 T=120 M50 B.1 G.2 ⇒ 94 (test_threshold.py porté)\n// triage.ts : triage(input, ctx, settings) → Verdict — port étendu de engine/qualifier.py (R1-R8)\n// modeParams.ts : parse/format du DSL (consolidation de utils/mode-params.ts)\n// settings.ts : resolveSettings(clauses, versions) → CollectiveSettings (défauts + valeurs adoptées)\n// state.ts : canTransition(decision, to) → boolean — machine à états UNIQUE (fin des copies Decision/Mandate)\n// impact.ts : computeConcerned(scope, circles, mandates) → { personId, reason }[]\n\ninterface TriageInput {\n title: string; tags: string[]\n scope: Decision['scope']\n reversibility: Reversibility\n weight: Weight\n urgent: boolean\n amendsClauseId?: Id\n}\ninterface TriageContext { // assemblé par le store, consommé pur — testable sans I/O\n myActiveMandates: Mandate[]\n otherActiveMandates: Mandate[]\n matchingClauses: Clause[] // index Q0 « déjà décidé ? » — le MÊME index que Cmd+K\n similarRecentCount: number // décisions adoptées partageant ≥2 tags sur 90 j\n computedConcernedIds: Id[]\n}\ninterface Verdict {\n route: DecisionRoute\n rule: TriageRule\n explanation: string // UNE phrase française — affichée telle quelle, jamais reformulée\n protocolId?: Id\n windowHours?: number\n framingDays?: number\n reviewRequired: boolean\n engravingSuggested: boolean\n conservatoryChain?: boolean // urgence : la décision de ratification chaînée est créée automatiquement\n alternatives: { route: DecisionRoute; label: string; cost: string }[] // « Je choisis autrement »\n suggestion?: { kind: 'claim-mandate' | 'create-rule'; prefill: Json } // R6 — jamais bloquant\n}\n\n// ── Persistance locale & synchro future ──────────────────────\ninterface Bundle { // export/import ET seeds — le MÊME chemin de code (marque blanche testée)\n schemaVersion: 2\n exportedAt: ISODate\n collective: Collective\n people: Person[]; circles: Circle[]\n decisions: Decision[]; concerns: Concern[]; objections: Objection[]; advices: Advice[]\n mandates: Mandate[]\n docs: TextDoc[]; clauses: Clause[]; versions: ClauseVersion[]\n protocols: Protocol[]; sessions: VoteSession[]; votes: Vote[]\n}\n// data/persistence.ts : IndexedDB (idb-keyval), clé `ld2:`, écriture débouncée 500 ms.\n// exportBundle() → fichier JSON téléchargé ; importBundle(file) → validation structurelle + détection de collisions d'ids.\n// Prêt-synchro : UUIDv7, updatedAt (LWW champ à champ côté serveur futur), archivedAt (soft delete),\n// Vote/Advice/Objection append-only. Le futur adaptateur FastAPI implémente le même contrat de stores\n// Pinia (collective, people, decisions, mandates, texts, votes) — AUCUN écran ne change.", + "cycle_de_vie": "ÉMERGENCE — la capture : une phrase dans la barre « Qu'est-ce que tu décides ? » (Fil ou FAB). Crée Decision{status:'draft'}. Dix secondes visées, aucun autre champ obligatoire.\n\nTRIAGE — le verdict : triage() pur recalcule à chaque geste ; valider le verdict fait passer draft → l'état de départ de la route. Le verdict est toujours expliqué (une phrase française) et toujours contournable (alourdir libre 1 tap ; alléger = note obligatoire + fenêtre d'objection imposée — l'asymétrie journalisée).\n\nFORMULATION (route collective, poids binding/structural) : status 'framing' — contre-propositions bornées (framingDays, défaut 14 j) : ClauseVersions ou variantes de body avec diff. Sinon la route collective saute directement au vote. C'est ici que la formulation s'affine — chaque version est éditable, comparée, retirable.\n\nDÉLIBÉRATION — trois formes selon la route :\n- 'solo' selfOnly : AUCUNE délibération — draft → adopted immédiat (2 gestes au total). Trace facultative, visibility choisie ('private' par défaut hors mandat).\n- 'mandate' et solo-avec-tiers-dérogatoire : draft → objection (fenêtre objectionWindowHours=48 h, touchés notifiés dans leur Fil, 2 boutons inline « Ça me va / J'objecte »). Silence vaut accord UNIQUEMENT si reversibility=='easy' — sinon la fenêtre exige au moins un « Ça me va » par cercle touché. À échéance sans objection ouverte → adopted. Objection 'content' maintenue → escalade : framing/voting (consentement du cercle). Objection 'boundary' (« j'ai été oublié ») → windowSuspendedAt posé, compte à rebours SUSPENDU jusqu'à résolution — la frontière prime sur le fond.\n- 'advice' : draft → advice (fenêtre adviceWindowHours=72 h) ; les touchés déposent Advice ; à échéance l'auteur décide → adopted (avis annexés à la fiche). Objection formelle pendant la fenêtre → escalade en collective/consent.\n- 'collective' : framing? → voting : VoteSession créée, corpus SNAPSHOTTÉ (union calculée + déclarés beforeSnapshot), W fixé. Clôture à closesAt ou décision manuelle du garant → outcome par le moteur unique (consent : zéro objection maintenue ; binary : votesFor ≥ wotThreshold ; nuanced : nuancedResult ; election : désignation) → adopted | rejected.\n\nADOPTION : decidedAt posé. Si amendsClauseId → la ClauseVersion liée passe 'current', l'ancienne 'superseded' — application AUTOMATIQUE, boucle vote⇒texte fermée. Si createsMandate → le Mandate est créé 'active'. Si weight structural ou reversibility irreversible → review.dueAt obligatoire (posée AVANT l'exécution) et gravure proposée. sunsetAt optionnel : à échéance, la décision expire d'elle-même (caducité) avec relance dans le Fil.\n\nRÉALISATION / ÉVALUATION : la décision vit en 'adopted', visible au registre ; stewardIds (garants) portent l'exécution ; les rapports de mandat et revoyures dues remontent dans le Fil. À review.dueAt : verdict de réalité en 3 boutons — 'confirmed' (review close ; un acte one-shot passe 'closed'), 'revise' (spawn décision enfant chainKind 'revision', pré-remplie, sous le protocole/l'inertie d'origine), 'revoke' (spawn chainKind 'revocation').\n\nRÉVISION / RÉVOCATION : toute décision adoptée porte la poignée « Remettre en question » — ouvre /decider pré-rempli (parentDecisionId, chainKind), protocole et inertie D'ORIGINE affichés : révoquer suit toujours le chemin qui a adopté, jamais improvisé, jamais plus coûteux. Si la décision enfant 'revocation' est adoptée → le parent passe 'revoked' (cimetière de la Mémoire).\n\nURGENCE (transversale) : toggle « décision conservatoire » — si reversibility != 'irreversible', la voie la plus légère s'exécute MAINTENANT (adopted conservatoire + fenêtre d'objection élargie) et la décision de ratification chaînée (chainKind 'ratification', route normale calculée) est créée et programmée automatiquement. Si irréversible : le raccourci est REFUSÉ avec explication. L'urgence diffère l'inclusion, ne la supprime jamais.\n\nTRANSMISSION : route 'transmit' → status 'transmitted' (terminal pour l'auteur) ; la tension apparaît dans le Fil du mandataire qui peut la reprendre (décision chaînée) ; alternative offerte : contester le mandat (revocation sur le mandat).\n\nMACHINE À ÉTATS UNIQUE : engine/state.ts est la seule table de transitions (draft→{advice,objection,framing,voting,adopted,transmitted} ; advice→{adopted,voting} ; objection→{adopted,framing,voting} ; framing→voting ; voting→{adopted,rejected} ; adopted→{revoked,closed} ; + garde windowSuspendedAt). Toute mutation de status passe par canTransition() — fin des machines copiées-collées Decision/Mandate de la v1.", + "triage_algorithme": "MOTEUR : engine/triage.ts — fonction PURE triage(input, ctx, settings) → Verdict. Port étendu de backend/app/engine/qualifier.py (tests test_qualifier.py portés en vitest comme spec exécutable). Tous les seuils viennent de CollectiveSettings, résolus depuis les clauses du Pacte — le triage est gouverné par ceux qu'il gouverne, les changer = amender le Pacte sous l'inertie de la clause.\n\nENTRÉES EXPLICITES (3 chips maximum, récoltées pendant la saisie — jamais un formulaire) :\n1. Touchés → scope : pré-calculé par croisement tags de la phrase × domains des cercles/mandats, corrigeable en un tap (moi seul / cercles cochés / personnes nommées). Plancher : l'auteur peut élargir, JAMAIS restreindre sous le calcul.\n2. Réversible ? → reversibility : facilement ('easy') / coûteux à défaire ('costly') / irréversible ('irreversible').\n3. Ça engage quoi ? → weight : rien ('light') / du temps ou des ressources ('binding') / la structure, les règles, du long terme ('structural').\nENTRÉES INFÉRÉES (zéro saisie) : mes mandats actifs couvrants, mandats des autres couvrants, clauses en vigueur qui matchent (index Q0 = le même index que Cmd+K — chercher et décider sont le même geste), récurrence (similarRecentCount : décisions adoptées partageant ≥2 tags sur 90 j), toggle urgence, clause visée (pré-lien depuis un texte).\n\nRÈGLES, DANS L'ORDRE — première qui matche (chaque règle a sa phrase française affichée telle quelle) :\n- R-U URGENCE (overlay) : urgent && reversibility != 'irreversible' ⇒ voie la plus légère légitime MAINTENANT (conservatoire : adopted + fenêtre d'objection élargie ×2) + décision de ratification chaînée créée automatiquement avec la route normale. urgent && irreversible ⇒ raccourci REFUSÉ : « Irréversible : l'urgence ne peut pas contourner le collectif » — route normale avec fenêtres minimales.\n- R0a DÉJÀ DÉCIDÉ : une clause en vigueur couvre ⇒ « C'est déjà décidé (E9, Acte Forgeron) — agis » avec lien, OU « Conteste la règle » ⇒ bascule en R3 avec l'inertie de la clause.\n- R0b MON MANDAT : scope.circleIds ⊆ domain.circleIds d'un de mes mandats actifs ⇒ route 'mandate' : « Ton mandat Modération couvre — décide, c'est tracé » ; trace obligatoire à l'observatoire + fenêtre d'objection 48 h aux touchés.\n- R0c MANDAT D'UN AUTRE : ⇒ route 'transmit' : « Le mandat de Cgeek couvre — transmets-lui » (un tap) ; alternative : « Conteste ce mandat » (révocation chaînée, protocole d'élection d'origine).\n- R2 MOI SEUL : scope.selfOnly ⇒ route 'solo' : « Personne d'autre n'est touché — décide » ; trace facultative, visibility 'private' par défaut, zéro fenêtre ; si irreversible : revoyure proposée pré-cochée (retirable — sur soi-même l'outil ne s'interpose pas).\n- R3 CLAUSE VISÉE : amendsClauseId ⇒ route 'collective', protocole = settings.protocolByRange.clauseByInertia[clause.inertia] : « Tu proposes une version de E9 — inertie haute, vote du corpus » ; framing selon weight.\n- R4 PETIT CERCLE RÉVERSIBLE : reversibility=='easy' && |concernés calculés| ≤ smallGroupMax(5) ⇒ route 'advice' : « Réversible et 3 personnes touchées — demande leur avis puis décide » ; fenêtre 72 h ; toute objection formelle escalade en R5/consent.\n- R5 COLLECTIF (défaut de l'escalade) : sinon ⇒ route 'collective', modalité par taille du corpus calculé : ≤ consentMax(7) → consentement (tour d'objections, zéro vote) ; ≤ collectiveMin(50) → vote nuancé 6 niveaux ; > 50 → vote binaire à formule inertielle (Seuil = C + B^W + (M + (1−M)(1−(T/W)^G))·max(0,T−C)) ; weight 'structural' ⇒ + framing 14 j + revoyure obligatoire + gravure suggérée + inertie haute ; reversibility 'irreversible' ⇒ revoyure obligatoire.\n- R6 RÉCURRENCE (overlay, jamais bloquant) : similarRecentCount ≥ recurrenceThreshold(3) ⇒ suggestion sous le verdict : « 3e fois — réclame un mandat » (createsMandate pré-rempli, wizard) ou « crée une règle » (nouvelle clause) — Ockham dans le temps : une décision collective en engendre mille individuelles.\n\nDÉROGATION ASYMÉTRIQUE (contrat du rasoir) : ALOURDIR est toujours libre — un tap dans « Je choisis autrement », journalisé (routeOverridden). ALLÉGER exige overrideNote obligatoire ET impose une fenêtre d'objection 48 h aux touchés (le consentement du cercle EST la fenêtre — pas de machinerie en plus) ; l'allègement est affiché en évidence sur la fiche. Le verdict n'est jamais un juge : il recommande, explique, et laisse faire.\n\nINCARNATION UX (/decider, accessible partout — FAB mobile, bouton header, pré-rempli par query : ?clause=, ?parent=&chain=, ?mandat=) : la phrase en haut ; les résultats Q0 « déjà décidé » apparaissent inline DÈS LA FRAPPE ; 3 chips en colonne (une seule visible à la fois sur mobile) ; toggle urgence discret. La carte VERDICT se recalcule à chaque geste : grande icône de route, la règle en français, la frise du processus (qui est convoqué — avatars, combien de temps — fenêtres, quel seuil — jauge miniature), UN bouton principal (« Décide », « Demande leur avis », « Ouvre le vote »…). En dessous, toujours visible : « Je choisis autrement » (alternatives dépliées avec leur coût). MÉTRIQUE PRODUIT : chemin nominal solo = phrase + Entrée + bouton = 2 gestes, < 10 secondes ; le tunnel n'affiche JAMAIS un champ qui ne change pas le verdict.", + "cercles_inclusion": "DEUX PORTES, UNE LISTE, CHAQUE ENTRÉE SOURCÉE (table Concern, horodatée).\n\nPORTE AUTOMATIQUE — les impactés calculés : à la validation du verdict, engine/impact.ts calcule l'union CALCULABLE : membres des cercles du scope ∪ personnes nommées ∪ titulaires des mandats dont le domaine intersecte le scope. Chaque inclusion crée Concern{origin:'computed', reason} avec sa raison en clair : « membre du cercle Forgerons », « titulaire du mandat Modération », « nommé par l'auteur ». AUDITABLE : un tap sur un avatar affiche la chaîne d'inclusion — jamais un « vous êtes invité » opaque. Les tags ne donnent JAMAIS un droit de vote : ils suggèrent des cercles, l'inclusion repose sur les listes nominatives des cercles (v2) — règle d'appartenance auditable, extensible plus tard (WoT, géo) sans changer le modèle. PLANCHER ANTI-EXCLUSION : l'auteur peut élargir la liste, jamais la restreindre sous le calcul. Les inclus voient la décision dans leur Fil avec le verdict et la fenêtre — une ligne dans l'écran qu'ils ouvrent déjà, pas un canal de plus.\n\nPORTE VOLONTAIRE — « Ça me concerne » : bouton présent sur toute décision visible (selon Decision.visibility et Collective.isTransparent), SANS justification requise — c'est un droit, pas une demande ; champ « pourquoi » optionnel et public. Crée Concern{origin:'declared', beforeSnapshot}. Droits : déclaré AVANT le snapshot de session ⇒ entre au corpus de vote ; APRÈS ⇒ voix consultative (ses avis et objections comptent et sont notifiés, pas de vote) — règle simple, affichée en compte à rebours sur la fiche. Les déclarations sont publiques : la transparence dissuade le noyautage.\n\nOBJECTION DE FRONTIÈRE (suspensive — Ostrom, la contestation des frontières prime sur le fond) : « J'ai été oublié » / « Untel manque » = Objection{kind:'boundary'} ⇒ windowSuspendedAt posé, le compte à rebours de la décision est SUSPENDU (pastille pause visible) jusqu'à résolution (intégration au périmètre ou réponse motivée) ; priorité maximale dans le Fil de l'auteur.\n\nSIGNAL D'AFFLUENCE (la subsidiarité auto-corrective) : si |déclarés| ≥ concernEscalateRatio(0.5) × |calculés|, bannière sur la fiche et dans le Fil de l'auteur : « Le périmètre déborde — élargis d'un cran » (ajout du cercle parent ou du collectif entier, un tap, re-triage, journalisé). L'impact réel corrige le périmètre déclaré ; un afflux massif déclenche une revue de périmètre, jamais un comptage brut.\n\nGRADIENT D'ENGAGEMENT (quatre niveaux, affichés sur la fiche) : observateur (lecture) < concerné (avis, objection — computed hors corpus ou declared tardif) < lié (vote — dans le corpus snapshotté) < garant (stewardIds — exécution et rapport).\n\nANTI-BRIGADING PAR CONSTRUCTION : déclarations publiques + snapshot qui ferme le corpus + entrée tardive consultative + escalade de périmètre — et la formule inertielle absorbe naturellement l'afflux : gonfler W sans que T suive fait MONTER le seuil vers la quasi-unanimité. ANTI-FLICAGE (ligne rouge produit) : hors mandat, la trace est facultative et visibility choisie ('private' par défaut pour le solo personnel) ; sous mandat, trace obligatoire visible du cercle électeur — la redevabilité s'applique au pouvoir, pas aux personnes.", + "navigation": "SHELL : layouts/default.vue (le shell sort enfin de app.vue). Header : logo-stamp (icône puits + rotation -10°) + « libreDecision » ; WorkspaceSelector (collectifs locaux + « Créer un collectif ») ; recherche globale Cmd+K (index unique : clauses en vigueur + décisions + mandats — le MÊME index que Q0 du triage) ; MoodSwitcher ; bouton « Décider » (desktop). FAB « Décider » 56 px en mobile, au-dessus du sceau. Sceau hexagramme #48 Tsing 井 fixe bas-droite (géométrie SVG canonique des consignes). layouts/bare.vue pour /creer.\n\nSIDEBAR — 4 ENTRÉES, pas une de plus (le rasoir appliqué à la nav) :\n1. Aujourd'hui (/) — le Fil + la capture. L'écran qu'on ouvre chaque jour ; à 3 personnes, c'est tout l'outil.\n2. Décisions (/decisions) — le registre + la Mémoire.\n3. Textes (/textes) — les documents sous vote permanent, Pacte épinglé en tête.\n4. Mandats (/mandats) — les pouvoirs délégués.\n\nDISSOLUTIONS VOLONTAIRES : les protocoles vivent dans le Pacte (/textes/pacte, clauses F) et dans l'étape modalité du verdict — pas de section. L'Atelier des formules = /textes/formules, lié depuis CHAQUE seuil affiché dans l'app. Le Sanctuaire = badge 井 + fiche de preuve sur les objets gravés + filtre ?filtre=gravees du registre — pas de section. La boîte à outils v1 disparaît : ContextMapper devient le « pourquoi ? » dépliable du verdict, SocioElection devient la modalité 'election' en session, WorkflowMilestones nourrit les gabarits. Pas de page réglages : les réglages SONT le Pacte ; les préférences locales (mood, export/import, identité, mode démo) = /donnees, accessible du menu du WorkspaceSelector.\n\nROUTES :\n/ — Aujourd'hui (Fil + capture) · /decider — tunnel de triage (pré-remplissable : ?clause=, ?parent=&chain=, ?mandat=) · /decisions — registre · /decisions/memoire — Mémoire (stats, revues, cimetière) · /decisions/[id] — fiche décision (URL permanente de la capture à la révocation) · /decisions/[id]/vote — salle de vote · /textes — bibliothèque · /textes/formules — Atelier des formules · /textes/[slug] — document vivant (pacte inclus, slug 'pacte-…') · /mandats — liste · /mandats/nouveau — wizard · /mandats/[id] — fiche mandat · /creer — onboarding collectif (layout bare) · /donnees — données locales, export/import, identité, mode démo.\n\nCOUCHES CODE : app/types/domain.ts (le noyau) ; app/engine/ (threshold, triage, modeParams, settings, state, impact — purs, vitest) ; app/data/ (persistence.ts IndexedDB + seeds/*.bundle.json + templates.ts gabarits) ; app/stores/ (collective, people, decisions, mandates, texts, votes — contrat commun prêt pour l'adaptateur FastAPI) ; app/composables/ (useMood conservé, useSettings, useFeed — sélecteurs purs, useSearch — index Cmd+K/Q0) ; app/components/ par domaine (verdict/, decisions/, votes/, texts/, mandates/, circles/, common/). Le Fil est un SÉLECTEUR PUR sur les stores — zéro table notification, zéro double-fetch. Budget : 400 lignes max par page, composants extraits (anti-syndrome protocols/index.vue 1545 l.).", + "ecrans": [ + { + "route": "/creer", + "nom": "Créer un collectif", + "role": "Onboarding en 3 écrans — le premier usage EST le tutoriel, zéro documentation", + "contenu": "Écran 1 : nom, couleur, mood. Écran 2 : gabarit en 5 cartes qui TRADUISENT le vocabulaire (Informel « à 3-15, on se fait confiance » / Association / Coopérative / Commune / Communauté monnaie libre) — chaque gabarit installe le Pacte pré-rempli (clauses settingKey avec les bons seuils), 4-6 protocoles canoniques et le cercle racine ; textes en langage concret (« qui doit être d'accord ? », jamais « corpus électoral »). Écran 3 : moi (prénom, isMe) + membres initiaux (noms, ajout rapide). Boutons d'import : « Explorer Duniter Ğ1 » et « Explorer l'Atelier du Canal » chargent les bundles seed par le MÊME chemin qu'un import utilisateur. Atterrissage sur / avec la première décision proposée : « Adoptez votre Pacte » (collective, consentement) — réussite < 10 minutes, testée avec des non-théoriciens." + }, + { + "route": "/", + "nom": "Aujourd'hui", + "role": "L'écran quotidien unique : capture + Fil personnel — la boîte de réception de la démocratie", + "contenu": "Barre de capture sticky « Qu'est-ce que tu décides ? » (Entrée → /decider avec la phrase). Fil trié par échéance, sections vides masquées : fenêtres d'objection où je suis touché (2 boutons inline « Ça me va / J'objecte », compte à rebours, pastille pause si suspendu) ; avis demandés (3 boutons favorable/réservé/défavorable inline) ; votes ouverts où je suis au corpus (jauge miniature, lien salle de vote) ; revoyures dues (verdict de réalité 3 boutons inline) ; rapports de mandat dus ; objections de frontière à traiter (priorité haute) ; suggestions de la boucle apprenante (« 3e fois — réclame ce mandat », « ce protocole n'a jamais servi — élague »). Badge UNIQUE de dette décisionnelle dans le header de section. Ensuite : l'activité du collectif (selon visibility). Tout est sélecteur Pinia pur — aucun stockage de notifications. Mobile-first absolu : c'est l'écran du comptoir, pas de la réunion." + }, + { + "route": "/decider", + "nom": "Le Verdict (tunnel de triage)", + "role": "Une phrase → un chemin exécutable en un tap — 10 s pour le routinier, 3 min pour le stratégique", + "contenu": "Phrase en haut (pré-remplie depuis la capture ou les query params clause/parent/chain/mandat). Résultats Q0 « déjà décidé ? » inline dès la frappe (même index que Cmd+K) : clause en vigueur → « agis » ou « conteste » ; mandat d'un autre → « transmets » ou « conteste le mandat ». 3 chips : touchés (pré-calculés par tags→cercles, avatars visibles, plancher non restrictible), réversibilité (3 options), engagement (3 options → weight). Toggle « urgence » discret (conservatoire). Carte VERDICT live : grande icône de route, règle en français (« R4 — Réversible et 3 personnes touchées → demande leur avis puis décide »), frise qui/combien-de-temps/quel-seuil, revoyure et gravure pré-cochées si exigées, UN bouton principal. « Je choisis autrement » toujours visible : alourdir 1 tap journalisé ; alléger = note obligatoire + fenêtre d'objection imposée. « Pourquoi ? » dépliable (pédagogie ContextMapper recyclée). Suggestion R6 sous le verdict (mandat/règle pré-remplis). Valider crée la Decision dans le bon état avec données STRUCTURÉES (cercles en ids, protocole appliqué — les deux fuites du wizard v1 corrigées)." + }, + { + "route": "/decisions", + "nom": "Registre des décisions", + "role": "Toutes les décisions par état — SectionLayout conservé", + "contenu": "Pills d'état (préparation / avis / objection / formulation / vote / en vigueur / clos / rejeté / révoqué) reprises du SectionLayout v1 avec les couleurs du cycle de vie. Filtres : cercle, tag, « me concerne », poids, gravées (井). Recherche locale = même index global. Cartes décision : titre, route (icône), poids, échéance, jauge miniature si vote, badge 井. Lien vers l'onglet Mémoire. Zéro création ici — on crée par la capture." + }, + { + "route": "/decisions/[id]", + "nom": "Fiche décision", + "role": "URL permanente de l'objet pivot, de la capture à la révocation — progressive disclosure par état", + "contenu": "En-tête : titre, route+règle de triage en français, poids, badges (urgence conservatoire, dérogation allégée avec note, 井). Timeline verticale du cycle (états traversés, horodatés). Panneau Cercles : avatars empilés calculés (tap → raison d'inclusion) et déclarés (glyphe main, note publique), bouton « Ça me concerne », compte à rebours du snapshot, signal d'affluence (bannière élargissement 1 tap), objection de frontière (suspend la fenêtre — pastille pause). Selon l'état seulement : bloc fenêtre (objection/avis, boutons inline, silence-vaut-accord affiché SEULEMENT si réversible) ; bloc formulation (versions, diff unifié, contre-propositions bornées) ; bloc session (lien salle de vote, résultat, corpus W/T) ; bloc vigueur (garants stewardIds, sunset, revoyure due avec 3 boutons) ; chaînage (parent/enfants : ratification, révision, révocation — cliquables). Poignée « Remettre en question » sur toute décision adoptée : ouvre /decider pré-rempli avec protocole et inertie D'ORIGINE affichés. Actions : export PV markdown (registre de légitimité, pas substitut légal), graver (fiche de preuve : sha256, date, niveau « empreinte locale — démo » — honnête). Budget 400 lignes, composants extraits." + }, + { + "route": "/decisions/[id]/vote", + "nom": "Salle de vote", + "role": "Le geste démocratique central — voter au pouce en 20 secondes", + "contenu": "Contexte : titre, texte/diff de la version proposée, lien fiche. Corpus visible : W, T, qui (avatars), snapshot horodaté, critères additionnels Smith/ComTech si protocole. Selon la méthode : consentement (« Ça me va » / « J'objecte » argumentée) ; binaire pour/contre ; nuancé 6 niveaux (0 CONTRE → 5 TOUT À FAIT, commentaire OBLIGATOIRE inline sur 0-1). Jauge inertielle : arc animé, seuil qui DESCEND quand la participation monte, phrase générée en français (« 94 pour requis, car 120 votants sur 7 224 — la formule exige presque l'unanimité à faible participation »), lien « comprendre ce seuil » → Atelier pré-réglé. Mon vote remplaçable (supersedesVoteId — historique audité visible). Clôture : résultat, décompte recalculé depuis les votes, adoption ⇒ application automatique (clause current, mandat créé) — zéro bouton d'acceptation manuel." + }, + { + "route": "/decisions/memoire", + "nom": "Mémoire", + "role": "Le miroir : la démocratie rendue mesurable — et le cimetière comme apprentissage", + "contenu": "Statistiques d'autonomie du collectif : ratio seul/mandat/avis/collectif, temps médian de décision par route, taux d'objection, participation moyenne (T/W), dérogations (alourdies/allégées). Revues dues et verdicts de réalité rendus (confirmée/révisée/révoquée). Cimetière des révoquées avec leur chaîne (qui a révoqué, comment, pourquoi). Décisions jamais revues : relance unique puis archivage proposé. Suggestions d'élagage : protocole jamais invoqué, mandat sans trace depuis 6 mois → revue. Filtres personne/cercle/tag/période. Graphiques sobres (skill dataviz à l'implémentation)." + }, + { + "route": "/textes", + "nom": "Textes", + "role": "La bibliothèque des documents sous vote permanent — on y consulte et conteste, on n'y travaille pas", + "contenu": "Pacte épinglé en tête (badge « les règles du jeu »). Cartes documents : titre, rôle, nombre de clauses, sessions en cours (mini-jauges), provenance (Ğ1 : Acte d'engagement Certification 33 clauses, Acte d'engagement forgeron 59 clauses v2.0.0-fr, Engagement Comité Technique). Lien Atelier des formules. Filtre gravées." + }, + { + "route": "/textes/[slug]", + "nom": "Document vivant", + "role": "L'héritier de documents/[slug] v1 — débranché des mocks, boucle vote⇒texte fermée", + "contenu": "Sections repliables + scroll-spy conservés. Chaque clause : code, titre, pastille d'inertie (low→max, CÂBLÉE au protocole d'amendement), mini-jauge branchée sur la vraie session si version proposée, statut. Tap clause → détail : version courante, versions proposées avec diff, décision fondatrice et amendements chaînés (cliquables). « Proposer une version » → /decider?clause= pré-lié (R3, protocole de l'inertie). Vue projetée conservée : « le document si les votes en cours passaient ». InertiaSlider pédagogique → Atelier pré-réglé. GenesisBlock rendu depuis Provenance normalisée (sources, voteRecord 97/23/19, contributeurs, notes — typos signalées jamais corrigées). Spécifique Pacte : les clauses settingKey affichent la valeur machine EN CLAIR (« Petit groupe : jusqu'à 5 personnes ») et leur effet est IMMÉDIAT à l'adoption (resolveSettings) ; la clause qui règle l'inertie est protégée par l'inertie maximale (G=.6, M=66 %) — modifier les règles de modification exige la quasi-unanimité." + }, + { + "route": "/textes/formules", + "nom": "Atelier des formules", + "role": "La pépite v1 conservée telle quelle — la pédagogie du seuil", + "contenu": "FormulaEditor porté : sliders M/B/G/C (+Smith/ComTech), rendu KaTeX de Seuil = C + B^W + (M + (1−M)(1−(T/W)^G))·max(0,T−C), table de participation (seuil pour T croissant), presets d'inertie (low/standard/high/max) comparés. Branché sur engine/threshold.ts UNIQUE — le même module que la jauge et le décompte (divergence v1 threshold.py/votes.py définitivement tuée). Accessible depuis chaque seuil affiché dans l'app, pré-réglé avec les paramètres du contexte (« pourquoi 94 ? »). Cas de référence Forgeron 2026 chargé en un clic (W=7224, T=120 ⇒ 94)." + }, + { + "route": "/mandats", + "nom": "Mandats", + "role": "La carte des pouvoirs : qui peut décider quoi, jusqu'à quand", + "contenu": "Cartes par statut (proposé/actif/expiré/révoqué) : titulaire (avatar), domaine (cercles + tags), échéance, prochain rapport dû, jauge de confiance (objections/traces récentes). Taxonomie unifiée (la modale rapide techcomm/smith v1 supprimée). Bouton « Réclamer un mandat » → wizard (le plus souvent pré-rempli par R6)." + }, + { + "route": "/mandats/[id]", + "nom": "Fiche mandat", + "role": "Le pouvoir délégué, visible et reprenable", + "contenu": "Titulaire, origine (la décision qui l'a créé — cliquable), domaine explicite (cercles + tags — c'est l'entrée R0b/R0c du triage), période, cercle électeur. Observatoire : flux des décisions prises sous ce mandat (chacune avec sa fenêtre d'objection, contestable). Rapports de redevabilité : dus/rendus, contenu. Jauge de confiance : ratio objections/traces sur la période. « Demander la révocation » en un geste : ouvre /decider chaîné (chainKind revocation) avec le protocole de nomination D'ORIGINE et le cercle électeur comme corpus — jamais improvisée, jamais plus coûteuse que l'élection." + }, + { + "route": "/mandats/nouveau", + "nom": "Wizard mandat", + "role": "Le meilleur flux de la v1, conservé et branché sur le pivot", + "contenu": "3 étapes : (1) rôle et domaine (titre, cercles couverts, tags, une phrase de mission) ; (2) modalité de nomination parmi 6 (auto-désignation ratifiée, élection sans candidat, vote nuancé, consentement, tirage au sort, rotation) avec pros/cons pédagogiques ; (3) durée bornée + cadence de rapports + cercle électeur. Sortie : une Decision createsMandate routée collective (la nomination EST une décision) — pré-remplie par la suggestion R6 le cas échéant." + }, + { + "route": "/donnees", + "nom": "Données locales", + "role": "Local-first assumé : tes données, ton fichier, ta machine", + "contenu": "Export du bundle JSON (téléchargement, schemaVersion 2), import avec validation et détection de collisions, réinitialisation par collectif. Chargement des seeds (Duniter Ğ1, Atelier du Canal). Identité locale (displayName, isMe). Mode démo « agir en tant que » (switch discret vers les personnes seedées — pour dérouler votes et consentements localement, clairement étiqueté démo). Niveaux de preuve expliqués honnêtement : empreinte locale (v2) / IPFS / ancrage chaîne (à venir — jamais affiché tant que non réel). Note synchro : « un serveur pourra se brancher plus tard, tes données ne changent pas de forme »." + } + ], + "seeds": "EXTRACTION — script backend/scripts/export_seed_bundle.py (venv existant) : importe les structures de seed.py (2 552 l. : les 33 items Certification, les 59 items Forgeron, GENESIS_CERTIFICATION, GENESIS_FORGERON, VOTER_NAMES ×11, mandates_data ×3, 3 VotingProtocols + 7 FormulaConfigs, QualificationProtocol) et émet frontend/app/data/seeds/duniter-g1.bundle.json au schéma Bundle v2. TEST DE NON-PERTE (vitest, bloquant) : 33 clauses cert + 59 clauses forgeron (l'incohérence « 51 vs 59 » est tranchée par le comptage du seed actuel = 59), répartition des presets d'inertie conservée, provenance.voteRecord == {for:97, against:23, invalid:19, wotSize:7224, thresholdRequired:97, modeParams:'D30M50B.1G.2S.1'}, 11 personnes dont 5 smiths, 3 mandats, protocoles complets. Le diff est exigé AVANT toute dépréciation de seed.py. Typos des textes votés : reportées dans Provenance.notes, JAMAIS corrigées (textes adoptés — les corriger exige un vote).\n\nMAPPING Ğ1 → NOUVEAU MODÈLE :\n- Organization 'duniter-g1' → Collective{template:'free-currency', color:'#22c55e', isTransparent:true}.\n- 11 DuniterIdentity → Person : Moul, Poka, Hugo, Elois, Cgeek (wotStatus:'smith') ; Galuel, Tortue, Inso, Tuxmain, Matograine, Maaltir ('member').\n- Groups (orphelins v1) → Circles enfin branchés comme corpus : « Toile de confiance » (racine, 11 membres — étiquetée « échantillon de démonstration », le W historique 7 224 vit dans Provenance, pas dans le corpus local) ; « Forgerons » (les 5 smiths, parent TdC, domains ['forge','noeuds','runtime']) ; « Comité Technique » (Elois, Cgeek, Maaltir, Hugo, Tuxmain, parent TdC, domains ['runtime','code','on-chain']).\n- Documents → TextDoc role 'reference' : (1) engagement-certification « Acte d'engagement Certification », 33 Clauses avec presets CÂBLÉS (fin de l'inertie décorative : low→G.1, standard→G.2, high→G.4/M60, very_high→max→G.6/M66), Provenance.sources = Licence Ğ1 v0.3.0 (git.duniter.org) + Charte 1.0 rejetée (topic 31066) + prépa v0.4.0 (topic 32375) + checklist (topic 32412) — LA LICENCE VIT ICI comme source normalisée ; (2) engagement-forgeron « Acte d'engagement forgeron » v2.0.0-fr, 59 Clauses, Provenance.voteRecord complet (97/23/19, W=7224, seuil 97, période 2026-01-07→02-06, urls forum + g1vote) ; (3) engagement-comite-tech « Engagement Comité Technique » v2.0.0-fr — ENFIN seedé, contenu depuis docs/dev (research_duniter_forum.md), la dette v1 soldée.\n- NOUVEAU : pacte-duniter-g1 (TextDoc role 'pact') — clauses settingKey depuis le QualificationProtocol v1 : triage.smallGroupMax=5, triage.collectiveMin=50, consentMax=7, fenêtres 48/72 h, framingDays=14, concernEscalateRatio=0.5, recurrenceThreshold=3, reviewDelayDays=90, + clauses F référençant les protocoles nommés ; la clause d'inertie sous preset 'max'.\n- INVARIANT TENU DÈS LE SEED : chaque clause courante reçoit sa DÉCISION FONDATRICE (Decision route 'collective', status 'adopted', title « Adoption fondatrice — », liée à la ClauseVersion 'current') — « toute clause est une décision », même l'héritage.\n- 3 VotingProtocols + 7 FormulaConfigs → 6 Protocols canoniques (FormulaConfig fusionnée dans Protocol.formula) : Consentement (consent, D7) ; Vote nuancé 6 niveaux (nuanced, D30, params nuanced conservés) ; Vote WoT standard (binary, D30M50B.1G.2) ; Vote forgeron Smith (binary, D30M50B.1G.2S.1) ; Vote Comité Technique (binary, D30M50B.1G.2T.1) ; Élection sans candidat (election, D14). Les 4 presets d'inertie mappés vers clauseByInertia. Le catalogue 18 modalités reste en bibliothèque documentaire (hors seed).\n- 3 Mandates : « Comité Technique — Session 2025-2026 » (holder Cgeek, active, electorCircle TdC, domain [ComTech], steps v1 → originDecision adoptée + reports mi-mandat dû — apparaît dans le Fil) ; « Administrateur des Forgerons — Rotation 2026-Q1 » (proposed : la Decision createsMandate est EN VOTE nuancé, corpus Forgerons — démontre la nomination vivante) ; « Modération Forum » (active, domain tags ['forum','moderation']).\n- Décisions historiques : « Runtime Upgrade 1100 » (adopted, weight structural, gravée proofLevel 'local', revoyure posée) ; « Évolution de la licence v0.4.0 » (framing, versions liées aux clauses cert — la préparation réelle du forum incarnée). Votes seedés sur 2-3 sessions de clauses (corpus 11, jauges vivantes).\n\nDEUXIÈME COLLECTIF — atelier-du-canal.bundle.json (LE test permanent de la marque blanche, greffe V1) : association fictive 12 membres, template 'association', Pacte propre avec seuils DIFFÉRENTS (smallGroupMax=4, fenêtres courtes), « Règlement intérieur » 8 clauses, cercles Bureau/Ateliers/Tous, mandat Trésorerie (rapport dû), et des décisions à TOUS les états de la machine (draft, advice en fenêtre, objection SUSPENDUE par une objection de frontière, framing avec 2 versions et diff, voting nuancé en cours, adopted avec revoyure due, revoked au cimetière, conservatoire + ratification chaînée) — chaque état de l'UI a sa donnée de démonstration, et tout contenu Ğ1 en dur dans le code est structurellement impossible.\n\nGABARITS (/creer) : app/data/templates.ts — 5 mini-modèles (pacte pré-rempli + protocoles + cercle racine) : informal (consentement partout, fenêtres 24 h, pas de mandat), association (cercles + mandats statutaires + nuancé), cooperative (advice process par défaut), commune (corpus larges, binaire inertiel, transparence), free-currency (le mapping Ğ1 ci-dessus en modèle). Les seeds s'importent par le MÊME chemin de code que l'import utilisateur — le seed est un import comme un autre.", + "design_directives": "ICONOGRAPHIE DU TRIAGE (vocabulaire visuel fixe, appris une fois) : chaque route a son icône et sa teinte — solo = i-lucide-zap ; sous mandat = i-lucide-key-round ; transmettre = i-lucide-send ; avis = i-lucide-message-circle ; collectif = i-lucide-users ; urgence/conservatoire = i-lucide-siren ; « déjà décidé » = i-lucide-book-open-check ; récurrence→mandat = i-lucide-repeat. La carte Verdict : icon-container 14 px avec la grande icône de route, nom de route en 700, la règle en une phrase en dessous, frise horizontale qui/durée/seuil, bouton principal pill 20 px plein accent, alternatives en pills fantômes. Teintes de route déclinées des custom properties du mood (jamais de couleurs codées en dur).\n\nREPRÉSENTATION DES CERCLES : stacks d'avatars superposés (initiales sur fond de teinte déterministe dérivée de l'id — pas d'images), débord -8 px, compteur « +N ». Origine distinguée SANS bordure : glyphe en coin de l'avatar (i-lucide-calculator = calculé, i-lucide-hand = déclaré) + fond légèrement teinté pour les déclarés. Tap → popover chaîne d'inclusion (« membre du cercle Forgerons — via la liste du cercle »). Les cercles emboîtés se rendent en chips imbriquées (parent estompé derrière l'enfant). Le plancher calculé est visuellement verrouillé (les avatars calculés ne portent pas de croix de retrait).\n\nCYCLE DE VIE : timeline verticale à pastilles pleines reliées par un trait de fond (pas de bordure) — draft neutre, avis/objection ambre AVEC compte à rebours textuel, vote bleu avec mini-jauge, en vigueur vert, revoyure violette, révoqué terre cuite, clos gris estompé, rejeté neutre barré. SUSPENSION (objection de frontière) : pastille i-lucide-pause + fond légèrement strié + libellé « compte à rebours suspendu — frontière contestée » : l'état le plus important à rendre visible. Les pills du SectionLayout reprennent exactement ces couleurs (un seul mapping état→couleur dans le CSS).\n\nJAUGE INERTIELLE (signature du produit) : arc 270°, remplissage = votes pour, curseur de seuil qui DESCEND visiblement quand la participation monte — l'inertie rendue sensible ; W, T et seuil en chiffres clairs sous l'arc ; phrase d'explication générée en français. KaTeX réservé à l'Atelier (jamais de formule brute dans le flux). Vote nuancé : 6 segments en gradient sémantique (défavorable→favorable) construit depuis la palette du mood via le skill dataviz à l'implémentation, jamais rouge/vert crus ; le commentaire obligatoire s'ouvre inline sous les niveaux 0-1.\n\nMOODS v2 (champ lexical du puits, 2 light + 2 dark) : « Source » (light clair, accent bleu d'eau), « Margelle » (light chaud sable/pierre), « Nappe » (dark bleu profond), « Minuit » (dark neutre encre). Classes .mood-* sur + custom properties, useMood.ts conservé. Socle inchangé : borderless (profondeur par background + box-shadow), arrondis 16/12/20/14, hover cards translateY(-3px) 0.12s, mobile-first clamp(), breakpoints 480/768/1024, touch ≥ 2.25 rem.\n\nGRAVURE ET PREUVE : badge 井 en tampon incliné -10° (cohérent avec le logo-stamp) sur tout objet gravé ; fiche de preuve = sha256 en mono tronqué copiable, date, niveau de preuve en toutes lettres (« empreinte locale — démo ») — la sobriété est l'honnêteté. Sceau hexagramme #48 Tsing : géométrie SVG canonique des consignes (viewBox 112×105, 6 traits, trait 1 en bas = dernier rect), opacity ~0.28, couleur accent, drop-shadow CSS — fixe bas-droite, JAMAIS recouvert ; le FAB « Décider » (56 px, bottom-right mobile) se place AU-DESSUS du sceau avec un décalage vertical qui le laisse visible.", + "hors_scope_v2": [ + "Synchronisation backend / adaptateur FastAPI — le contrat des stores Pinia et le Bundle sont prêts ; brancher l'API n'exigera de changer aucun écran", + "Auth Duniter challenge-réponse et vérification Ed25519/Sr25519 — identités locales en v2, champ duniterAddress réservé", + "Multi-appareils et multi-utilisateurs simultanés — v2 mono-poste ; le mode démo « agir en tant que » (étiqueté) exerce les flux collectifs localement", + "Notifications sortantes (email, digest) — tous les mécanismes temporels sont affichés in-app dans le Fil ; en conséquence le silence-vaut-accord est réservé au réversible et présenté comme tel", + "WebSocket temps réel — la réactivité Pinia locale suffit ; useWebSocket conservé en attente de l'adaptateur", + "Délégation liquide / procuration — le modèle chaîné l'accueillera (v3), rien dans l'UI v2", + "IPFS et ancrage on-chain, signatures de vote vérifiées — proofLevel 'local' uniquement, libellé « empreinte locale — démo » ; les champs ipfsCid/chainRef existent mais ne s'affichent jamais tant que non réels", + "Protocoles opérationnels / ProcessTemplate (Embarquement Forgeron 7 étapes, checklists à clauses pièges) — documentés, non construits", + "Jugement majoritaire et le catalogue des 18 modalités × 9 nominations — 6 protocoles canoniques seedés, le reste en bibliothèque documentaire", + "Fédération inter-collectifs et escalade au-delà du collectif racine", + "Suggestion LLM de modalités (stub qualify_ai non porté)", + "Exports juridiques avancés et signatures qualifiées — le PV markdown simple est inclus ; positionnement : registre de légitimité, jamais substitut légal", + "Règles d'appartenance calculées WoT/géographiques — v2 : listes nominatives des cercles ; l'enum de règle est extensible sans migration" + ], + "plan_implementation": [ + "1. SOCLE ET MOTEURS — branche v2, shell sorti d'app.vue vers layouts/, moods 4 ambiances, types/domain.ts complet, engine/ TS pur (threshold, nuanced, modeParams, settings, triage, state, impact) avec vitest portant test_threshold.py / test_qualifier.py / test_nuanced.py / test_mode_params.py — le cas Forgeron W=7224 T=120 ⇒ 94 vert AVANT tout écran (le bug latent le plus dangereux tué en premier)", + "2. PERSISTANCE ET SEEDS — data/persistence.ts (IndexedDB idb-keyval, export/import Bundle), script backend/scripts/export_seed_bundle.py, bundles duniter-g1 + atelier-du-canal, tests de non-perte bloquants (33/59 clauses, genesis 97/23/19/7224, 11 personnes, 3 mandats, 6 protocoles), gabarits templates.ts", + "3. SHELL ET ONBOARDING — header (WorkspaceSelector, Cmd+K sur l'index unique, MoodSwitcher, bouton Décider), FAB mobile, sceau 井 canonique, /creer 3 écrans + imports seeds, /donnees (export/import/reset, identité, mode démo)", + "4. LE GESTE — /decider complet (Q0 inline, 3 chips, verdict live expliqué, urgence conservatoire chaînée, dérogation asymétrique journalisée, création structurée) + / Aujourd'hui (capture sticky, Fil en sélecteurs purs, boutons inline, badge unique de dette) — mesurer le chemin nominal : 2 gestes, < 10 s", + "5. CYCLE DE VIE — /decisions (registre SectionLayout) + /decisions/[id] (timeline, panneau Cercles auditable, « Ça me concerne », objection de frontière suspensive, signal d'affluence, fenêtres avec silence-vaut-accord réversible seulement, revoyure 3 boutons, poignée « Remettre en question » chaînée, PV markdown, gravure locale honnête)", + "6. LE VOTE — /decisions/[id]/vote (consentement, binaire, nuancé avec commentaire négatif obligatoire), jauge inertielle + explication française, snapshot de corpus (W ≠ 0 à jamais), re-vote audité, adoption ⇒ application automatique (clause current / mandat créé)", + "7. LES TEXTES — /textes + document vivant (clauses, versions/diff, vue projetée, inertie CÂBLÉE, Provenance/GenesisBlock) + Pacte effectif (resolveSettings lu par le triage — la méta-gouvernance devient l'usage) + /textes/formules (FormulaEditor sur le moteur unique)", + "8. LES MANDATS — /mandats, fiche (observatoire des décisions sous mandat, rapports, jauge de confiance, révocation chaînée au protocole d'origine), wizard 3 étapes pré-rempli par R6", + "9. MÉMOIRE ET BOUCLE APPRENANTE — /decisions/memoire (stats d'autonomie, revues dues, cimetière), suggestions dans le Fil (récurrence ⇒ mandat, mandat objecté ⇒ revue, protocole inutilisé ⇒ élagage, sunset échu ⇒ caducité) — le système se simplifie lui-même", + "10. FINITION LIVRABLE — QA mobile+desktop zéro régression (480/768/1024, toucher 2.25 rem), parcours complets sur les 2 seeds ET un collectif vierge (« Adoptez votre Pacte » < 10 min, testé avec un non-théoricien), a11y clavier/contraste, README + CONTRIBUTING, npm run build zéro erreur, screenshots — livrable à toute structure" + ] + }, + "judgements": [ + [ + { + "titre": "V1 — La grammaire générative (6 primitives)", + "fidelite": 9, + "ockham": 6, + "faisabilite": 5, + "reve": 8, + "commentaire": "Lentille utilisateur final : la plus complète intellectuellement (Q0 « est-ce déjà décidé ? », cercles auditables, objection d'oubli suspensive, révocation protocolisée dès la création — tout y est), mais le membre de l'association de 12 doit traverser « tension, périmètre, corpus-formule, cheminement » et 6 sections de nav + 10 écrans. La vision admet elle-même le risque (« illisible pour le novice ») sans le résoudre structurellement : le vocabulaire concret est promis, pas conçu. Pour la citoyenne de 40 000, la délégation liquide native et le seuil expliqué en langage naturel sont du vrai pouvoir — si elle arrive jusque-là. Faisabilité : délégation liquide + Sanctuaire + local-first + 6 primitives en une refonte, c'est deux refontes. Le rêve est réel mais c'est le rêve du théoricien de la gouvernance, pas celui de la trésorière d'asso." + }, + { + "titre": "V2 — Le compagnon de toutes les décisions", + "fidelite": 9, + "ockham": 9, + "faisabilite": 7, + "reve": 8, + "commentaire": "La seule vision pensée depuis le geste quotidien, exactement l'intention (« tout le monde prend tous les jours des décisions ») : une phrase → verdict → un bouton, 10 secondes, 2 boutons inline « Ça me va / J'objecte » dans le Fil, un seul badge, digest unique. Le membre d'asso de 12 vit dans UN écran ; la citoyenne s'abonne à des tags et reçoit une ligne dans un écran qu'elle ouvre déjà. Ockham est mécanique, pas déclaratif : escalade par exception, formulaires progressifs, système qui s'élague lui-même (protocole inutilisé → suppression suggérée). La boucle apprenante (5 traces → « réclame ce mandat ») est la meilleure idée du lot : les mandats émergent de la pratique. Faisabilité correcte (3 espaces, valeur solo jour 1) mais la boucle apprenante et la détection de similarité sont sous-spécifiées. Faiblesse : le Puits/Mémoire à grande échelle est moins outillé que V3 sur le calcul d'impact — l'inclusion par tags libres est plus fragile qu'un graphe." + }, + { + "titre": "V3 — La légitimité calculée (graphe d'attaches, sceau)", + "fidelite": 8, + "ockham": 6, + "faisabilite": 4, + "reve": 7, + "commentaire": "La meilleure réponse à « identifier automatiquement les impactés » : le graphe d'attaches avec inclusion explicable (« vous êtes convoquée parce que signataire de la clause A7 ») donne à la citoyenne de 40 000 un pouvoir qu'aucun régime ne lui a donné — savoir POURQUOI elle a voix. L'escalade automatique par concernement est la subsidiarité rendue honnête. Mais pour l'asso de 12 : déclarer des empreintes, des usages, des territoires avant que le calcul fonctionne, c'est de la saisie de graphe déguisée — le démarrage à froid est reconnu et non résolu (« sceau dégradé » = l'outil avoue qu'il ne marche pas au début). Le sceau de légitimité multi-critères sera lu comme une note technocratique par un non-technicien, la vision l'admet. 8 entités dont ATTACHE générique : le noyau le plus lourd des quatre. Faisabilité la plus basse : graphe + sceau calculé + fédération fractale en une refonte est irréaliste. Le rêve est grandiose (« du foyer à la nation ») mais froid." + }, + { + "titre": "V4 — Décider juste, du trio à la toile (4 sections + Trancher)", + "fidelite": 8, + "ockham": 8, + "faisabilite": 8, + "reve": 7, + "commentaire": "La plus livrable et la seule qui pense vraiment au non-technicien à l'entrée : gabarits d'onboarding qui TRADUISENT (« qui doit être d'accord ? » au lieu de « corpus électoral »), 3 écrans, première décision « Adoptez votre Pacte » — le premier usage est le tutoriel, testé hors communauté Ğ1. Le Pacte-comme-document-voté est l'idée la plus élégante des quatre : les réglages ne sont plus une page de settings mais l'usage même du produit, et la méta-gouvernance cesse d'être décorative. Rasoir appliqué à l'outil lui-même (4 sections au lieu de 6, boîte à outils fondue dans le flux, budget 400 lignes/page) et phasage jour1/v2.1/v2.2 explicite avec diff de migration YAML : c'est la seule vision qui ressemble à un plan exécutable. Retrouvailles Ğ1 les plus précises (packs YAML vérifiés par diff, inertie décorative corrigée). En retrait sur le rêve : ton d'ingénieur, et le gradient des concernés (observateur < concerné < lié < garant de V1) y est moins riche ; l'inclusion par graphe y est plus sommaire que V3." + } + ], + [ + { + "titre": "V1 — La grammaire générative (6 primitives)", + "fidelite": 9, + "ockham": 6, + "faisabilite": 6, + "reve": 9, + "commentaire": "Théoriquement la plus rigoureuse : le pliage « mandat = décision qui crée du pouvoir, protocole = décision sur les décisions, clause = décision qui reste vraie » est réellement génératif — consentement, advice, JM, élection sans candidat, liquide, référendum sont dérivés par composition de gestes, pas énumérés. Ostrom est mobilisé avec précision (frontières calculables, emboîtement, objection d'oubli SUSPENSIVE — la contestation des frontières prime sur le fond, c'est du vrai design institutionnel). Révocation protocolisée à la création et tripwires définis AVANT exécution : de vrais mécanismes, pas des cases. Mais 6 primitives + 4 effets + gestes composables = noyau élégant pour le théoricien, lourd à incarner ; la délégation liquide native et Q0 plein-texte gonflent le jour 1. Le risque théorique propre : la grammaire fuit dans l'UI malgré l'avertissement." + }, + { + "titre": "V2 — Le compagnon quotidien (Fil/Puits/Mémoire)", + "fidelite": 8, + "ockham": 9, + "faisabilite": 7, + "reve": 7, + "commentaire": "Le rasoir comme mécanique et non comme slogan : escalade par exception (l'objection est LE geste universel de montée en cran — individuel → consultation → vote, très propre théoriquement), et surtout la boucle apprenante — mandats qui émergent de la pratique (5 traces → réclame un mandat), protocoles inutilisés élagués, système qui se simplifie lui-même : c'est la contribution la plus originale en théorie de la gouvernance adaptative. Mémoire/statistiques d'autonomie rend la démocratie mesurable. Faiblesse de fondation : l'inclusion par croisement de TAGS est un fondement de légitimité flou (qui vote = similarité sémantique ?) — contestable, contrairement aux corpus calculables de V1 ou au graphe de V3 ; et le silence-vaut-accord porte trop de poids à l'échelle. Révocation réelle mais moins outillée (pas de tripwires ni d'observatoire d'indicateurs)." + }, + { + "titre": "V3 — La légitimité calculée (graphe d'attaches + sceau)", + "fidelite": 8, + "ockham": 5, + "faisabilite": 5, + "reve": 9, + "commentaire": "La meilleure théorie de la LÉGITIMITÉ des quatre : le sceau comme enregistrement vérifiable {couverture d'information, T/W, seuil, objections, conformité} avec la couverture d'information en condition DURE (« la légèreté sans l'information n'est pas légère, elle est illégitime » — la phrase clé du lot), la dérogation asymétrique (alourdir libre, alléger consenti et inscrit), l'escalade automatique par ratio de concernement qui rend la subsidiarité honnête, et l'Observatoire où la dérive d'indicateur ROUVRE l'acte — évaluation et révocation comme moments normaux du cycle. Mais le prix est exorbitant : 8 entités, graphe d'attaches (usages, territoires, signatures) à entretenir avec cold-start reconnu (« sceau dégradé » en phase déclarative = la légitimité calculée est fausse au démarrage, précisément quand la confiance se joue), protocoles opérationnels dans le scope. La moins Ockham, la moins livrable en une refonte." + }, + { + "titre": "V4 — Décider juste (4 sections + Pacte)", + "fidelite": 9, + "ockham": 8, + "faisabilite": 8, + "reve": 7, + "commentaire": "La méta-gouvernance la plus aboutie OPÉRATIONNELLEMENT : le Pacte — les réglages du collectif SONT un document sous vote permanent, la clause d'inertie protégée par l'inertie maximale, « modifier un réglage = proposer une version = voter » — c'est la seule vision où l'auto-référence cesse d'être un discours pour devenir le chemin ordinaire du produit. Révocation réelle et concrète (poignée « revenir sur X » pré-remplie avec l'inertie du protocole d'origine sur TOUTE décision adoptée), revoyure obligatoire sur l'irréversible, R-Urgence avec ratification chaînée automatique, plancher d'inclusion non restrictible (anti-exclusion par construction). Générativité par énumération plutôt que par composition — couvre le même espace que V1 mais sans en démontrer la clôture. Phasage jour1/v2.1/v2.2 et migration YAML avec diff : la plus livrable. Rêve plus sobre — pragmatique assumé." + } + ], + [ + { + "titre": "Le Puits : une grammaire générative de la décision (6 primitives)", + "fidelite": 9, + "ockham": 6, + "faisabilite": 5, + "reve": 9, + "commentaire": "Le modèle le plus complet intellectuellement : le pliage des 4 effets (acte/norme/pouvoir/règle) unifie vraiment mandats, clauses et protocoles, et l'Épreuve du réel (tripwires définis AVANT exécution, revoyure, cimetière des révoquées) est la meilleure réponse à l'évaluation-révocation. Mais zéro phasage : délégation liquide native + Sanctuaire + sous-toiles + 10 écrans + 6 sections jour 1 = indéliverable en une refonte. Le noyau à 6 primitives est génératif sur le papier mais son vocabulaire d'interface (Le Vivant, Les Pouvoirs, L'Atelier) exige un glossaire — le risque d'abstraction que la vision liste elle-même est réel. Migration Ğ1 bien pensée (YAML + 2e org peuplée comme test permanent de marque blanche : idée à voler)." + }, + { + "titre": "Le compagnon de toutes les décisions (3 espaces, pivot Décision)", + "fidelite": 9, + "ockham": 9, + "faisabilite": 8, + "reve": 7, + "commentaire": "La plus fidèle au cœur de l'intention (les cent décisions quotidiennes) et l'audit de code le plus profond et vérifiable : divergence engine/threshold.py vs routers/votes.py, 51 vs 59 items, méta-gouvernance = PUT non protégé, ComTech jamais seedé — cette vision a réellement lu la maquette. Modèle pivot + spécialisations-vues = migration naturelle des machines à états dupliquées ; IA à 3 espaces = la plus navigable ; le système qui s'élague lui-même (mandat sans trace → revue, protocole inutilisé → suppression) est de l'Ockham génératif rare. Faiblesse produit : l'inclusion par croisement de tags libres est le maillon flou du modèle de données (similarité ≠ impact), et le pitch 'compagnon-habitude' fait moins rêver que 'la démocratie qui n'a jamais existé'. Valeur solo jour 1 = vraie parade au démarrage à froid." + }, + { + "titre": "La légitimité calculée, du foyer à la nation (graphe d'attaches + sceau)", + "fidelite": 8, + "ockham": 5, + "faisabilite": 4, + "reve": 8, + "commentaire": "Le graphe d'attaches (membre-de/titulaire-de/usager-de/résident-de/signataire-de) est la réponse la plus rigoureuse à 'identifier automatiquement les impactés', et l'escalade par concernement massif est excellente. Mais c'est la vision la moins livrable FINIE : le démarrage à froid du graphe est rédhibitoire (sans attaches déclarées, cercle d'impact faux, donc promesse centrale cassée dès le jour 1), et le sceau de légitimité exige le tracking de couverture d'information — donc toute l'infra de notifications — comme prérequis du concept central. 8 entités + escalier de gravité + protocoles opérationnels + fédération fractale = la machinerie la plus lourde, en contradiction avec son propre discours Ockham. Le sceau reste une idée forte à greffer en version dégradée (niveau de preuve honnête)." + }, + { + "titre": "Décider juste, du trio à la toile (4 sections + Trancher, Pacte)", + "fidelite": 8, + "ockham": 8, + "faisabilite": 9, + "reve": 7, + "commentaire": "La seule vision qui livre un OUTIL FINI : phasage jour1/v2.1/v2.2 explicite, diff automatisé seed.py→packs YAML avant suppression, unification de la formule AVANT tout écran de vote (le bug latent le plus dangereux, correctement identifié), contrat DataAdapter avec tests de conformité, budget 400 lignes/page contre le syndrome protocols/index.vue, étiquetage honnête de la crypto. Le Pacte — les réglages du collectif EN document sous vote permanent, clause d'inertie protégée par l'inertie maximale — est la meilleure idée produit des quatre : méta-gouvernance enfin concrète ET tutoriel d'onboarding ('Adoptez votre Pacte'). Rasoir appliqué à la nav elle-même (4 sections, Sanctuaire = badge-propriété, toolbox dissoute). Retraits : l'épreuve du réel est plus mince que la vision 1 (revoyure présente mais sans tripwires/indicateurs), le graphe d'impact via Ressources demande quand même une déclaration initiale, et le pitch est le moins lyrique." + } + ] + ] + }, + "workflowProgress": [ + { + "type": "workflow_phase", + "index": 1, + "title": "Comprendre" + }, + { + "type": "workflow_phase", + "index": 2, + "title": "Visions" + }, + { + "type": "workflow_phase", + "index": 3, + "title": "Jugement" + }, + { + "type": "workflow_phase", + "index": 4, + "title": "Synthèse" + }, + { + "type": "workflow_agent", + "index": 1, + "label": "lire:frontend-ux", + "phaseIndex": 1, + "phaseTitle": "Comprendre", + "agentId": "ada99a1d33a1d136c", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786408018042, + "queuedAt": 1786408018014, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "Le frontend (16 pages, ~21 600 lignes, pas de dossier layou…", + "promptPreview": "Tu cartographies le frontend d'une maquette Nuxt 4 appelée libreDecision (outil de gouvernance collective, UI en français). Racine: /home/yvv/Documents/PROD/DEV/libreDecision/frontend.\nLis en profondeur: app/pages/ (toutes les pages), app/components/ (survole chaque dossier: auth, common, decisions, documents, mandates, protocols, sanctuary, toolbox, votes), app/stores/ (les 8 stores Pinia), app/c…", + "lastProgressAt": 1786408825768, + "tokens": 157125, + "toolCalls": 34, + "durationMs": 807725, + "resultPreview": "{\"summary\":\"Le frontend (16 pages, ~21 600 lignes, pas de dossier layouts/ : tout le shell vit dans app.vue, 740 l.) est une SPA Nuxt 4 avec sidebar 6 sections : Décisions, Documents, Mandats, Protocoles, Outils, Sanctuaire. Le header porte le WorkspaceSelector (organisations, header X-Organization injecté par useApi, refetch des 4 stores au changement) et le MoodSwitcher (4 ambiances via @yvv/nux…" + }, + { + "type": "workflow_agent", + "index": 2, + "label": "lire:backend-domaine", + "phaseIndex": 1, + "phaseTitle": "Comprendre", + "agentId": "ad1ed3d42887a0f29", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786408018045, + "queuedAt": 1786408018014, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "Backend FastAPI async (SQLAlchemy 2, SQLite/PG, Alembic) or…", + "promptPreview": "Tu cartographies le backend FastAPI d'une maquette appelée libreDecision (gouvernance collective). Racine: /home/yvv/Documents/PROD/DEV/libreDecision/backend.\nLis en profondeur: app/models/ (14 tables: decision, document, group, mandate, organization, protocol, qualification, sanctuary, user, vote, cache), app/routers/, app/engine/ (mode_params, nuanced_vote, qualifier, smith_threshold, techcomm_t…", + "lastProgressAt": 1786408556146, + "tokens": 119188, + "toolCalls": 23, + "durationMs": 538100, + "resultPreview": "{\"summary\":\"Backend FastAPI async (SQLAlchemy 2, SQLite/PG, Alembic) organisé en 4 couches : models (19 tables), engine (calcul pur, testé), services (orchestration), routers (12 modules REST + WebSocket).\\n\\nMODÈLE DE DOMAINE. L'identité est un DuniterIdentity (adresse SS58, wot_status, flags is_smith/is_techcomm) avec sessions bearer hashées. Le workspace est l'Organization (slug, org_type, is_t…" + }, + { + "type": "workflow_agent", + "index": 3, + "label": "lire:contenus-seeds", + "phaseIndex": 1, + "phaseTitle": "Comprendre", + "agentId": "ac4dec13083b76353", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786408018046, + "queuedAt": 1786408018014, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "La maquette libreDecision embarque un corpus Ğ1 réel et den…", + "promptPreview": "Tu inventories les CONTENUS et CONTEXTES réels d'une maquette de gouvernance collective libreDecision, pour la communauté Duniter/Ğ1 (monnaie libre). Racine: /home/yvv/Documents/PROD/DEV/libreDecision.\nLis: backend/seed.py EN ENTIER (licence Ğ1 v0.3.0, Engagement Certification ~33 items, Forgeron ~51 items, Runtime Upgrade, FormulaConfigs, VotingProtocols, personnes: 1000i100, Natha, Pini, Yvv, el…", + "lastProgressAt": 1786408459467, + "tokens": 90776, + "toolCalls": 11, + "durationMs": 441420, + "resultPreview": "{\"summary\":\"La maquette libreDecision embarque un corpus Ğ1 réel et dense, entièrement en français, réparti entre backend/seed.py (source de vérité des contenus), research_duniter_forum.md (matière brute forum) et docs/content (8 docs user + 11 docs dev).\\n\\nLe seed crée : 2 organisations (« Duniter G1 » slug duniter-g1, « Axiom Team » slug axiom-team) ; 7 FormulaConfigs nommées (« Standard Licenc…" + }, + { + "type": "workflow_agent", + "index": 4, + "label": "lire:design-system", + "phaseIndex": 1, + "phaseTitle": "Comprendre", + "agentId": "a15c11f99a48a6141", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786408018047, + "queuedAt": 1786408018014, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "Le design system de libreDecision (frontend Nuxt 4, SSR off…", + "promptPreview": "Tu cartographies le SYSTÈME DE DESIGN d'une app Nuxt 4 libreDecision. Racine: /home/yvv/Documents/PROD/DEV/libreDecision/frontend.\nLis: app/assets/css/ (moods.css et tout autre css), app.vue (sceau hexagramme 井 .app-seal), nuxt.config.ts, package.json (note la dépendance @yvv/nuxt-base — trouve-la dans node_modules/@yvv/nuxt-base et lis son contenu: layer Nuxt avec mécanisme d'ambiances useMood), …", + "lastProgressAt": 1786408292265, + "tokens": 45970, + "toolCalls": 23, + "durationMs": 274218, + "resultPreview": "{\"summary\":\"Le design system de libreDecision (frontend Nuxt 4, SSR off, port 3002) repose sur un seul fichier de tokens, app/assets/css/moods.css, et un mécanisme d'ambiances externalisé dans le layer @yvv/nuxt-base (dépendance git v0.1.0, node_modules/@yvv/nuxt-base — 4 fichiers seulement : nuxt.config.ts vide, package.json, README, app/composables/useMood.ts).\\n\\nQuatre moods appliqués via clas…" + }, + { + "type": "workflow_agent", + "index": 5, + "label": "vision:theoricien", + "phaseIndex": 2, + "phaseTitle": "Visions", + "agentId": "aa860b5363b4aceb9", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786408825804, + "queuedAt": 1786408825778, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "libreDecision v2 — Le Puits : une grammaire générative de l…", + "promptPreview": "INTENTION DU COMMANDITAIRE (Yvv) pour libreDecision v2 — refonte de zéro:\n\"Le propos est que n'importe quel collectif, du plus structuré au plus informel, puisse aller le plus loin possible dans l'auto-décision de ses membres. Pareil pour l'échelle: d'une organisation à 3 personnes à une population entière. L'outil doit donner la capacité d'une démocratie qui n'a jamais existé.\nTout le monde prend…", + "lastProgressAt": 1786409173728, + "tokens": 58319, + "toolCalls": 2, + "durationMs": 347924, + "resultPreview": "{\"titre\":\"libreDecision v2 — Le Puits : une grammaire générative de la décision (6 primitives pour toutes les gouvernances)\",\"pitch\":\"On change de ville, on ne change pas de puits (hexagramme 48) : libreDecision fait de chaque décision — du choix d'un café entre trois amis au budget d'investissement d'une population — un objet vivant qui sait qui il engage, comment il doit se prendre, et quand il …" + }, + { + "type": "workflow_agent", + "index": 6, + "label": "vision:quotidien", + "phaseIndex": 2, + "phaseTitle": "Visions", + "agentId": "afd47ac8f9951a1f1", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786408825805, + "queuedAt": 1786408825778, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "Trois espaces, pas seize pages. Le shell sort enfin de app.…", + "promptPreview": "INTENTION DU COMMANDITAIRE (Yvv) pour libreDecision v2 — refonte de zéro:\n\"Le propos est que n'importe quel collectif, du plus structuré au plus informel, puisse aller le plus loin possible dans l'auto-décision de ses membres. Pareil pour l'échelle: d'une organisation à 3 personnes à une population entière. L'outil doit donner la capacité d'une démocratie qui n'a jamais existé.\nTout le monde prend…", + "lastProgressAt": 1786409085102, + "tokens": 44329, + "toolCalls": 1, + "durationMs": 259297, + "resultPreview": "{\"architecture_info\":\"Trois espaces, pas seize pages. Le shell sort enfin de app.vue vers layouts/ ; le header garde le sélecteur de collectif (WorkspaceSelector + X-Organization conservés) et le MoodSwitcher ; recherche omniprésente Cmd+K ; chaque décision a une URL permanente de la capture à la révocation.\\n\\n1. AUJOURD'HUI (/) — l'écran du quotidien, le seul qu'on ouvre chaque jour : la barre d…" + }, + { + "type": "workflow_agent", + "index": 7, + "label": "vision:echelle", + "phaseIndex": 2, + "phaseTitle": "Visions", + "agentId": "a33a9df9d0474ea81", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786408825806, + "queuedAt": 1786408825779, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "Le Puits — libreDecision v2 : la légitimité calculée, du fo…", + "promptPreview": "INTENTION DU COMMANDITAIRE (Yvv) pour libreDecision v2 — refonte de zéro:\n\"Le propos est que n'importe quel collectif, du plus structuré au plus informel, puisse aller le plus loin possible dans l'auto-décision de ses membres. Pareil pour l'échelle: d'une organisation à 3 personnes à une population entière. L'outil doit donner la capacité d'une démocratie qui n'a jamais existé.\nTout le monde prend…", + "lastProgressAt": 1786409074613, + "tokens": 44314, + "toolCalls": 1, + "durationMs": 248807, + "resultPreview": "{\"titre\":\"Le Puits — libreDecision v2 : la légitimité calculée, du foyer à la nation\",\"pitch\":\"Chaque décision porte en elle la carte exacte de ceux qu'elle touche : libreDecision la calcule, convoque le plus petit cercle suffisant, et délivre un sceau de légitimité vérifiable — avec les mêmes règles pour un foyer de 3 personnes et une toile de confiance de plusieurs millions. Plus besoin de devin…" + }, + { + "type": "workflow_agent", + "index": 8, + "label": "vision:produit", + "phaseIndex": 2, + "phaseTitle": "Visions", + "agentId": "aeb7195e1e07686eb", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786408825806, + "queuedAt": 1786408825780, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "**Navigation définitive — 4 sections + 1 geste.** Le shell …", + "promptPreview": "INTENTION DU COMMANDITAIRE (Yvv) pour libreDecision v2 — refonte de zéro:\n\"Le propos est que n'importe quel collectif, du plus structuré au plus informel, puisse aller le plus loin possible dans l'auto-décision de ses membres. Pareil pour l'échelle: d'une organisation à 3 personnes à une population entière. L'outil doit donner la capacité d'une démocratie qui n'a jamais existé.\nTout le monde prend…", + "lastProgressAt": 1786409105879, + "tokens": 50736, + "toolCalls": 2, + "durationMs": 280073, + "resultPreview": "{\"architecture_info\":\"**Navigation définitive — 4 sections + 1 geste.** Le shell (enfin sorti de app.vue vers layouts/default.vue) porte : le sélecteur de collectif (WorkspaceSelector conservé, X-Organization), le MoodSwitcher, et un bouton « Trancher » toujours visible (header desktop, FAB mobile). Sidebar 4 entrées :\\n\\n1. **Fil** (`/`) — l'accueil. Ce qui M'attend d'abord (votes ouverts avec ja…" + }, + { + "type": "workflow_agent", + "index": 9, + "label": "juge:1", + "phaseIndex": 3, + "phaseTitle": "Jugement", + "agentId": "a55185c023a24aaf3", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786409173759, + "queuedAt": 1786409173732, + "attempt": 1, + "lastToolName": "StructuredOutput", + "promptPreview": "INTENTION DU COMMANDITAIRE (Yvv) pour libreDecision v2 — refonte de zéro:\n\"Le propos est que n'importe quel collectif, du plus structuré au plus informel, puisse aller le plus loin possible dans l'auto-décision de ses membres. Pareil pour l'échelle: d'une organisation à 3 personnes à une population entière. L'outil doit donner la capacité d'une démocratie qui n'a jamais existé.\nTout le monde prend…", + "lastProgressAt": 1786409303610, + "tokens": 50418, + "toolCalls": 1, + "durationMs": 129851, + "resultPreview": "{\"scores\":[{\"titre\":\"V1 — La grammaire générative (6 primitives)\",\"fidelite\":9,\"ockham\":6,\"faisabilite\":5,\"reve\":8,\"commentaire\":\"Lentille utilisateur final : la plus complète intellectuellement (Q0 « est-ce déjà décidé ? », cercles auditables, objection d'oubli suspensive, révocation protocolisée dès la création — tout y est), mais le membre de l'association de 12 doit traverser « tension, périmè…" + }, + { + "type": "workflow_agent", + "index": 10, + "label": "juge:2", + "phaseIndex": 3, + "phaseTitle": "Jugement", + "agentId": "adb229f6b577f0f12", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786409173761, + "queuedAt": 1786409173733, + "attempt": 1, + "lastToolName": "StructuredOutput", + "promptPreview": "INTENTION DU COMMANDITAIRE (Yvv) pour libreDecision v2 — refonte de zéro:\n\"Le propos est que n'importe quel collectif, du plus structuré au plus informel, puisse aller le plus loin possible dans l'auto-décision de ses membres. Pareil pour l'échelle: d'une organisation à 3 personnes à une population entière. L'outil doit donner la capacité d'une démocratie qui n'a jamais existé.\nTout le monde prend…", + "lastProgressAt": 1786409373213, + "tokens": 52331, + "toolCalls": 3, + "durationMs": 199452, + "resultPreview": "{\"scores\":[{\"titre\":\"V1 — La grammaire générative (6 primitives)\",\"fidelite\":9,\"ockham\":6,\"faisabilite\":6,\"reve\":9,\"commentaire\":\"Théoriquement la plus rigoureuse : le pliage « mandat = décision qui crée du pouvoir, protocole = décision sur les décisions, clause = décision qui reste vraie » est réellement génératif — consentement, advice, JM, élection sans candidat, liquide, référendum sont dérivé…" + }, + { + "type": "workflow_agent", + "index": 11, + "label": "juge:3", + "phaseIndex": 3, + "phaseTitle": "Jugement", + "agentId": "ae92ec5231944066b", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786409173762, + "queuedAt": 1786409173733, + "attempt": 1, + "lastToolName": "StructuredOutput", + "promptPreview": "INTENTION DU COMMANDITAIRE (Yvv) pour libreDecision v2 — refonte de zéro:\n\"Le propos est que n'importe quel collectif, du plus structuré au plus informel, puisse aller le plus loin possible dans l'auto-décision de ses membres. Pareil pour l'échelle: d'une organisation à 3 personnes à une population entière. L'outil doit donner la capacité d'une démocratie qui n'a jamais existé.\nTout le monde prend…", + "lastProgressAt": 1786409390632, + "tokens": 52270, + "toolCalls": 4, + "durationMs": 216870, + "resultPreview": "{\"scores\":[{\"titre\":\"Le Puits : une grammaire générative de la décision (6 primitives)\",\"fidelite\":9,\"ockham\":6,\"faisabilite\":5,\"reve\":9,\"commentaire\":\"Le modèle le plus complet intellectuellement : le pliage des 4 effets (acte/norme/pouvoir/règle) unifie vraiment mandats, clauses et protocoles, et l'Épreuve du réel (tripwires définis AVANT exécution, revoyure, cimetière des révoquées) est la meil…" + }, + { + "type": "workflow_agent", + "index": 12, + "label": "synthese:blueprint", + "phaseIndex": 4, + "phaseTitle": "Synthèse", + "agentId": "a23137e9888693ced", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786409390664, + "queuedAt": 1786409390641, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "libreDecision", + "promptPreview": "INTENTION DU COMMANDITAIRE (Yvv) pour libreDecision v2 — refonte de zéro:\n\"Le propos est que n'importe quel collectif, du plus structuré au plus informel, puisse aller le plus loin possible dans l'auto-décision de ses membres. Pareil pour l'échelle: d'une organisation à 3 personnes à une population entière. L'outil doit donner la capacité d'une démocratie qui n'a jamais existé.\nTout le monde prend…", + "lastProgressAt": 1786410285801, + "tokens": 90923, + "toolCalls": 12, + "durationMs": 895137, + "resultPreview": "{\"nom_produit\":\"libreDecision\",\"manifeste\":\"Tout le monde décide, tout le temps. libreDecision attrape chaque décision en une phrase et répond en dix secondes : décide seul et trace-le, décide sous mandat, demande leur avis, ou porte-la au collectif — avec le bon cercle, la bonne modalité, le bon seuil, jamais plus lourd que l'enjeu. Les personnes touchées sont incluses par calcul, chacune voit po…" + } + ], + "totalTokens": 856699, + "totalToolCalls": 117 +} \ No newline at end of file diff --git a/docs/dev/corpus-intention.json b/docs/dev/corpus-intention.json new file mode 100644 index 0000000..9387c50 --- /dev/null +++ b/docs/dev/corpus-intention.json @@ -0,0 +1,525 @@ +{ + "summary": "Lire les 2 livres de Yvv + analyser SejeteralO, synthétiser le corpus d intention pour la conception v2", + "agentCount": 8, + "logs": [ + "Lectures terminées — synthèse du corpus" + ], + "result": { + "corpus": { + "doctrine": "Aucune démocratie n'a jamais existé. Nous vivons sous une oligarchie électorale : le pouvoir aux mains de quelques-uns, validés par l'élection. Ses piliers convenus omettent la clé de voûte : la décision, pouvoir le plus générique et le plus en amont de tous les autres. Privé de décision, une personne n'a aucun pouvoir, un peuple non plus. On ne délègue pas, on abandonne ; « a voté » rime avec « a renoncé ». Le rêve est donc de créer une première démocratie — sans adjectif — durable, fractale, désirable : un peuple qui prend en charge et maîtrise la décision.\n\nL'issue n'est pas la révolution : dans le référentiel du rapport de forces avec l'État, les peuples perdent toujours. Toute impasse est un appel à une nouvelle géométrie : changer de référentiel. L'asservissement fonctionne au couple peur-culpabilisation ; l'émancipation exige le couple symétrique plaisir-rêve — le rêve de jour, tenace, distinct de l'espoir et de l'illusion. L'utopie n'est pas l'atopie mais le projet qui n'a pas encore trouvé son lieu dans l'espace-temps. Le peuple adulte est un ğ(x) politique : non encore observé, mais dont on ne peut dire qu'il n'existe pas. Aucun sauveur : mon nom est personne — on juge les idées, jamais leur auteur.\n\nLe peuple adulte conçoit et bâtit sa capacité à la décision. Il ne délègue plus, il mandate. Il n'est pas consulté, il est instruit. Il ne cède pas un consentement, il négocie — chaque virage. Son méta-pouvoir premier : définir et contrôler en amont qui décide de quoi — périmètres et modalités sont eux-mêmes objets de décision. Il contrôle le pouvoir qu'il mandate et ne se contrôle jamais lui-même — la confiance exclut le contrôle, et réciproquement ; elle est à géométrie variable, accordée progressivement sur le vécu. Chaque décision emporte une décision connexe d'allocation de ressources, assumée au contact direct des impacts. Les représentants sont ambassadeurs et négociateurs, jamais décideurs ; les mandats sont spécifiques, sous contrôle, révocables, détachés de tout statut. L'élection, mythe et meilleure ennemie de la démocratie, retourne dans la boîte à outils pour les seules problématiques d'échelle et de mandats. On évalue des choses ; les personnes, jamais : alerte rouge.\n\nLes périmètres se définissent sur trois vecteurs : géographique — commune pivot, bassin de vie, couches d'oignon, par défaut la plus petite échelle, la montée d'échelle étant elle-même une décision ; thématique — cartographie vectorielle des sujets, liaisons et interdépendances, symptômes distingués des causes ; enjeux — court et long terme, urgences, volumes. Pour chaque sujet, deux cercles : les concernés en premier lieu, puis ceux qui se sentent concernés.\n\nLe clivage binaire tue la réflexion, le parti politique tue la politique, le référendum est un tir à la corde. Il faut raisonner curseur : toute question devient une table de mixage — grandes lignes graduées, boutons de réglage, volumes inclus — que l'on déplace, évalue, ajuste, recommence. À grand volume, cela devient la décision incrémentielle : dossier découpé finement, chacun navigue dans les points qui le concernent, pondère ses préférences, positionne ses curseurs, valide les agrégations d'un clic ; la cartographie de clôture reste visible de tous, sous les feux de la rampe populaire, et les décisions stratégiques subissent le joug de la sagesse populaire.\n\nLe numérique est aujourd'hui la condition de possibilité d'une démocratie — décision distribuée et structurée en très grand volume, représentations visuelles manipulables par tout un peuple — s'il est libre, chiffré, décentralisé, maîtrisé par le peuple. Mais l'outil représente, agrège et éclaire : il ne décide jamais ; l'IA n'y sert qu'à la recherche des biais dans les traitements décisionnels, et un degré d'inefficacité collectivement choisi est le prix pour échapper à la décision algorithmique. Plus le numérique prend de la place, plus il faut en donner à la relation humaine : passerelles pour les sans-écran, intermédiation humaine, lieux physiques de brassage — pour faire société, le seul moyen est de physiquement se rencontrer.\n\nL'adhésion tient à quatre conditions : mon raisonnement réellement pris en compte d'une manière que chacun peut saisir, une volonté collective évolutive, ma contribution possible à son évolution, une décision observable et rectifiable. Le contrat social est un ouvrage vivant, minimaliste sous rasoir d'Ockham, écrit au fil de l'eau : définition incrémentielle de la décision, des droits et des usages. Plutôt que préfigurer, l'observatoire des décisions consigne comment elles sont prises dans les faits, puis cristallise des protocoles de quelques phrases quand c'est mûr. Protocoles plutôt que légiférer : prévoir la façon de traiter, jamais l'issue.\n\nCette démocratie naît en écosystème dans la nation existante — succession, pas sécession ; elle institue des symétries face aux institutions sourdes ; elle ne se réclame pas, elle se déclare ; elle ne conquiert pas, elle se rend désirable et s'étend par grappes en liaison permanente ; elle ne fait pas la quête. Ses boussoles sont des lois dynamiques : Autonomie, Équilibre, Liaison. Son économie jumelle est celle du don mesuré en DUs — le DU n'a pas de valeur, il mesure les valeurs — la création monétaire étant le premier des pouvoirs et un commun. libreDecision est le troisième axe du triptyque « logiciel libre ; monnaie libre ; décision collective » ; son mandat fondateur : concevoir et développer un système visuel de décisions et d'acquisition collective des expériences.", + "vocabulaire": [ + { + "terme": "démocratie (sans adjectif)", + "sens": "Le régime qui n'a jamais existé : un peuple qui prend en charge et maîtrise la décision. Ne jamais accoler d'adjectif restrictif (participative, directe...) dans l'UI." + }, + { + "terme": "oligarchie électorale", + "sens": "Nom exact du régime en vigueur : le pouvoir aux mains de quelques-uns, validés par l'élection ; jamais démocratique." + }, + { + "terme": "peuple adulte", + "sens": "Peuple qui conçoit et bâtit sa capacité à la décision : il ne délègue plus, il mandate ; il n'est pas consulté, il est instruit ; il ne cède pas un consentement, il négocie chaque virage." + }, + { + "terme": "mandater / mandat", + "sens": "Substitut de la délégation : confier une mission spécifique, à périmètre défini, sous contrôle, révocable, détachée de tout statut. Le mot 'déléguer' est banni de l'UI." + }, + { + "terme": "instruire / instruction", + "sens": "Phase où les options sont présentées, qualifiées, évaluées et illustrées par des compétences pluridisciplinaires avant tout arbitrage — remplace 'consulter'." + }, + { + "terme": "qui décide de quoi", + "sens": "Méta-pouvoir premier : définir et contrôler en amont les périmètres et modalités de décision. Nom naturel de la couche constituante de l'outil." + }, + { + "terme": "périmètre", + "sens": "Espace de décision défini sur trois vecteurs : géographique/topologique, thématique, échelle des enjeux et priorités (les volumes étant des réglages techniques)." + }, + { + "terme": "bassin de vie", + "sens": "Périmètre vécu de première proximité (commune pivot, vallée, quartiers, rayon de vélo) ; échelle d'amorce par défaut." + }, + { + "terme": "couches d'oignon", + "sens": "Construction des échelles supérieures par agrégation depuis la petite échelle — jamais par découpage descendant." + }, + { + "terme": "raisonner curseur / table de mixage", + "sens": "Structurer toute question en grandes lignes graduées et boutons de réglage multi-critères (volumes inclus) ; cycle déplacer-évaluer-ajuster-recommencer. Anti-binaire." + }, + { + "terme": "décision incrémentielle", + "sens": "Mode de décision de grand volume — ni majoritaire, ni quadratique, ni consensuel : micro-positionnements pondérés agrégés progressivement, chacun naviguant dans les points qui le concernent." + }, + { + "terme": "cartographie de clôture", + "sens": "Restitution finale visible de tous : positions soutenues, hiérarchie des enjeux, niveaux d'urgence (modèle Taïwan)." + }, + { + "terme": "cartographie thématique vectorielle", + "sens": "Carte des sujets avec liaisons et interdépendances (jamais de silos) ; qualifier un sujet plutôt que le titrer ; distinguer symptômes et causes." + }, + { + "terme": "clivage binaire", + "sens": "Fléau qui tue la réflexion (pour/contre, tir à la corde) ; à distinguer des polarités réelles qu'il faut concilier. Proscrit par défaut dans l'outil." + }, + { + "terme": "vote (vs élection)", + "sens": "Le vote porte sur des choses (réglages, propositions) ; l'élection est un outil relégué aux seules problématiques d'échelle et de mandats." + }, + { + "terme": "feux de la rampe", + "sens": "Transparence radicale : décisions, options, arbitrages bénéfices/risques et actes des mandatés à la vue et au su de tous ; mémoire vive documentée." + }, + { + "terme": "sagesse populaire", + "sens": "Capacité du plus grand nombre à arbitrer bénéfices et risques ; toute décision stratégique en subit le joug." + }, + { + "terme": "confiance / contrôle (oxymore)", + "sens": "« La confiance exclut le contrôle, et réciproquement » : on contrôle le pouvoir mandaté, jamais les membres entre eux." + }, + { + "terme": "confiance à géométrie variable", + "sens": "Habilitations progressives, contextuelles, fondées sur le vécu, jamais acquises a priori — pas de score global ni définitif." + }, + { + "terme": "changement de référentiel", + "sens": "Méthode de sortie d'impasse : reformuler le problème dans une autre géométrie ; un référentiel n'est pas le monde." + }, + { + "terme": "ğ(x)", + "sens": "Proposition non encore observée dont on ne peut dire qu'elle n'existe pas (clin d'œil à Gödel) ; statut du projet lui-même." + }, + { + "terme": "Autonomie – Équilibre – Liaison", + "sens": "Devise recyclée en lois dynamiques (Liberté→Autonomie, Égalité→Équilibre, Fraternité→Liaison) ; boussoles de l'outil." + }, + { + "terme": "concernés en premier lieu / se sentant concernés", + "sens": "Deux cercles de légitimité par sujet, avec rôles ou poids différents dans le processus." + }, + { + "terme": "symétrie / institution symétrique", + "sens": "Institution miroir que le peuple institue et habite quand l'institution établie est sourde ; son poids = le volume des personnes qui l'habitent." + }, + { + "terme": "RIC permanent", + "sens": "Réappropriation interne du référendum : structurer soi-même son fonctionnement en continu, sans le clivage binaire du RIC institutionnel." + }, + { + "terme": "engagements fermes / seuil critique", + "sens": "Recueillir des engagements et ne déclencher l'action collective qu'au quota atteint ; sinon partie remise." + }, + { + "terme": "observatoire des décisions", + "sens": "Consigner comment les décisions sont réellement prises (qui, comment, quand) avant de normer ; puis cristalliser en protocoles de quelques phrases quand c'est mûr." + }, + { + "terme": "définition incrémentielle", + "sens": "La décision, les droits et les usages ne sont pas préfigurés : définis au fil de l'eau, versionnés, minimalistes (rasoir d'Ockham)." + }, + { + "terme": "protocole (vs légiférer)", + "sens": "Prévoir la façon de traiter les cas plutôt que d'édicter des règles et juger les issues ; appliqué avec le consentement des parties." + }, + { + "terme": "décision algorithmique", + "sens": "Repoussoir absolu : destin d'une société qui fait de l'efficacité son leitmotiv. L'outil représente, agrège, éclaire — ne décide jamais." + }, + { + "terme": "brassage", + "sens": "Fonction des lieux physiques adossés aux protocoles numériques : brasser groupes, idées, points de vue, résultats d'expériences." + }, + { + "terme": "épine dorsale numérique", + "sens": "Rôle de l'outil : système nerveux (volume, granularité, représentation, chiffrement, suivi) — jamais une totalité ; passerelles hors-écran obligatoires." + }, + { + "terme": "contrainte / calculé", + "sens": "Tags distinguant ce qui est imposé par le cadre, ce qui est voté, et ce qui découle du calcul (leçon SejeteralO à généraliser)." + }, + { + "terme": "cristallisation / publication", + "sens": "Geste distinct de l'agrégation vivante : figer un état daté (courbe publiée, protocole mûr) qui devient référence du cycle suivant." + }, + { + "terme": "DU / don mesuré / degré de gratitude", + "sens": "Le DU n'a pas de valeur, il mesure les valeurs ; le don se mesure en affectant des DUs au geste vécu ; vocabulaire économique : donner/recevoir/mesurer, jamais vendre/acheter/payer." + }, + { + "terme": "coefficients relatifs", + "sens": "Formules consenties dans l'échange (ancienneté, volume, solde net, pénibilité) qui rétablissent des équilibres sans légiférer ni juger." + }, + { + "terme": "mobiz / sherpiz", + "sens": "Personnes impliquées dans les productions collectives (jalon 15 %) / porteurs tournants dépositaires de la vision et de la mémoire (3,5 %), sans statut." + }, + { + "terme": "boucles bouclantes", + "sens": "Les boucles d'échange doivent boucler et se croiser ; les dessiner au fur et à mesure pour que chacun identifie les trous et y pourvoie." + }, + { + "terme": "@personne", + "sens": "Pseudonymat anti-appropriation : on débat du contenu, jamais de l'auteur ; neutralise l'ad hominem et la figure du sauveur." + } + ], + "principes_produit": [ + "Jamais de oui/non par défaut : toute question est décomposée en curseurs multi-critères gradués (volumes inclus) avec cycle déplacer-évaluer-ajuster-recommencer ; le binaire n'existe que comme cas dégénéré explicitement choisi par le périmètre.", + "L'outil représente, agrège et éclaire — il ne décide jamais : aucune recommandation prescriptive, aucun classement automatique d'options ; l'IA est strictement cantonnée à la recherche des biais dans les traitements décisionnels.", + "Couche méta constituante (« qui décide de quoi ») : périmètres, modalités, seuils, agrégateurs, calendriers sont eux-mêmes objets de décision du collectif — rien de figé dans le code qui devrait être une décision.", + "Anti-préfiguration : définition incrémentielle de la décision, des droits et des usages, au fil de l'eau, sous rasoir d'Ockham ; l'outil observe avant de normer (observatoire des décisions) et propose des banques de questions d'amorce, jamais de templates rigides ni de workflow imposé.", + "Subsidiarité par défaut : la plus petite échelle est l'échelle par défaut de toute décision ; la montée d'échelle est une décision explicite, tracée, réversible (couches d'oignon).", + "Instruction avant arbitrage : le cycle de vie de toute décision comporte une phase d'instruction — options présentées, qualifiées (qualitatif + quantitatif), évaluées et illustrées ; l'utilisateur est instruit, pas consulté.", + "Traçabilité de la prise en compte : chaque raisonnement individuel est visiblement intégré « d'une manière que chacun peut saisir » ; toute décision est évolutive, observable, rectifiable — le consentement n'est jamais cédé une fois pour toutes (vote révisable, renégociation à chaque virage).", + "On évalue des choses, jamais des personnes (alerte rouge) : aucun score, note, réputation, badge ou classement de personnes, nulle part dans le produit.", + "Le contrôle porte exclusivement sur le pouvoir mandaté (reddition de comptes, mesure, révocation) ; aucun mécanisme de contrôle des membres entre eux — le contrôle du peuple par le peuple sonnerait le glas.", + "Dépersonnalisation des propositions : pseudonymat type @personne pour le débat (on juge les idées) ; l'identité forte (toile de confiance) ne sert qu'à garantir l'unicité du vote — deux identités découplées.", + "Transparence par défaut, pas de huis clos : cadre, faisceau des positions, agrégats, cartographie de clôture et actes des mandatés visibles de tous ; la règle d'agrégation est nommée, expliquée en langage courant et elle-même décidable.", + "Faisabilité encodée dans le moteur, pas dans la validation : séparation cadre imposé / forme votée / variables déduites par équation de réalité — toute option exprimable est valide par construction, zéro message d'erreur punitif.", + "Impact personnalisé permanent : « qu'est-ce que ça change pour moi » calculé sur les données réelles du registre des affectés avant toute soumission ; le statu quo est toujours affiché comme baseline comparable.", + "Chaque décision emporte sa décision connexe d'allocation de ressources et le suivi de ses impacts dans la durée — on assume au contact direct des impacts.", + "Souveraineté technique non négociable : logiciel libre, chiffrement, architecture décentralisée/distribuée, sobriété (référence raspberry pi), zéro dépendance bigtech, aucune donnée personnelle centralisée, espace intime intouchable par utilisateur.", + "Passerelles hors-écran et intermédiation humaine obligatoires : l'outil est une épine dorsale articulée à des lieux physiques de brassage — « pour faire société, le seul moyen est de physiquement se rencontrer ».", + "Multi-groupes natif en grappes : chaque groupe porte sa finalité, ses protocoles, sa carte ; coopérations ponctuelles, comparaison des effets et scission féconde outillées ; jamais de fédération représentative ni de plateforme centrale ; alerte équipe > 12.", + "Vocabulaire de l'auteur dans toute l'UI : démocratie sans adjectif, votes ≠ élections, mandater/instruire/négocier, périmètres, curseurs, décision incrémentielle, cristallisation, Autonomie–Équilibre–Liaison ; laïcité structurelle (aucune variable religieuse dans les processus).", + "Mémoire vive : consignation des décisions et des mandats, liaison engagements/discours/actes des mandatés, mémoire collective portée par l'outil pour rendre possible la rotation des rôles sans perte.", + "Mode expérimental et droit à l'erreur : tester, mesurer, réviser (« ne pas détenir de vérité, plutôt mesurer ») ; réglage explicite du degré d'inefficacité collectivement accepté — l'outil n'optimise jamais la vitesse de décision." + ], + "mecanismes": [ + { + "nom": "Méta-décision constituante", + "description": "Avant toute décision de fond, le collectif décide qui décide de quoi et comment : périmètres, modalités, seuils, agrégateurs, qui évalue et qui assume. La constitution du groupe est un document vivant, minimaliste, écrit au fil de l'eau.", + "source": "Mon nom est personne — « Qui décide de quoi ? » + conclusion (constituante, rasoir d'Ockham)" + }, + { + "nom": "Périmètres à trois vecteurs + subsidiarité", + "description": "Tout périmètre se définit par : géographique/topologique (commune pivot, bassin de vie, agrégation en couches d'oignon), thématique (carte vectorielle avec liaisons et interdépendances, symptômes/causes), enjeux (court/long terme, urgence, volumes). Défaut = plus petite échelle ; la montée d'échelle est une décision explicite.", + "source": "Mon nom est personne — « Qui décide de quoi ? »" + }, + { + "nom": "Deux cercles de concernés", + "description": "Par sujet, distinguer les concernés en premier lieu et ceux qui se sentent concernés, avec rôles ou poids paramétrables par périmètre — positionner l'enjeu au plus juste allège la réflexion.", + "source": "Mon nom est personne — « Qui décide de quoi ? » / « Pouvoirs régaliens »" + }, + { + "nom": "Instruction populaire préalable", + "description": "Phase obligatoire du cycle de vie : options présentées, qualifiées, évaluées et illustrées par des compétences pluridisciplinaires ; le collectif est instruit, pas consulté.", + "source": "Mon nom est personne — « Peuple adulte »" + }, + { + "nom": "Vote à curseurs multi-critères (table de mixage)", + "description": "Toute question structurée en grandes lignes graduées et boutons de réglage, dont les volumes ; positions graduées et pondérées, cycle itératif déplacer-évaluer-ajuster-recommencer ; empêche amalgames et formation de camps.", + "source": "Mon nom est personne — « Clivages binaires »" + }, + { + "nom": "Décision incrémentielle (modèle Taïwan)", + "description": "Dossier complexe découpé finement en points ; chacun navigue dans les points qui le concernent, pondère ses préférences, positionne ses curseurs, valide les agrégations d'un clic ; restitution par cartographie de clôture (positions soutenues, hiérarchie des enjeux, niveaux d'urgence) visible de tous.", + "source": "Mon nom est personne — « Nouvelle carte »" + }, + { + "nom": "Décision paramétrique continue sous contrainte", + "description": "Classe de décision où le bulletin est un vecteur de paramètres bornés (produit d'intervalles) manipulé par gestes directs ; trois strates : paramètres structurels fixés par le cadre, paramètres de forme votés, variables dérivées résolues par une équation de réalité appliquée au registre réel des affectés — toute option exprimable est faisable par construction.", + "source": "SejeteralO (engine/pricing.py, docs/equations.html)" + }, + { + "nom": "Agrégation par médiane élément par élément", + "description": "Médiane composante par composante des contributions actives : robuste aux extrêmes, politiquement explicable, et le résultat est lui-même une décision valide du même type que les bulletins (domaine en produit d'intervalles). À nommer et expliquer dans l'UI, et à rendre décidable par la couche méta.", + "source": "SejeteralO (engine/median.py) + libreDecision v1 (engine médiane)" + }, + { + "nom": "Vote révisable + cristallisation", + "description": "Un seul vote actif par membre, remplaçable jusqu'à la deadline (dernier vote compte, historique conservé), compte à rebours visible, recalcul continu de l'agrégat ; puis geste distinct de cristallisation/publication horodatée qui devient référence du cycle suivant.", + "source": "SejeteralO (votes.is_active, published_*)" + }, + { + "nom": "Impact personnalisé + double ancre", + "description": "Avant toute soumission, chaque membre voit ce que l'option change pour lui (profil renvoyé dès l'authentification, calcul sur ses attributs réels) ; le statu quo calculé sur les mêmes données et une proposition de référence de l'animateur restent affichés en permanence comme ancres de comparaison.", + "source": "SejeteralO (« Mon prix au m³ », current_model.py, courbe publiée)" + }, + { + "nom": "Faisceau des positions", + "description": "Overlay public anonymisé de toutes les contributions actives superposées à l'agrégat en trait épais : la décision en formation est observable en continu, la dispersion des préférences est une information de gouvernance de premier rang.", + "source": "SejeteralO (VoteOverlayChart, endpoint overlay)" + }, + { + "nom": "Exploration bidirectionnelle contrainte↔résultat", + "description": "Le résultat agrégé est lui-même manipulable (barre médiane draggable) et rétro-calcule la contrainte correspondante — négocier le cadre par manipulation du résultat.", + "source": "SejeteralO (barre de prix médian draggable)" + }, + { + "nom": "Mandats spécifiques outillés", + "description": "Recrutement, mandat à périmètre défini et jalons, équipes pour réaliser et équipes pour mesurer, reddition de comptes, révocation, rétribution « instituée, pilotée par le peuple » ; représentants ambassadeurs et négociateurs, jamais décideurs ; mandats détachés de tout statut.", + "source": "Mon nom est personne — « Peuple adulte » + partie finale (« nécessite d'être très bien outillé »)" + }, + { + "nom": "Élection reléguée + interdit de noter les personnes", + "description": "L'élection est un module secondaire réservé aux problématiques d'échelle et de mandats (système souple de recrutement/révocation) ; interdit absolu d'évaluer des personnes — on évalue des choses et des propositions.", + "source": "Mon nom est personne — « Le mythe de l'élection »" + }, + { + "nom": "Engagements fermes à seuil critique", + "description": "Recueillir des engagements fermes et ne déclencher l'action collective qu'une fois le quota atteint, sinon partie remise — crowdaction/pledge à seuil, force du volume sans victime.", + "source": "Mon nom est personne — « Pouvoirs régaliens » (impôts)" + }, + { + "nom": "Consentement et fléchage des contributions", + "description": "Esprit de l'art. 14 DDHC : constater la nécessité, consentir librement, suivre l'emploi de chaque contribution, déterminer collectivement quotité/assiette/durée ; chaque donateur peut flécher son apport, la répartition collective ne portant que sur les dons libres.", + "source": "Mon nom est personne (impôts) + Une économie du don (modèle Axiom-Team)" + }, + { + "nom": "RIC permanent interne + constituante tirée au sort", + "description": "Le collectif structure son propre fonctionnement en continu (initiatives internes, garantie sur qui rédige l'initiative) ; ateliers constituants et tirage au sort pour les textes fondateurs — jamais de référendum binaire.", + "source": "Mon nom est personne — partie finale (RIC, Chouard)" + }, + { + "nom": "Institutions symétriques", + "description": "Quand une institution établie est sourde, le collectif institue un miroir qu'il habite (maison du peuple, Institut de la nomination qui qualifie postes/profils/candidats sans nommer) ; son poids de négociation = le volume des personnes qui l'habitent.", + "source": "Mon nom est personne — partie finale (« Tant pis, je crée une symétrie »)" + }, + { + "nom": "Observatoire des décisions", + "description": "Journal consignant les décisions telles qu'elles sont réellement prises (qui, comment, quand) sans procédure imposée a priori ; fonction de cristallisation en protocoles de quelques phrases éditables quand l'équipe estime que c'est mûr.", + "source": "Une économie du don — questions d'amorce" + }, + { + "nom": "Protocoles et coefficients relatifs consentis", + "description": "Préférer les protocoles (traitement prévu, pas d'issue projetée) au légiféré ; coefficients relatifs paramétrables (ancienneté, volume, solde net, pénibilité) appliqués dans l'échange avec consentement explicite des parties — jamais imposés globalement, jamais de catégories de personnes.", + "source": "Une économie du don — « légiférer vs protocoles », coefficients (prototype grateWizard)" + }, + { + "nom": "Budget de gratitude → appel d'offres permanent", + "description": "Chacun construit son échelle de valeurs (mots-clés, pourcentages ou DUs affectés) ; l'agrégation anonyme des échelles fait émerger une cartographie des besoins et envies (« prod bounty ») guidant les équipes qui produisent.", + "source": "Une économie du don — budget de gratitude" + }, + { + "nom": "Tirage au sort ludique à plafond", + "description": "Rétribution ou répartition de tâches par tirage aléatoire (boîte à gratitudes) avec plafond contre les spécialisations — évite les débats sur la valeur des tâches ; gamification assumée.", + "source": "Une économie du don — jeu 321...DU" + }, + { + "nom": "Rôles tournants à mémoire portée", + "description": "Sherpiz sans statut, dépositaires de la vision d'ensemble et de la mémoire, à rotation obligatoire ; l'outil porte la mémoire collective pour rendre la rotation possible sans perte ; seuils démographiques configurables (15 % mobiz, 3,5 % sherpiz) assumés comme arbitraires.", + "source": "Une économie du don — dimensionnement du bassin" + }, + { + "nom": "Modalité secret/ouvert et poids des compétences", + "description": "Vote secret (sans voir les choix des autres) vs ouvert comme paramètre de chaque décision ; la pondération des compétences aguerries est un chantier ouvert — paramétrable par périmètre, jamais tranché dans le code.", + "source": "Mon nom est personne — expériences de fouloscopie (échecs)" + }, + { + "nom": "Confiance à géométrie variable", + "description": "Habilitations progressives, contextuelles, fondées sur l'historique vécu, ajustables en finesse, jamais acquises a priori — sans jamais devenir un score de personne.", + "source": "Mon nom est personne — « Peuple adulte »" + }, + { + "nom": "Reformulation multi-référentiels", + "description": "Une même question peut être reformulée et présentée dans plusieurs référentiels (vues alternatives du même problème) comme outil de sortie d'impasse.", + "source": "Mon nom est personne — « Référentiel »" + }, + { + "nom": "Traitement précoce des conflits et départs", + "description": "Dès la formation d'une équipe autour d'une production, l'outil propose de définir le traitement des conflits et des départs — pas après coup.", + "source": "Une économie du don — questions d'amorce" + }, + { + "nom": "Degré d'inefficacité accepté", + "description": "Réglage explicite par périmètre du temps de réflexion et de la non-optimisation acceptés (droit à l'erreur, diversité) — garde-fou contre la pente de la décision algorithmique.", + "source": "Mon nom est personne — « Conditions de possibilités »" + } + ], + "production_collective": [ + "Cartographies thématiques vectorielles construites par chaque collectif, partagées et adaptées entre groupes — liaisons, interdépendances, symptômes/causes visibles, jamais de silos.", + "Contrat social vivant : constitution minimaliste écrite au fil de l'eau, versionnée, observée et mesurée en continu, révisable sans embonpoint — support du RIC permanent interne.", + "Observatoire auto-alimenté : le collectif produit ses propres données et indicateurs (économie locale, flux, participation) du fait de ses protocoles et outils, sans dépendre des données institutionnelles.", + "Visualisation collective des boucles d'échange : dessiner les boucles au fur et à mesure, rendre visibles celles qui ne bouclent pas et les croisements manquants, pour que chacun identifie les trous et y pourvoie — expliciter plutôt que programmer.", + "Productions collectives graduées en vitesses (« passer la seconde ») : productions individuelles → filières semi-artisanales et réseaux décentralisés → logistique inter-bassins → industrialisation versatile et gestion de communs (le logement en graal) ; équipes productives limitées (~12, alerte au-delà).", + "Caisses et cagnottes instituantes comme objets de première classe : dons fléchés avec tableaux de suivi de chaque affectation, répartition collective sur les seuls dons libres, caisse d'investissement locale avançant des DUs pour amorcer.", + "Budget de gratitude agrégé anonymement en appel d'offres permanent (« prod bounty ») : cartographie des besoins et envies consultable par les équipes qui produisent.", + "Gestion des communs (l'eau en premier) via des formes type SCIC : une part et une voix par habitant et par collectivité ; maisons du peuple et assemblées auto-instituées dotées de prérogatives opérationnelles.", + "Feux de la rampe collectifs : travail d'archiviste-rhétoricien consignant votes, nominations, contradictions et liaison discours/actes des mandatés — la mémoire vive comme production commune.", + "Lobbying populaire : opposer aux textes « un jugement construit par le peuple », think tanks maison à petite échelle en liaison les uns avec les autres ; boycotts ciblés et synchronisés.", + "Événementiel comme primitive : dates, débuts/fins, cadences, rendez-vous annuels (librodrome, Ğ(marché), Ğeconomicus) produisant des feuilles de route de productions.", + "Éducation populaire multi-formats pour armer les esprits (histoire, logique, rhétorique, sciences) — chacun transmet ce qu'il maîtrise sous l'égide du plaisir ; acquisition collective des expériences : évaluer avec la curiosité du chercheur, valoriser le droit de se tromper.", + "Fiches d'interopérabilité par groupe (règles, coefficients, pratiques explicites) pour créer compatibilités et passerelles entre groupes voisins et lointains.", + "Grappes de groupes en liaison permanente : coopérations ponctuelles, factorisation des efforts, visibilité collective, complémentarité — jamais de fédération représentative à voix unique ; scission féconde et comparaison des effets plutôt que consensus forcé.", + "Actions collectives à engagements fermes et seuil critique (crowdaction) : le geste ne se déclenche qu'au quota atteint.", + "Mise en face-à-face qualifiée et mappée des besoins et des ressources d'un bassin (manque identifié des réseaux locaux existants type Solaris)." + ], + "sejeteralo_lecons": [ + "Une décision continue devient votable si on la réduit à un petit espace paramétrique (≤ 6-7 dimensions) manipulé par gestes directs (poignées, curseurs, souris + tactile) — jamais par saisie de chiffres ; les contraintes géométriques des poignées garantissent la validité par construction : zéro validation a posteriori, zéro message d'erreur.", + "Trois strates affichées et taguées : cadre imposé en lecture seule (« contrainte »), forme votée (éditeur), variables déduites (« calculé ») — le votant comprend ce qu'il contrôle et ce qui découle ; personne ne vote le montant total, on vote sa répartition.", + "Équation de réalité pluggable : une fonction pure (paramètres libres + structurels + registre des membres) → variables dérivées + impact par membre ; à généraliser en « espace de politique paramétré + contrainte de conservation + profil personnel d'impact » — ne pas transposer le spécifique eau (Bézier bi-tranche, RP/RS/PRO, axe inversé).", + "Moteur de calcul dupliqué : implémentation client miroir pour le feedback instantané pendant le geste, recompute serveur débouncé (150-300 ms) faisant autorité — pattern obligatoire pour tout simulateur de décision temps réel.", + "Impact personnel permanent : l'authentification renvoie le profil du membre et l'UI maintient une case « qu'est-ce que ça change pour moi » interpolée en continu — répondre à cette question avant toute autre pédagogie.", + "Superposer la population réelle dans l'espace de décision lui-même (un point par foyer sur la courbe, histogrammes, compteurs par tranche) plutôt que dans des statistiques séparées.", + "Double ancre systématique : le statu quo calculé sur les mêmes données (jamais absent de l'écran) + une proposition de référence publiée par l'animateur comme état initial commun — en assumant que l'ancrage est un pouvoir à encadrer par la couche méta.", + "Le vote est un état révisable, pas un événement : un seul vote actif par membre, dernier vote compte, historique conservé par désactivation, deadline en compte à rebours, clôture automatique ; puis cristallisation/publication horodatée distincte de l'agrégation vivante — abaisse le coût psychologique du premier vote.", + "Rendre l'agrégation visible : faisceau translucide des votes des autres (toggle) + agrégat en trait épais + compteur de votes ; MAIS expliquer la règle (la médiane par paramètre est muette chez SejeteralO et ne produit pas une courbe effectivement votée — défaut à corriger : nommer, vulgariser et rendre décidable l'agrégateur).", + "Onboarding zéro-compte : un code court distribué physiquement porte à la fois l'identité et le profil de personnalisation, sans email ni compte ; montée vers l'identité souveraine (toile de confiance Duniter) en divulgation progressive (« Bientôt disponible » grisé).", + "Deux interfaces sur le même modèle et le même moteur : sliders paramétriques nommés pour l'expert qui cadre, manipulation directe pour le novice qui choisit.", + "Structure de page « agir d'abord, comprendre ensuite » : l'outil interactif et le bloc vote en haut, la situation de départ, les explications et le contenu éditorial du périmètre en dessous.", + "Exploration bidirectionnelle : rendre le résultat agrégé lui-même draggable pour rétro-calculer la contrainte — la frontière cadre/résultat devient négociable par manipulation.", + "Robustesse numérique de série : bornes validées sur chaque paramètre (Pydantic ge/le), sanitize NaN/inf, dégénérescences spécifiées (zéro vote → référence publiée ou défaut ; contrainte insoluble → repli défini) — jamais d'écran vide.", + "Corrections exigées pour la généralisation : alternative clavier + champ numérique aux poignées drag-only, libellés métier sur tout paramètre abstrait (jamais de a, b, c bruts), pas d'alert() pour les erreurs.", + "Multi-tenancy par périmètre : registre des affectés importé avec attributs individuels consommés par le moteur d'impact ; éligibilité dérivée de l'appartenance au registre (1 membre = 1 voix), contrôle strict membre↔périmètre, contenu éditorial (mini-CMS) par périmètre, séparation nette des rôles cadreur/votant." + ], + "tensions": [ + "Anonymat vs identité : dépersonnaliser les propositions (@personne, anti ad-hominem, anti-sauveur) tout en garantissant l'unicité du vote (toile de confiance, 1 membre = 1 voix) — imposer deux identités découplées dans le même outil : pseudonyme de débat, certification de vote, jamais reliées publiquement.", + "Transparence radicale vs secret : feux de la rampe et faisceau public des positions d'un côté, modalité de vote secret paramétrable, espace intime intouchable et Institution du Secret de l'autre — le caractère secret/ouvert doit être une décision par périmètre, pas un défaut technique.", + "Contrôler sans surveiller : les mêmes primitives de traçabilité servent le contrôle légitime des mandatés et pourraient outiller le contrôle du peuple par le peuple (glas absolu) — cloisonner par construction : mémoire exhaustive sur les mandats et les décisions, zéro métrique sur les personnes.", + "Simplicité du geste vs rigueur du jugement : le drag fluide et le clic unique (SejeteralO) rendent le vote désirable mais risquent de recréer du zapping ; l'instruction préalable et le multi-critères ralentissent — assumer le degré d'inefficacité accepté comme réglage, pas comme échec UX.", + "Anti-préfiguration vs utilisabilité immédiate : la doctrine interdit les templates rigides (« une chorégraphie du don ne se télécharge pas en .ods ») mais un outil vide est inutilisable — livrer des primitives composables, des banques de questions d'amorce et des exemples cristallisables, jamais des workflows verrouillés.", + "Égalité vs compétence : 1 membre = 1 voix est le principe, mais le juste poids des compétences aguerries est un chantier explicitement ouvert chez l'auteur — le rendre paramétrable par périmètre via la couche méta, jamais tranché dans le code, et sans jamais noter des personnes.", + "Numérique condition de possibilité vs primauté du physique : l'outil est indispensable (grand volume) mais doit s'auto-limiter — passerelles hors-écran, intermédiation humaine, renvois vers les lieux de brassage ; il sert la logistique et la décision, pas le lien social.", + "Agrégat mathématique vs jugement construit : la médiane est robuste et explicable mais risque de devenir une décision algorithmique de fait si elle clôt le débat — l'agrégat éclaire une délibération (cartographie de clôture), la cristallisation reste un geste humain distinct et daté.", + "Mémoire vive vs droit à l'erreur : archiver les engagements et actes des mandatés (déshonneur documenté) tout en garantissant la révisabilité et le droit de se tromper des membres — archiver les mandats, pardonner les positions ; l'historique des votes révisés n'est jamais opposable à une personne.", + "Ancrage vs influence : la proposition de référence publiée comme état initial (SejeteralO) structure le débat mais donne un pouvoir d'ancrage à l'animateur — le cadre lui-même (qui publie la référence, qui fixe les contraintes) doit être décidable par la couche méta.", + "Petite échelle par défaut vs population entière : la subsidiarité impose le bassin de vie, l'ambition vise le très grand volume — architecture fractale en grappes agrégées (couches d'oignon) sans plateforme centrale ni fédération représentative à voix unique.", + "Doctrine forte vs neutralité axiomatique : l'outil est doctrinal dans sa structure (pas de binaire, pas de notes de personnes, pas de partis, laïcité structurelle) mais neutre sur les contenus décidés — incarner les garde-fous sans jamais orienter une décision de fond." + ] + }, + "lectures_ok": 7 + }, + "workflowProgress": [ + { + "type": "workflow_phase", + "index": 1, + "title": "Lecture" + }, + { + "type": "workflow_phase", + "index": 2, + "title": "Corpus" + }, + { + "type": "workflow_agent", + "index": 1, + "label": "livre:personne-1/3", + "phaseIndex": 1, + "phaseTitle": "Lecture", + "agentId": "ac7f01b06d2adbc37", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786410717874, + "lastProgressAt": 1786410717874, + "cached": true, + "resultPreview": "{\"resume\":\"Cette tranche ouvre « Mon nom est personne » par un prologue consacré à l'anonymat de l'auteur, geste politique délibéré : le livre est « orphelin », nul ne peut se l'approprier ni l'incarner, afin que seul le contenu compte — « ce livre n'est à personne, ce qu'il devient appartient à tout le monde ». L'édition préfigure la doctrine : licence Creative Commons, original sur IPFS, portefe…", + "promptPreview": "Tu lis une tranche d'un livre écrit par Yvv, l'auteur-commanditaire d'un outil numérique de décision collective appelé libreDecision. Son intention: \"donner à n'importe quel collectif, du plus informel au plus structuré, de 3 personnes à une population entière, la capacité d'une démocratie qui n'a jamais existé\". Ces livres sont la source doctrinale de l'outil.\nLis INTÉGRALEMENT la tranche indiqué…" + }, + { + "type": "workflow_agent", + "index": 2, + "label": "livre:personne-2/3", + "phaseIndex": 1, + "phaseTitle": "Lecture", + "agentId": "a475c2f4568f3586e", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786410717874, + "lastProgressAt": 1786410717874, + "cached": true, + "resultPreview": "{\"resume\":\"Cette tranche clôt la partie théorique et ouvre la partie « Concrètement ». L'auteur achève la métaphore du peuple adulte : l'émancipation vis-à-vis de la nation mère est « une succession, pourquoi en faire une sécession ? ». Un peuple adulte choisit lui-même son mythe fondateur, un « rêve lucide » qu'il réalise sans demander la permission. Contre l'obsession de « sauver le monde », il …", + "promptPreview": "Tu lis une tranche d'un livre écrit par Yvv, l'auteur-commanditaire d'un outil numérique de décision collective appelé libreDecision. Son intention: \"donner à n'importe quel collectif, du plus informel au plus structuré, de 3 personnes à une population entière, la capacité d'une démocratie qui n'a jamais existé\". Ces livres sont la source doctrinale de l'outil.\nLis INTÉGRALEMENT la tranche indiqué…" + }, + { + "type": "workflow_agent", + "index": 3, + "label": "livre:personne-3/3", + "phaseIndex": 1, + "phaseTitle": "Lecture", + "agentId": "ac48a9f90753697ec", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786410717875, + "lastProgressAt": 1786410717875, + "cached": true, + "resultPreview": "{\"resume\":\"Cette tranche finale couvre la partie opérationnelle du livre et sa conclusion. Yvv part de l'hygiène numérique comme « simple pratique élémentaire » d'un peuple affranchi, puis défend la nécessité des institutions contre la tentation anarchiste : une démocratie a besoin de symboles pour « penser et agir à grande échelle », mais structure ne signifie jamais concentration — on peut « str…", + "promptPreview": "Tu lis une tranche d'un livre écrit par Yvv, l'auteur-commanditaire d'un outil numérique de décision collective appelé libreDecision. Son intention: \"donner à n'importe quel collectif, du plus informel au plus structuré, de 3 personnes à une population entière, la capacité d'une démocratie qui n'a jamais existé\". Ces livres sont la source doctrinale de l'outil.\nLis INTÉGRALEMENT la tranche indiqué…" + }, + { + "type": "workflow_agent", + "index": 4, + "label": "livre:don-1/2", + "phaseIndex": 1, + "phaseTitle": "Lecture", + "agentId": "adc77a4e35421e9ae", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786410717875, + "lastProgressAt": 1786410717875, + "cached": true, + "resultPreview": "{\"resume\":\"Cette première moitié du livre pose les fondations doctrinales d'une « économie du don » rendue concevable par la monnaie libre (la June/Ğ1, application de la TRM — Théorie Relative de la Monnaie). Yvv annonce un essai, pas un guide : « des repères concrets et conceptuels » pour nourrir une réflexion collective dans chaque « bassin de vie », le passage à l'action appartenant aux collect…", + "promptPreview": "Tu lis une tranche d'un livre écrit par Yvv, l'auteur-commanditaire d'un outil numérique de décision collective appelé libreDecision. Son intention: \"donner à n'importe quel collectif, du plus informel au plus structuré, de 3 personnes à une population entière, la capacité d'une démocratie qui n'a jamais existé\". Ces livres sont la source doctrinale de l'outil.\nLis INTÉGRALEMENT la tranche indiqué…" + }, + { + "type": "workflow_agent", + "index": 5, + "label": "livre:don-2/2", + "phaseIndex": 1, + "phaseTitle": "Lecture", + "agentId": "a6a50a62c4b3e5e77", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786410717875, + "lastProgressAt": 1786410717875, + "cached": true, + "resultPreview": "{\"resume\":\"Cette seconde moitié du livre passe de la théorie à la mise en œuvre. Yvv pose d'abord sa formule de la raison d'être d'une économie : « Couvrir les besoins, pour vivre, nourrir les plaisirs de vivre. Avant tout comment produire, comment distribuer, sans nuire. » L'économie commence par la production distribuée, pas par l'échange — l'échange est un « baromètre », pas une variable. D'où …", + "promptPreview": "Tu lis une tranche d'un livre écrit par Yvv, l'auteur-commanditaire d'un outil numérique de décision collective appelé libreDecision. Son intention: \"donner à n'importe quel collectif, du plus informel au plus structuré, de 3 personnes à une population entière, la capacité d'une démocratie qui n'a jamais existé\". Ces livres sont la source doctrinale de l'outil.\nLis INTÉGRALEMENT la tranche indiqué…" + }, + { + "type": "workflow_agent", + "index": 6, + "label": "sejeteralo:frontend-ux", + "phaseIndex": 1, + "phaseTitle": "Lecture", + "agentId": "a5c56cb80e6f2d924", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786410717876, + "lastProgressAt": 1786410717876, + "cached": true, + "resultPreview": "{\"resume\":\"SejeteralO (frontend Nuxt 4/Vue 3, CSS vanilla à variables, SVG natif sans lib de charting) est une plateforme où les habitants d'une commune votent sur la FORME de la courbe tarifaire de l'eau. Le cœur est la page citoyenne unique commune/[slug]/index.vue (~1900 lignes) : un grand graphique SVG interactif où deux courbes de Bézier cubiques (tranche « population » bleue, tranche « cas e…", + "promptPreview": "Tu analyses le projet SejeteralO, un outil de décision destiné aux collectivités (communes) et leur population: tarification participative de l'eau. Les citoyens votent sur la FORME de la courbe tarifaire via des éditeurs Bézier interactifs; le système calcule un tarif à équilibre de recettes en temps réel; l'agrégation des votes se fait par médiane élément par élément.\nRacine: /home/yvv/Documents…" + }, + { + "type": "workflow_agent", + "index": 7, + "label": "sejeteralo:backend-modele", + "phaseIndex": 1, + "phaseTitle": "Lecture", + "agentId": "aa3312d6973519e7d", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786410717876, + "lastProgressAt": 1786410717876, + "cached": true, + "resultPreview": "{\"resume\":\"SejeteralO est un backend FastAPI/SQLAlchemy qui fait voter les citoyens d'une commune non pas sur un choix discret mais sur la FORME d'une courbe tarifaire de l'eau. L'objet de la décision est un vecteur de 6 paramètres continus (vinf ≥ 0 ; a, b, c, d, e ∈ [0,1], validés par Pydantic) qui définissent une courbe de Bézier cubique à deux paliers prix/volume (palier 1 : 0→vinf, tarificati…", + "promptPreview": "Tu analyses le backend du projet SejeteralO, outil de tarification participative de l'eau pour communes (citoyens votent des courbes tarifaires Bézier, agrégation par médiane, équilibre de recettes).\nRacine: /home/yvv/Documents/PROD/DEV/SejeteralO/backend. Lis: app/engine/ (pricing.py compute_p0/compute_tariff/compute_impacts, integrals.py coefficients α₁ α₂ β₂, median.py médiane élément par éléme…" + }, + { + "type": "workflow_agent", + "index": 8, + "label": "synthese:corpus", + "phaseIndex": 2, + "phaseTitle": "Corpus", + "agentId": "a53cb3feb548c571a", + "model": "claude-fable-5", + "state": "done", + "startedAt": 1786410717902, + "queuedAt": 1786410717877, + "attempt": 1, + "lastToolName": "StructuredOutput", + "lastToolSummary": "Aucune démocratie n'a jamais existé. Nous vivons sous une o…", + "promptPreview": "Tu synthétises le CORPUS D'INTENTION de libreDecision v2, outil de décision collective voulu par Yvv (\"n'importe quel collectif, de 3 personnes à une population entière, la capacité d'une démocratie qui n'a jamais existé\"). Tu disposes des extractions fidèles de ses 2 livres (\"Mon nom est personne — le pouvoir de la décision au nom de l'émancipation\" et \"Une économie du don — enfin concevable\") et…", + "lastProgressAt": 1786411457798, + "tokens": 92908, + "toolCalls": 10, + "durationMs": 739896, + "resultPreview": "{\"doctrine\":\"Aucune démocratie n'a jamais existé. Nous vivons sous une oligarchie électorale : le pouvoir aux mains de quelques-uns, validés par l'élection. Ses piliers convenus omettent la clé de voûte : la décision, pouvoir le plus générique et le plus en amont de tous les autres. Privé de décision, une personne n'a aucun pouvoir, un peuple non plus. On ne délègue pas, on abandonne ; « a voté » …" + } + ], + "totalTokens": 92908, + "totalToolCalls": 10 +} \ No newline at end of file diff --git a/docs/dev/lectures-corpus-brut.json b/docs/dev/lectures-corpus-brut.json new file mode 100644 index 0000000..dc481be --- /dev/null +++ b/docs/dev/lectures-corpus-brut.json @@ -0,0 +1,971 @@ +{ + "lectures": [ + { + "resume": "Cette seconde moitié du livre passe de la théorie à la mise en œuvre. Yvv pose d'abord sa formule de la raison d'être d'une économie : « Couvrir les besoins, pour vivre, nourrir les plaisirs de vivre. Avant tout comment produire, comment distribuer, sans nuire. » L'économie commence par la production distribuée, pas par l'échange — l'échange est un « baromètre », pas une variable. D'où la métaphore centrale : « passer la seconde », quitter le régime des productions individuelles (Ğmarchés, vide-greniers, mécénat) pour les productions collectives (filières semi-artisanales, « boucles bouclantes », réseaux décentralisés, mutualisation d'équipements, centrales villageoises), puis la troisième (déploiements logistiques inter-bassins, unités mobiles), et une quatrième/cinquième : « industrialisation versatile » et gestion de communs, le logement étant posé comme « graal ».\n\nL'« économie de greffe » structure la relation avec l'économie euro : pas de substitution mais une cohabitation de deux économies séparées « comme l'huile et l'eau », qu'on ne mélange jamais, reliées par des « nexus » — des sas qui transforment la nature de ce qui transite (le « transfert d'énergie fiat » n'est légitime que pour produire, pas pour consommer). Pistes concrètes : parts hybrides de copropriété (token junes + stable euro), « investissement double » (un équipement sert les deux économies), chorégraphies de dons euros avec « lisibilité précise des affectations de chaque euro ».\n\nLe dimensionnement d'un bassin de vie donne des ratios arbitraires mais mobilisateurs : sur 60 000 habitants, viser 30 000 dans l'économie, 15 % de « mobiz » (personnes impliquées dans les productions collectives) et 3,5 % de « sherpiz » (porteurs qui consacrent plus de temps, « dépositaires de la vision d'ensemble et de la mémoire », qui doivent tourner, sans statut). L'échelle d'amorce la plus gérable : 1 000 personnes, 75 mobiz, 3 sherpiz, « à rayon de vélo ». Il propose de produire ses propres données et indicateurs, un « observatoire » auto-alimenté par les protocoles et outils du collectif, pour cartographier l'économie locale.\n\nPour les entrepreneurs euro, l'indicateur de « roue libre » (seuil de rentabilité + planchers de trésorerie) détermine l'heure exacte où l'on est « totalement libre… de donner » sans risque pour son outil de travail. L'« économie de flux inversés » est une « règle dynamique » : je donne avant de recevoir, la demande précède l'offre ; une « caisse d'investissement locale » peut avancer des DUs pour amorcer (exemple de la paysanne meunière). Les boucles d'échange « doivent boucler » et « doivent se croiser » ; il faut les dessiner au fur et à mesure, chacun identifiant les trous.\n\nIl conçoit un « Ğ(marché) » inédit : 100 % liberté de choisir sa monnaie, guichet d'entrée façon octroi médiéval, jetons à usage unique, « lutins » lycéens accompagnant le jeu des « coefficients relatifs », garantie de couverture des coûts marginaux des producteurs par mutualisation des recettes euros, « cercle de répartition » en clôture, trésor mécène en filet.\n\nSur le légal : ne rien contourner, transparence et sincérité, « bonne foi », provisionner les risques (TVA sur dons, retour de kick), déclarer les dons en DUs au « questeur », laisser « le législateur légiférer ». Le DU n'est pas un actif mais une mesure : « le DU n'a pas de valeur, il mesure les valeurs ». La démarche peut sincèrement se dire « performance artistique collective », « chorégraphie du don ».\n\nSuivent les greffes : assos caritatives, ESS, tiers-lieux, FabLabs (et FabUnits pour des « lignes de fabrique » autogérées), France Travail (vigilance sur les faux espoirs), agriculteurs, artisans (posture : proposer d'abord ses propres offres), et surtout les 15-25 ans, « destination la plus stratégique » d'une aventure au pas de temps de 10 ans. Contre la fédération (représentation, voix unique, clivages), il défend « coopérations ponctuelles, factorisation des efforts, visibilité collective, complémentarité », et l'événement comme régénérateur d'énergie : le « librodrome », deux jours sur trois axes — « logiciel libre ; monnaie libre ; décision collective ». Refusant toute fiche prête à l'emploi (« une chorégraphie du don ne se télécharge pas en fichier .ods »), il livre des questions d'amorce : ressources, chemin, rythme cardiaque, protocoles de décision rapide, traitement précoce des conflits et départs, et surtout un « observatoire des décisions » qui consigne comment elles sont prises dans les faits plutôt que de les préfigurer.\n\nLes annexes traitent des cryptos (la june est un « ovni » : réseau monétique propre, forgerons rétribués par caisse de dons, référence raspberry pi ; critique « ponzinomics » du bitcoin ; refus d'une introduction précoce sur DeX, proposition d'un fork « F1 » comme sas) et du logiciel libre, lignée directe de la monnaie libre, avec hommage aux réfractaires au numérique et appel à une « intermédiation humaine » pour une vie numérique sans écran.", + "theses": [ + "L'économie est avant tout affaire de production, pas d'échange : l'échange est un baromètre, la production est la variable ; il faut « passer la seconde », des productions individuelles aux productions collectives, à l'échelle du bassin de vie.", + "L'économie du don ne remplace pas l'économie euro : elle s'y greffe ; les deux économies doivent rester séparées (jamais mélangées) et reliées par des « nexus », des sas qui transforment ce qui transite ; savoir passer de l'une à l'autre est la vraie puissance.", + "Les flux doivent être inversés : je donne avant de recevoir (règle dynamique, non morale), la demande précède l'offre, et l'investissement ne sert plus qu'à amorcer la pompe.", + "Le DU n'a pas de valeur, il mesure les valeurs ; le vendre en euros n'a pas de sens dans ce référentiel — une économie naissante a besoin du flux inverse (« c'est comme si on demandait à un bébé d'allaiter sa mère »).", + "Face au droit : transparence, sincérité, bonne foi, provisions de prudence ; ne rien dissimuler, laisser le législateur légiférer, et assumer sincèrement la qualification de « performance artistique collective » / « chorégraphie du don ».", + "Des seuils chiffrés arbitraires mais mobilisateurs structurent l'amorce : 15 % de mobiz, 3,5 % de sherpiz ; l'échelle gérable au départ est un bassin de 1 000 personnes (75 mobiz, 3 sherpiz), « à rayon de vélo ».", + "La fédération est une fausse piste (représentation, voix unique, clivages) ; préférer coopérations ponctuelles, factorisation des efforts, visibilité collective, complémentarité — et l'événement comme outil de régénération d'énergie.", + "Ne pas préfigurer les fonctionnements : observer et consigner comment les décisions sont réellement prises (« observatoire des décisions »), puis cristalliser en protocoles de quelques phrases quand c'est mûr ; éviter les commissions hâtives qui satellisent.", + "La jeunesse (15-25 ans) est la destination la plus stratégique ; l'aventure a un pas de temps de 10 ans et œuvre pour cette génération.", + "La june est un ovni crypto : réseau monétique propre, sobre (référence raspberry pi), forgerons rétribués par une caisse de dons ; il faut retarder toute introduction sur un marché (DeX), éventuellement via un fork-sas « F1 », pour protéger l'économie naissante des dynamiques spéculatives.", + "La monnaie libre s'inscrit dans la lignée directe du logiciel libre : l'émancipation économique et l'émancipation numérique (cloud libre localisé, autonomie numérique du bassin de vie) sont indissociables." + ], + "concepts": [ + { + "nom": "Passer la seconde", + "definition": "Métaphore de boîte de vitesses : passer des productions individuelles (1re) aux productions collectives (2de), puis aux déploiements logistiques (3e) et à l'industrialisation versatile / gestion de communs (4e-5e), le logement étant le graal." + }, + { + "nom": "Économie de greffe", + "definition": "Économie qui se greffe sur l'existant sans le remplacer ; deux économies cohabitent « comme l'huile et l'eau », séparées mais l'une dans l'autre, sans jamais se mélanger." + }, + { + "nom": "Nexus", + "definition": "Plus qu'une passerelle : un sas qui transforme la nature de ce qui transite entre les deux économies (ex. énergie stockée euro convertie en mouvement d'une économie monnaie-libriste ; tokens hybrides junes + stable euro pour la copropriété d'équipements)." + }, + { + "nom": "Transfert d'énergie fiat", + "definition": "Apport d'euros ou d'équipements euro vers l'économie du don ; légitime et utile s'il sert à produire, simple artefact ou illusion s'il sert une consommation directe." + }, + { + "nom": "Économie de flux – inversés", + "definition": "Lecture vectorielle de l'économie : donner avant de recevoir comme règle dynamique (non morale) ; la demande précède l'offre ; identifier les flux et y « veiller » comme des canaux d'irrigation que chaque nouvel entrant peut choisir de servir." + }, + { + "nom": "Roue libre", + "definition": "Indicateur individuel combinant seuil de rentabilité saisonnier et planchers de trésorerie, qui détermine l'heure exacte (de l'année, du mois, de la semaine) à partir de laquelle l'entrepreneur est totalement libre de donner sans aucun risque pour son outil de travail." + }, + { + "nom": "Mobiz", + "definition": "Personnes impliquées dans les productions collectives ; seuil jalon de 15 % de la population visée du bassin de vie." + }, + { + "nom": "Sherpiz", + "definition": "Poignée de porteurs (seuil de 3,5 %) qui consacrent plus de temps que les autres et sont dépositaires de la vision d'ensemble et de la mémoire ; sans statut ni longue durée : elles peuvent — ou doivent — tourner." + }, + { + "nom": "Bassin de vie", + "definition": "Périmètre naturel de l'économie (bassin versant d'une vallée, groupe de communes type RPI, quartiers urbains) ; échelle d'amorce gérable : environ 1 000 personnes à rayon de vélo." + }, + { + "nom": "Boucles bouclantes", + "definition": "Boucles d'échange circulaires : « les boucles doivent boucler » (sinon impasse, ressentiment, abandon) et « doivent se croiser » (pour absorber les asymétries) ; à dessiner au fur et à mesure pour que chacun identifie les trous et y pourvoie." + }, + { + "nom": "Ğ(marché)", + "definition": "Format inédit de marché où le chaland choisit sa monnaie à 100 % (prix euro ou gratitude en DUs) : guichet d'entrée façon octroi, jetons à usage unique, lutins accompagnateurs, mutualisation des recettes euros garantissant les coûts marginaux des producteurs, cercle de répartition en clôture." + }, + { + "nom": "Coefficients relatifs", + "definition": "Jeu de modulation de la gratitude entre offreur et receveur (ancienneté, solde net, etc.) autour d'un point d'équilibre annoncé en DUs par la personne qui offre." + }, + { + "nom": "Observatoire des décisions", + "definition": "Plutôt que préfigurer comment les décisions doivent être prises, consigner comment elles sont prises dans les faits, comme base de réflexion pour donner leur chance à de nouvelles façons de faire, puis traduire en protocoles de quelques phrases quand c'est mûr." + }, + { + "nom": "Industrialisation versatile", + "definition": "Capacité d'un bassin de vie à produire en petites séries, via des fabriques éphémères ou mobiles et des mobilisations événementielles ; inclut la fabrique de moyens de production." + }, + { + "nom": "Investissement double", + "definition": "Investissement sur un équipement ou une infrastructure servant à la fois une économie euro (qui couvre les frais fiat) et une économie monnaie-libriste, sans interférence de l'euro pour cette dernière." + }, + { + "nom": "Chorégraphie du don", + "definition": "Requalification sincère de l'économie du don en performance artistique collective : transformer une formule mathématique en toile de dons via la forge numérique d'une mesure universelle ; titre alternatif envisagé du livre." + }, + { + "nom": "Gestion à l'anglaise", + "definition": "Gestion manipulable des entrées-sorties, proche d'une gestion de trésorerie (dite « au point mort »), support du calcul de la roue libre." + }, + { + "nom": "F1 – fork one", + "definition": "Proposition d'une seconde monnaie TRM dédiée à l'introduction sur un DeX, jouant le rôle de sas symbolique et technique qui préserve la june de la dynamique spéculative des marchés." + }, + { + "nom": "Boîte à gratitudes", + "definition": "Dispositif ludique de tirage au sort pour rétribuer les tâches (expérience « made in zion »)." + }, + { + "nom": "Pseudo-isolées", + "definition": "Communautés à échelle restreinte, revalorisées comme échelle peut-être appropriée ou désirable, pour peu qu'elles soient interconnectées." + }, + { + "nom": "Petit équilibre", + "definition": "Posture militante d'un modèle économique euro qui se contente de couvrir les besoins avérés, en évacuant toute vénalité (terme emprunté aux collectivités)." + }, + { + "nom": "Librodrome", + "definition": "Événement structurant de deux jours en gestation, sur trois axes : vie numérique / amorce d'une économie / émancipation citoyenne — logiciel libre ; monnaie libre ; décision collective — avec plateforme coopérative de productions collectives." + } + ], + "mecanismes_decision": [ + "Décentralisation des décisions et des responsabilités, posée comme principe de positionnement général du modèle (avec transparence, prudence, esprit de labo).", + "Rôle tournant des sherpiz : sans statut, dépositaires de la vision d'ensemble et de la mémoire, elles doivent tourner — mandat de fait fondé sur le temps consacré, pas sur l'élection.", + "Observatoire des décisions : consigner comment les décisions sont réellement prises avant tout protocole, puis cristalliser en « protocoles de quelques phrases quand c'est mûr » ; méfiance envers les commissions hâtives qui « satellisent ».", + "Chaque équipe définit son « rythme cardiaque », ses « protocoles pour prendre rapidement les décisions », ses « espaces pour raisonner ensemble », ses outils — autant de fonctionnements que d'équipes.", + "Dépoussiérer de façon très précoce le traitement des conflits et des départs dès qu'une équipe se forme autour d'une production.", + "Tours de table structurés d'amorce : ressources humaines et matérielles acquises/accessibles, conditions non négociables et tabous, envies mobilisantes puis contraintes, positions sur risques et limites individuelles/collectives.", + "Cercle de répartition des euros en clôture de Ğ(marché), avec mutualisation des recettes et garantie de couverture des coûts marginaux (trésor mécène en filet).", + "Fléchage des dons (modèle Axiom-Team) : tableaux de bord permettant aux donateurs de choisir l'affectation de chaque don ; l'association ne décide de la répartition que sur les dons libres.", + "Refus de la fédération et de la représentation (voix unique) au profit de coopérations ponctuelles, factorisation des efforts, visibilité collective, complémentarité.", + "Distinguer institutions et personnes : s'adresser aux personnes physiques, pas aux rôles ; repérer les moments où elles « changent de casquette ».", + "Visualisation collective des boucles d'échange (les dessiner au fur et à mesure) pour générer des initiatives individuelles spontanées : chacun identifie les trous et y pourvoit — expliciter plutôt que programmer.", + "Boîte à gratitudes : tirage au sort ludique pour rétribuer les tâches.", + "Inspiration possible des DAO (Decentralized Autonomous Organizations), « à notre manière monnaie-libriste ».", + "Négociation au cas par cas des lieux (privé affranchi, public/mairie, établissement scolaire, lieu recevant du public avec privatisation par invitation protocolisée)." + ], + "production_collective": [ + "Gradation en vitesses : 1) productions individuelles (Ğmarchés, prestations, autoconsommation) ; 2) productions collectives récurrentes en volume, filières semi-artisanales, boucles bouclantes, réseaux décentralisés (épicerie, restaurant, réparation), mutualisation locale des équipements et de services solidaires, centrales villageoises ; 3) déploiements logistiques inter-bassins et ruraux-urbains, fabrique de moyens de production, unités mobiles ; 4-5) industrialisation versatile, gestion de communs, logement.", + "Filière pain comme cas d'école : questions de tenue dans la durée, volumes, relais entre entrées (jusqu'à la farine) et sorties (mangeuses de pain) ; amorce par caisse d'investissement locale avançant des DUs à la paysanne meunière.", + "Filière bois de chauffage autogérée à l'échelle d'une vallée ou de 1 000 habitations ; autoconsommation électrique mutualisée (centrales villageoises, « grid locale »).", + "Filières de fermentation (vin, bière) valorisées comme « monnaies sociales », avec réappropriation de protocoles de prudence sanitaire propres.", + "Conserveries événementielles (15 000 conserves), productions collectives événementielles s'appuyant sur recycleries et plateformes logistiques existantes.", + "Épicerie décentralisée comme extension naturelle de la production et nœud du tissage économique ; jardins partagés, bars associatifs et tiers-lieux comme nœuds logistiques urbains.", + "FabLabs et FabUnits : « lignes de fabrique » configurables pour petites séries sans investissement pluriannuel, formation à l'autonomie de production, autogestion peu coûteuse, filière plastique locale « du déchet à l'ustensile ».", + "Cloud libre complet de « confiance localisée » en investissement double : modèle d'affaires euro couvrant les frais des deux économies, services identiques côté monnaie-libriste avec coefficients relatifs ; déploiement de nœuds locaux (ipfs, réseau monétique, IA localisée, messagerie chiffrée, opencloud, forum, wiki, visio).", + "Food truck en roue libre : mise à disposition des ingrédients et du four un jour donné, création d'un lieu et d'une activité, les bénéficiaires évaluent les gratitudes avec les coefficients.", + "Ğ(marché) garni de producteurs professionnels avec liberté totale de choix de la monnaie, orchestré comme une chorégraphie ludique avec lycéens.", + "Événement librodrome : deux jours alternant conférences et ateliers (install party, Ğeconomicus, Ğ(marché)), produisant des feuilles de route de productions et un rendez-vous annuel pour pousser les curseurs.", + "Écosystème technique juniste lui-même comme production collective exemplaire : blockchain Duniter migrée sous Substrate, forgerons rétribués par caisse de dons, financement par dons fléchés dépassant les subventions." + ], + "implications_outil": [ + "Implémenter un « observatoire des décisions » : journal consignant les décisions telles qu'elles sont réellement prises (qui, comment, quand), sans imposer de procédure a priori ; puis fonction de cristallisation en « protocoles de quelques phrases » éditables quand l'équipe estime que c'est mûr — l'outil observe avant de normer.", + "Anti-préfiguration : pas de templates rigides ni de « fiche prête à l'emploi » (« une chorégraphie du don ne se télécharge pas en fichier .ods ») ; l'outil doit déclencher les initiatives et les partages d'expérience sans les préfigurer ; proposer plutôt des banques de questions d'amorce (ressources, chemin, échelle, tabous, risques).", + "Rôles tournants et mémoire : modéliser les sherpiz comme rôles à rotation (dépositaires vision + mémoire) ; l'outil doit porter la mémoire collective pour rendre la rotation possible sans perte.", + "Périmètres emboîtés paramétrables : bassin de vie, rayon de vélo, groupe de communes, vallée, inter-bassins ; tableaux de bord de seuils démographiques (15 % mobiz, 3,5 % sherpiz) comme indicateurs de palier configurables et assumés comme arbitraires.", + "Observatoire auto-alimenté : produire ses propres données et indicateurs de l'économie créée « du fait de nos protocoles et nos outils » — cartographie de l'économie locale, mesure des flux, sans dépendre des données institutionnelles.", + "Visualisation des boucles d'échange : dessiner les boucles au fur et à mesure de leur constitution, rendre visibles les boucles qui ne bouclent pas et les croisements manquants, pour que chacun « identifie les trous et y pourvoie » (l'auteur signale qu'une initiative existe mais n'est pas exploitable — chantier à reprendre).", + "Séparation stricte des deux économies dans les données : ne jamais mélanger comptabilité euro et mesure en DUs ; modéliser les « nexus » comme objets explicites (investissement double, caisses de dons euros à affectations lisibles, tokens hybrides de copropriété).", + "Fléchage des contributions : chaque donateur choisit l'affectation de son don, avec tableaux de bord de suivi de chaque affectation ; répartition collective uniquement sur les dons libres (modèle Axiom-Team).", + "Transactions en gratitude : point d'équilibre annoncé en DUs par l'offreur et jeu de coefficients relatifs paramétrables (ancienneté, solde net…) entre les deux parties ; vocabulaire à respecter — offre/gratitude/mesure, jamais prix/valeur/paiement.", + "Transparence et bonne foi par conception : consignation exportable des dons et mesures (annexe « pour le questeur »), dashboard des seuils légaux (troc, bénévolat, dons en nature, legs), provisions de prudence ; rien à cacher.", + "Traitement des conflits et des départs à configurer très tôt dans le cycle de vie d'une équipe/production — l'outil doit le proposer dès la formation du groupe, pas après coup.", + "Événementiel comme primitive UX : date, début/fin, cadence, marqueur, rendez-vous annuel, feuilles de route de productions issues de l'événement ; formats jalonnés (Ğ(marché), Ğeconomicus, install party).", + "Positionnement doctrinal : libreDecision est explicitement le troisième axe du triptyque « logiciel libre ; monnaie libre ; décision collective » du librodrome ; cohérence attendue avec l'écosystème libre (licences, décentralisation, sobriété type raspberry pi).", + "Accessibilité radicale : prévoir l'intermédiation humaine pour les « démunis du numérique » — des rôles de médiation permettant à quelqu'un de participer à l'économie et aux décisions sans aucun contact avec l'écran.", + "Éviter les structures de fédération dans le modèle de données : privilégier les coopérations ponctuelles inter-groupes, la complémentarité et la visibilité mutuelle plutôt que représentation et voix unique." + ], + "citations": [ + "Couvrir les besoins, pour vivre, nourrir les plaisirs de vivre. Avant tout comment produire, comment distribuer, sans nuire.", + "Le DU n'a pas de valeur, il mesure les valeurs, c'est l'unité universelle que notre existence produit.", + "Les boucles doivent boucler. Les boucles doivent se croiser.", + "plutôt que de préfigurer comment doivent être prises les décisions, l'idée d'un « observatoire des décisions » qui consignent comment elles sont prises dans les faits, pourrait constituer une base de réflexion initiale très riche et fertile", + "Une chorégraphie du don ne se télécharge pas en fichier .ods. Elle s'improvise à plusieurs, elle se répète, elle se rate parfois, elle se réécrit selon les personnes et les lieux.", + "Logiciel libre ; monnaie libre ; décision collective." + ] + }, + { + "resume": "SejeteralO est un backend FastAPI/SQLAlchemy qui fait voter les citoyens d'une commune non pas sur un choix discret mais sur la FORME d'une courbe tarifaire de l'eau. L'objet de la décision est un vecteur de 6 paramètres continus (vinf ≥ 0 ; a, b, c, d, e ∈ [0,1], validés par Pydantic) qui définissent une courbe de Bézier cubique à deux paliers prix/volume (palier 1 : 0→vinf, tarification de la population ; palier 2 : vinf→vmax, consommations exceptionnelles). Un septième paramètre, p0 (prix au point d'inflexion), n'est jamais voté : il est DÉDUIT d'une équation de réalité — p0 = (Recettes − Σabo − Σβ₂) / Σ(α₁+α₂) — calculée dans engine/pricing.py (compute_p0) à partir des données réelles des 363 foyers de Saoû (volume_m3, statut RP/RS/PRO, importés depuis Eau2018.xls par seed.py). Les coefficients α₁, α₂, β₂ (integrals.py) linéarisent la facture de chaque foyer en fonction de p0 (facture = abo + (α₁+α₂)·p0 + β₂), ce qui rend la contrainte d'équilibre résoluble en une division : toute courbe exprimable par un citoyen est automatiquement faisable budgétairement.\n\nL'architecture sépare trois familles de paramètres : structurels fixés par l'admin (recettes cibles, abonnements abop/abos, pmax, vmax — table tariff_params), libres votés par les citoyens (les 6 paramètres de forme, table votes), et dérivés par contrainte (computed_p0, stocké avec chaque vote). L'agrégation (engine/median.py) est une médiane élément par élément des votes actifs — choisie explicitement contre la moyenne et contre la médiane géométrique parce qu'elle est « transparent and politically explainable » et que « the result is itself a valid set of Bézier parameters » : le domaine étant un produit d'intervalles, la médiane composante par composante reste dans le domaine, donc l'agrégat est lui-même une décision valide dont on recalcule le p0.\n\nLe vote est un état révisable, pas un événement : submit_vote désactive les votes précédents du foyer (is_active=False) avant d'insérer le nouveau ; un seul vote actif par foyer ; une deadline (vote_deadline) borne la période. L'authentification citoyenne repose sur un auth_code de 8 caractères lié au foyer (1 foyer = 1 voix), l'admin sur JWT avec rôles super_admin/commune_admin. Le périmètre est la commune (slug), avec registre des affectés (households) dont les attributs individuels alimentent le moteur de calcul.\n\nLe système calcule systématiquement trois objets de comparaison : (1) le statu quo — current_model.py implémente le tarif linéaire actuel (p0_linéaire = (recettes − Σabo)/Σvolumes) toujours renvoyé comme baseline ; (2) une courbe de référence publiée par l'admin (published_vinf..published_p0, published_at) qui sert d'ancre quand il n'y a pas de vote ; (3) des impacts personnalisés — compute_impacts produit une table ancien prix / nouveau prix RP / nouveau prix RS à des volumes de référence (30, 60, 90, 150, 300, 600 m³), et compute_tariff projette la facture de chaque foyer (household_bills). Le routeur votes expose publiquement la courbe médiane courante ET le « faisceau » (overlay) de toutes les courbes votées anonymisées : la décision en formation est visible en continu.\n\nLe README insiste sur la double implémentation du moteur : « Le frontend calcule localement pour un retour instantane ; le backend fait autorite » (Python numpy côté serveur, Cardano + Newton-Raphson en TypeScript côté client) — le feedback immédiat sur l'impact fait partie du geste de vote. Le code contient aussi les garde-fous d'un calcul sur le réel : sanitize des NaN/inf, volume minimal 1e-5, cas dégénérés (abonnements ≥ recettes → p0=0, aucun vote → courbe publiée ou défaut vinf=400, a..e=0.5).\n\nLe modèle décisionnel générique extractible : une classe de « décisions à paramètre continu sous contrainte » définie par (i) un espace de paramètres libres bornés, (ii) des paramètres structurels de cadrage, (iii) une fonction de résolution qui déduit les variables dépendantes d'une équation de réalité appliquée au registre des membres du périmètre, (iv) une fonction d'impact par membre, (v) un agrégateur médiane composante par composante, (vi) un statu quo et une proposition de référence toujours affichés, (vii) un vote révisable à vote actif unique jusqu'à une deadline, puis une cristallisation (publication) de la décision.", + "theses": [ + "Une décision collective peut porter sur un objet continu (une courbe, un vecteur de paramètres) et non sur un choix discret : le bulletin de vote EST une configuration complète de l'objet décidé.", + "La décision doit respecter une équation de réalité : une partie de la décision n'est pas votée mais déduite par résolution de contrainte (p0 équilibre les recettes), garantissant que toute option exprimable est automatiquement faisable.", + "La médiane élément par élément est l'agrégateur adapté aux décisions paramétriques : robuste aux votes extrêmes, politiquement explicable, et — le domaine étant un produit d'intervalles — son résultat est lui-même une décision valide du même type que les bulletins.", + "Chaque votant doit pouvoir voir l'impact personnel de toute option AVANT de se prononcer (facture projetée, comparaison au tarif actuel) : la simulation fait partie du geste de vote, d'où le moteur dupliqué client/serveur avec autorité serveur.", + "Le vote est un état révisable et non un événement : un seul vote actif par foyer, remplaçable jusqu'à la deadline, avec recalcul continu de la décision collective (médiane + p0) visible publiquement.", + "Le périmètre de décision est ancré dans des données réelles (registre de 363 foyers avec volumes et statuts importés) : la faisabilité ET la légitimité (1 foyer = 1 code = 1 voix) découlent du registre des affectés.", + "Séparation des pouvoirs paramétriques : l'autorité fixe le cadre (recettes, vmax, pmax, abonnements), les citoyens fixent la forme (6 paramètres), la contrainte de réalité fixe le reste (p0) — personne ne vote le montant total, on vote sa répartition." + ], + "concepts": [ + { + "nom": "p0 (prix d'inflexion)", + "definition": "Variable dérivée jamais votée, « toujours déduite de l'équilibre de recettes » : p0 = (R − Σabo − Σβ₂) / Σ(α₁+α₂). C'est le degré de liberté sacrifié à la contrainte de réalité ; stocké avec chaque vote (computed_p0) et recalculé pour la médiane." + }, + { + "nom": "paramètres structurels vs paramètres de forme", + "definition": "Distinction de docs/equations.html : les structurels (vmax, pmax, R, abop, abos) sont « fixés par la commune » ; les paramètres de forme (a..e ∈ [0,1], vinf) sont « votés par les citoyens ». Trois strates : cadre imposé / forme votée / variable déduite." + }, + { + "nom": "médiane élément par élément (parametric median)", + "definition": "Agrégation par médiane indépendante de chacun des 6 paramètres des votes actifs (engine/median.py), « chosen over geometric median » pour sa transparence politique et parce que le résultat est lui-même un jeu de paramètres Bézier valide." + }, + { + "nom": "coefficients α₁, α₂, β₂", + "definition": "Intégrales Bézier (integrals.py) qui linéarisent la facture d'un foyer en fonction de p0 : facture = abo + (α₁+α₂)·p0 + β₂. Cette linéarisation rend la contrainte d'équilibre global résoluble analytiquement en une seule division." + }, + { + "nom": "vote actif (is_active)", + "definition": "Chaque nouveau vote d'un foyer désactive le précédent ; un seul vote compte par foyer à tout instant. Le vote est un état courant révisable jusqu'à la vote_deadline, pas un acte irréversible." + }, + { + "nom": "courbe de référence publiée (published curve)", + "definition": "Jeu de paramètres publié par l'admin (published_vinf..published_p0 + published_at) servant d'ancre : affiché en superposition, utilisé comme décision par défaut tant qu'aucun vote n'existe. Étape de cristallisation distincte de l'agrégation vivante." + }, + { + "nom": "baseline / modèle linéaire courant", + "definition": "current_model.py calcule toujours le tarif actuel (prix unique au m³ équilibrant les mêmes recettes) et le renvoie avec chaque courbe : le statu quo est un objet de comparaison permanent, jamais absent de l'écran de vote." + }, + { + "nom": "impacts", + "definition": "compute_impacts produit pour des volumes de référence (30–600 m³) le triplet ancien prix / nouveau prix RP / nouveau prix RS ; compute_tariff projette la facture individuelle de chaque foyer (household_bills). L'effet distributif de la décision est calculé, pas supposé." + }, + { + "nom": "faisceau des votes (overlay)", + "definition": "Endpoint public renvoyant les paramètres de toutes les courbes votées actives (anonymisées) : visualisation de la dispersion des préférences autour de la médiane, la décision en formation est observable en continu." + }, + { + "nom": "auth_code foyer", + "definition": "Code de 8 caractères unique par foyer (households.auth_code) : l'éligibilité au vote découle de l'appartenance au registre des affectés du périmètre (1 foyer = 1 voix), pas d'une inscription libre." + } + ], + "mecanismes_decision": [ + "Vote paramétrique continu : le citoyen soumet un vecteur (vinf, a, b, c, d, e) borné et validé (Pydantic ge/le), c'est-à-dire une courbe complète, pas une préférence sur des options prédéfinies.", + "Résolution de contrainte à chaque soumission : compute_p0 déduit la variable d'équilibre à partir des données réelles de tous les foyers et la stocke avec le vote (computed_p0).", + "Agrégation vivante par médiane composante par composante des seuls votes actifs, recalculée à chaque consultation (GET /votes/current), avec p0 recalculé pour la médiane.", + "Vote révisable à exemplaire unique : submit_vote désactive les votes précédents du foyer (is_active=False) avant insertion ; has_voted marque la participation.", + "Deadline de scrutin (communes.vote_deadline) fixée par l'admin.", + "Publication/cristallisation : l'admin publie une courbe de référence (POST /params/publish) avec son p0 calculé et un horodatage — geste distinct de l'agrégation continue.", + "Statu quo systématique : le modèle linéaire actuel est calculé et renvoyé avec chaque réponse comme point de comparaison obligatoire.", + "Table d'impacts à volumes de référence + facture projetée par foyer : l'effet personnel et distributif de chaque option est exposé avant le vote.", + "Transparence du processus : overlay public de toutes les courbes votées (anonymisées) + médiane publique + histogramme de distribution des consommations.", + "Contrôle d'éligibilité par périmètre : auth_code lié au foyer, vérification household.commune_id == commune.id (403 sinon), JWT citoyen 4h limité au slug de la commune.", + "Séparation des rôles : super_admin (crée communes et admins), commune_admin (paramètres structurels, import des foyers, publication), citoyen (vote de forme uniquement).", + "Défauts dégradés explicites : sans vote, la décision affichée est la courbe publiée, sinon une courbe par défaut (vinf=400, a..e=0.5) — jamais d'écran vide." + ], + "production_collective": [ + "Co-construction d'un artefact continu unique (la courbe tarifaire) par superposition de contributions individuelles complètes — chaque vote est une version entière de l'artefact, l'agrégat en est la médiane.", + "Constitution d'un substrat de données commun : import CSV/XLSX des foyers réels par l'admin (identifier, statut RP/RS/PRO, volume_m3), avec preview, template téléchargeable, data_year et data_imported_at tracés.", + "Registre des affectés comme base du corps électoral : 363 foyers seedés avec codes d'authentification générés, fixtures de dev distinctes (DEVTEST2-4).", + "Double implémentation du moteur de calcul (Python numpy autoritaire côté serveur, TypeScript Cardano/Newton-Raphson côté client) maintenues synchronisées — production logicielle organisée autour du couple feedback instantané / autorité serveur.", + "CMS par commune (commune_contents : pages markdown éditables par l'admin) pour l'information et le cadrage délibératif contextuel du scrutin.", + "Multi-tenancy : plusieurs communes indépendantes sur une même instance, chacune avec son périmètre, ses paramètres structurels, ses admins (relation N-N admin_commune) et son corps électoral." + ], + "implications_outil": [ + "Introduire dans libreDecision v2 une classe de décision « paramètre continu sous contrainte » à côté des décisions discrètes, définie par un schéma déclaratif : liste de paramètres libres avec bornes (produit d'intervalles), paramètres structurels fixés par l'animateur du périmètre, et variables dérivées.", + "Moteur de réalité pluggable : une fonction pure (paramètres libres + structurels, registre des membres) → variables dérivées + impact par membre. La contrainte d'équilibre (Σ contributions = objectif) est le prototype ; l'API générique doit permettre d'autres équations de réalité.", + "Agrégateur par défaut = médiane composante par composante des contributions actives — choisir des domaines en produit d'intervalles garantit que l'agrégat est une décision valide du même type que les bulletins ; rendre l'agrégateur explicite, nommé et documenté dans l'UI (robustesse aux extrêmes = argument politique).", + "Impact personnalisé avant soumission : le bulletin doit afficher « ce que cette option change pour vous » (équivalent des household_bills et de la table d'impacts) — prévoir un calcul client instantané miroir du calcul serveur autoritaire.", + "Vote comme état révisable : un seul vote actif par membre, remplacement par désactivation (historique conservé), recalcul continu de la décision agrégée, deadline, puis étape de cristallisation/publication distincte (published_* + published_at).", + "Toujours afficher deux ancres de comparaison : le statu quo (baseline calculée sur les mêmes données) et une proposition de référence de l'animateur — jamais une décision continue « dans le vide ».", + "Périmètre = contexte + registre de membres porteurs d'attributs individuels consommés par le moteur d'impact (chez SejeteralO : volume et statut) ; éligibilité dérivée de l'appartenance au registre (code par membre), contrôle strict membre↔périmètre à chaque écriture.", + "Visualisation du faisceau : endpoint public anonymisé de toutes les contributions actives superposées à la médiane — la dispersion des préférences est une information de gouvernance de premier rang.", + "Robustesse numérique de série : bornes de validation sur chaque paramètre, sanitize NaN/inf, cas dégénérés définis (0 vote, contrainte insoluble → valeur de repli 0), volumes epsilon — un moteur de décision continue doit spécifier ses dégénérescences.", + "Séparation des pouvoirs paramétriques comme pattern d'UI : trois panneaux distincts — cadre (lecture seule pour le votant), forme (éditeur continu type courbe/curseurs), conséquences (variables dérivées + impacts, jamais éditables)." + ], + "citations": [ + "It's transparent and politically explainable", + "The result is itself a valid set of Bézier parameters", + "Compute p0 (inflection price) that balances total revenue.", + "Le frontend calcule localement pour un retour instantane ; le backend fait autorite.", + "le prix d'inflexion $p_0$ est calculé automatiquement pour équilibrer les recettes de la commune", + "Agrégation votes : médiane élément par élément des votes actifs (pas moyenne)" + ] + }, + { + "resume": "Cette première moitié du livre pose les fondations doctrinales d'une « économie du don » rendue concevable par la monnaie libre (la June/Ğ1, application de la TRM — Théorie Relative de la Monnaie). Yvv annonce un essai, pas un guide : « des repères concrets et conceptuels » pour nourrir une réflexion collective dans chaque « bassin de vie », le passage à l'action appartenant aux collectifs. Il refuse toute étiquette politique et toute dépendance aux institutions (« laisser couler la rivière »), et insiste sur la nécessité d'une finalité par groupe — la meilleure candidate étant « la réalisation d'une autonomie collective à échelle humaine ». Plusieurs groupes aux finalités différentes peuvent coexister dans un bassin sans être concurrents.\n\nContre le fantasme du don gratuit hérité de Mauss et du judéo-chrétien, il définit un don opératoire : geste de transmettre en premier lieu, sans réciprocité symétrique, mais assuré d'un contre-don (« Mon économie me le rendra »). Ce don se mesure : l'acte d'affecter un DU (Dividende Universel) au geste vécu du don, « selon ses échelles du moment, individuelles et collectives », dans « une mesure décentralisée et relativiste, pour des milliards d'estimations, toutes légitimes ». Thèse structurelle : tous les échanges sont asymétriques ; les communautés « pseudo-isolées » peuvent gérer ces asymétries en « légiférant » (édicter des règles) ou par « protocoles » (approche algorithmique qui prévoit la façon de traiter plutôt que l'issue), mais une population ouverte ne le peut pas — seule une mesure partagée (la monnaie) résout les asymétries sans ressentiment. Le don mesuré n'oblige pas celui qui reçoit, contrairement au « je ne veux rien en retour » qui, de facto, oblige. Deux expériences illustrent : « eco si nuestra » (Espagne : 1 DU = 1 h de travail dû, concertation collective des prix, corrélation calories) et « 321...DU » (tâches rétribuées par tirage dans une « boîte à gratitudes », plafond de 12 DUs contre les spécialisations, gamification).\n\nSuit un réquisitoire contre la monnaie-dette (fiat) via trois « leçons » : c'est une cavalerie (les intérêts jamais créés imposent une croissance sans option), une machine à faillites (chaise musicale où manque le quart des chaises, chômage structurel), une pyramide arbitraire qui impose ses valeurs (une poignée de personnes décide ce qui vaut ; les échelles de valeurs sont déterminées hiérarchiquement, « nous n'avons pas voix au chapitre »). D'où « à chaque monnaie son économie » : la création monétaire est un code génétique, programmatique. Les monnaies locales indexées euro ne rompent pas ce code.\n\nLa TRM offre l'alternative : « corréler directement le flux monétaire au flux de la vie humaine ». Chaque personne crée sa part égale relative de monnaie (symétrie dans l'espace-temps, non-nuisance à la Locke) ; le DU est l'invariant, seule mesure universelle possible car adossée à la vie humaine. La « convergence à la moyenne » (vers ~3.850 DUs à stabilité monétaire) réduit les écarts relatifs sans redistribution : « pas besoin de déshabiller Jacques pour habiller Paul ». Pas d'inflation monétaire possible dans le référentiel relativiste. Reste un « biais de genèse » (les anciens ont créé plus de DUs), corrigeable non en légiférant mais par un « coefficient relatif à l'ancienneté » appliqué dans l'échange avec le consentement des parties — prototype dans l'appli grateWizard. Autres coefficients proposés : progressif au volume (alourdir la captation de grosses quantités, « régulation organique et mutualisée » sans quotas ni catégories de personnes, pente glissante du jugement), relatif au solde net (solde moins DUs créés, amplitude réglable), à la pénibilité, à tout ce qui semble juste.\n\nLe « retournement sémantique » refonde le vocabulaire : je ne vends plus, je donne ; je n'achète plus, je reçois ; je ne paye plus, je mesure (un « degré de gratitude ») ; le prix devient « valeur d'équilibre » ; la monnaie devient « la mesure ». Construire ses « échelles de valeurs » propres, décentralisées par bassin de vie, sans besoin de se mettre tous d'accord : chacun pondère ses valeurs ; si incompatibilité mortifère, on crée un autre groupe et on compare les effets. Yvv imagine un outil de « budget de gratitude » (échelle de mots-clés, pourcentages ou DUs affectés) dont l'agrégation anonyme ferait apparaître un « appel d'offres » permanent, un « prod bounty » — cartographie des besoins et envies pour les équipes qui produisent. Enfin, le DU est un « commun monétaire » : dividende = subdivision égale du capital commun créé chaque jour, capital d'investissement pour amorcer les productions collectives. L'économie commence par la production distribuée (« passer la seconde »), pas par la circulation.", + "theses": [ + "Une économie du don n'est concevable que si le don se mesure : affecter un DU au geste vécu du don gère les asymétries inévitables de tout échange, sans obliger celui qui reçoit.", + "Tous les échanges sont asymétriques ; les communautés pseudo-isolées peuvent légiférer, une société ouverte ne le peut pas — seule une mesure partagée (la monnaie comme mesure) résout les asymétries à grande échelle.", + "La création monétaire est un code génétique programmatique : « à chaque monnaie son économie ». La monnaie-dette impose cavalerie, faillites structurelles et une pyramide arbitraire qui décide ce qui vaut.", + "La TRM corrèle le flux monétaire au flux de la vie humaine : le DU est le seul étalon universel invariant possible, créé par chaque personne à part égale relative (symétrie espace-temps, non-nuisance).", + "La convergence à la moyenne établit des équilibres sans redistribution ni prélèvement : « pas besoin de déshabiller Jacques pour habiller Paul ».", + "L'économie commence par la production distribuée, pas par la circulation ni l'échange : « passer la seconde » ; faire tourner des DUs en boucle ne crée pas une économie.", + "Chaque groupe doit se donner une finalité propre ; la meilleure candidate est « la réalisation d'une autonomie collective à échelle humaine ». Chercher à constituer une seule communauté est une erreur stratégique : multiplier les groupes par affinité et proximité.", + "Il n'est pas nécessaire de se mettre tous d'accord sur les valeurs : chacun pondère librement ses échelles de valeurs, l'invariant DU permet un référentiel commun à travers des milliards d'échelles différentes.", + "Préférer les protocoles (traitement algorithmique consenti dans l'échange, ex. coefficients relatifs) au fait de légiférer (édicter des règles, juger, créer des catégories de personnes — pente glissante).", + "La création monétaire est un commun : le DU est la subdivision égale du capital commun créé chaque jour, capacité collective d'investissement pour amorcer des productions." + ], + "concepts": [ + { + "nom": "don mesuré / la mesure du don", + "definition": "Geste de transmettre en premier lieu, sans réciprocité symétrique, mais assuré d'un contre-don ; mesuré par l'acte d'affecter un DU (fractions et multiples) au geste vécu du don, selon ses échelles du moment, individuelles et collectives." + }, + { + "nom": "DU – Dividende Universel", + "definition": "Unité de mesure invariante relativiste créée quotidiennement par chaque personne, corrélée à l'espérance de vie ; non pas revenu d'un capital mais subdivision égale du capital commun créé chaque jour." + }, + { + "nom": "TRM – Théorie Relative de la Monnaie", + "definition": "Théorie de Stéphane Laborde définissant la monnaie libre : création monétaire par DU, symétrie spatiale et temporelle, corrélation du flux monétaire au flux de la vie humaine." + }, + { + "nom": "bassin de vie / bassin économique", + "definition": "Périmètre vécu (oikos) où une économie se crée ; préféré à « zone » qui évoque la fermeture. C'est en passant à l'échelle d'un bassin de vie que l'expérimentation devient économique." + }, + { + "nom": "communautés pseudo-isolées", + "definition": "Collectifs à périmètre identifiable (vie communautaire, jeu, métier, pratique) capables de gérer les asymétries par règles ou protocoles internes, contrairement aux populations ouvertes." + }, + { + "nom": "asymétries", + "definition": "Propriété de la totalité des échanges : on n'échange jamais une chose pour la même chose. Leur résolution est le rôle pivot, structurel, de la monnaie dans l'échange." + }, + { + "nom": "légiférer vs protocoles", + "definition": "Légiférer = édicter une règle supposée faire loi, accompagnée d'un protocole pour la faire respecter. Les protocoles seuls sont une approche plus algorithmique qui prévoit la façon de traiter plutôt que de projeter l'issue du traitement." + }, + { + "nom": "passer la seconde", + "definition": "Image du changement de régime : sortir de la première vitesse (échanges type SEL, énergie dépensée pour avancer au pas) par un embrayage vers la production distribuée, seul commencement d'une économie." + }, + { + "nom": "retournement sémantique", + "definition": "Refonte du vocabulaire économique dans le sens vectoriel du don : je ne vends plus, je donne ; je n'achète plus, je reçois ; je ne paye plus, je mesure ; le prix devient valeur d'équilibre ; la monnaie devient la mesure." + }, + { + "nom": "degré de gratitude", + "definition": "Valeur exprimée et imprimée (dans un registre partagé distribué) lors d'une transaction ; poids, chaleur ou intensité donnés à un don selon sa propre balance et celle des autres." + }, + { + "nom": "échelles de valeurs", + "definition": "Positionnement relatif de ce qui passe devant, construit individuellement puis collectivement autour du DU, de façon totalement décentralisée sur chaque bassin de vie ; en permanence variantes." + }, + { + "nom": "coefficients relatifs", + "definition": "Formules consenties appliquées dans les échanges (ancienneté, volume, solde net, pénibilité...) qui rectifient des biais ou rétablissent des équilibres sans légiférer ni jugement de valeur moral." + }, + { + "nom": "biais de genèse", + "definition": "Écart de volume de DUs créés entre anciens et nouveaux créateurs monétaires, dû au démarrage non simultané ; corrigeable par un coefficient relatif à l'ancienneté." + }, + { + "nom": "convergence à la moyenne", + "definition": "Propriété mathématique de la TRM : en ajoutant à tous la même quantité relative chaque jour, tous les créateurs monétaires tendent asymptotiquement vers la moyenne (~3.850 DUs à stabilité), sans prélèvement." + }, + { + "nom": "symétrie dans l'espace-temps", + "definition": "Personne, en aucun lieu ni aucune époque, ne peut créer de la monnaie au détriment de l'accès monétaire d'une autre personne — interprétation de la non-nuisance de Locke." + }, + { + "nom": "commun monétaire", + "definition": "La création monétaire considérée comme un commun : tous co-propriétaires à part égale relative du capital monétaire du bassin de vie, capacité d'investissement pour produire, transformer, distribuer." + }, + { + "nom": "inversion de flux", + "definition": "Renversement du sens économique : partager un stock limité plutôt qu'écouler un maximum ; valoriser davantage la réception d'une grande quantité pour pénaliser mécaniquement les concentrations et gestes prédateurs." + }, + { + "nom": "finalité", + "definition": "Raison d'être que chaque groupe (local ou thématique) doit se définir ; la monnaie n'est pas une finalité en soi. Candidate : l'autonomie collective à échelle humaine." + }, + { + "nom": "Toile de Confiance (TdC / Web of Trust)", + "definition": "Protocole de certification des créateurs monétaires, ciment du système fiduciaire : intégrité hybride (cryptographique et humaine) rivalisant avec une autorité centrale ou une biométrie." + }, + { + "nom": "appel d'offres / prod bounty", + "definition": "Agrégation anonyme, lisible et exploitable, des échelles de valeurs individuelles : cartographie permanente des besoins et envies à laquelle les équipes qui produisent peuvent se fier." + }, + { + "nom": "boîte à gratitudes", + "definition": "Mécanisme du jeu 321...DU : rétribution des tâches par tirage aléatoire, évitant les débats sur la valeur des tâches ; plafond de 12 DUs contre les spécialisations ; gamification du fonctionnement quotidien." + }, + { + "nom": "stabilité monétaire", + "definition": "Situation atteinte quand le nombre de créateurs monétaires se stabilise : la moyenne oscille entre 3.742 et 3.925 DUs, plafond asymptotique du référentiel relativiste." + }, + { + "nom": "solidarité organique", + "definition": "Solidarité alimentée par la mesure des dons à l'échelle de toutes celles qui pratiquent une économie du don (par opposition à la solidarité mécanique qu'imposerait la réciprocité obligée)." + } + ], + "mecanismes_decision": [ + "Protocoles plutôt que législation : configurer des traitements algorithmiques qui prévoient la façon de traiter les cas, au lieu d'édicter des règles et de juger chaque issue.", + "Coefficient relatif à l'ancienneté : formule unique qui baisse le degré de gratitude pour les nouveaux créateurs et l'augmente pour les anciens, effet s'amenuisant dans le temps ; appliqué uniquement dans l'échange, avec le consentement des parties, sans jugement moral (prototype grateWizard).", + "Coefficient progressif au volume : valeur de référence qui augmente avec la quantité reçue — régulation organique et mutualisée des concentrations, sans quotas ni catégories de personnes (rejetées comme « pente glissante », « voie du jugement, voix du législateur »).", + "Coefficient relatif au solde net : soustraire les DUs créés du solde du compte ; ajuster le degré de gratitude en l'augmentant pour les soldes négatifs et le diminuant pour les positifs, avec amplitude réglable.", + "Se mettre d'accord collectivement sur une mesure qui fasse sens (ex. la pénibilité, le mérite « vrai » que le groupe partage) plutôt que sur des règles de comportement.", + "Chaque groupe définit sa propre finalité ; plusieurs groupes à finalités différentes coexistent dans un bassin de vie sans être concurrents.", + "Scission féconde plutôt que consensus forcé : si des incompatibilités s'avèrent mortifères, créer un autre groupe, comparer les effets produits sans affrontement ; orchestrer les affrontements inévitables en jeu ou en spectacle.", + "Concertation collective sur les valeurs de référence (eco si nuestra : prix maintenus « au plus bas », 1 DU = 1 heure due à la communauté, repères caloriques).", + "Rétribution des tâches par tirage au sort dans une « boîte à gratitudes » avec plafond (12 DUs) contre les spécialisations — évite les débats sur la valeur des tâches (jeu 321...DU).", + "Budget de gratitude individuel : échelle de mots-clés (produits, services, gestes, caisses) avec pourcentages ou nombres de DUs affectés ; option limite de portefeuille ou calcul de flux désiré.", + "Agrégation anonyme des échelles individuelles pour faire émerger un « appel d'offres » permanent (« prod bounty ») guidant les équipes de production.", + "Réflexion d'interopérabilité entre groupes : compatibilité et passerelles entre règles et pratiques locales (leçon des « larmes du pot de confiture » où le convoi espagnol a dû légiférer en live).", + "Caisses et cagnottes mutualisées (cagnotte d'accueil locale, cagnotte des devs et forgerons, « caisses diverses qui institueront éventuellement notre façon de fonctionner »).", + "Mesure décentralisée et relativiste : des milliards d'estimations, toutes légitimes — chacun pondère selon ses échelles sans besoin d'accord global sur les valeurs." + ], + "production_collective": [ + "La production distribuée comme acte fondateur de l'économie (« passer la seconde ») : l'économie commence quand un transfert permet lui-même de produire, pas quand on échange de l'existant.", + "Amorçage par transferts d'énergie stockée depuis l'économie euro (outils, machines, conditionnement, devises) par les personnes « affranchies » qui ont gagné leur partie dans l'euro.", + "Production collective semi-artisanale décentralisée : fonctionnement qui donne une place utile et valorisante à chacun dès qu'il le souhaite.", + "Construction de filières et de valeurs d'attraction structurantes (ex. la bière artisanale, valeur-monnaie du registre du coup de main) pour déclencher spontanément la production dans un bassin.", + "Valorisation des gestes d'amorce d'écosystème : gratifier indépendamment la présence d'un stand, la fourniture d'un stand à un autre, la livraison des tables du marché — investir par l'attention.", + "Multiplication de groupes locaux par affinité et proximité (« recruter des équipes »), entretien de relations et de protocoles avec les groupes voisins et lointains.", + "Expérience d'épicerie en circuit fermé à masse monétaire constante (chacune est épicière, vendre pour acheter) — observée avec intérêt mais jugée structurellement en impasse car nécessitant contrôles et législation.", + "Le commun monétaire (~4 millions de DUs pour 1.024 créateurs projetés dans son bassin) comme capital d'investissement pour produire, transformer, distribuer.", + "Coopérations intercommunautaires via la monnaie libre pour les communautés pseudo-isolées non repliées ; usage interne ludique (colocations, tiers-lieux) pour réduire le besoin de légiférer." + ], + "implications_outil": [ + "Vocabulaire de l'interface calqué sur le retournement sémantique : donner/recevoir/mesurer/gratitude/valeur d'équilibre au lieu de vendre/acheter/payer/prix — l'auteur voudra retrouver ses mots exacts (don, contre-don, degré de gratitude, mesure, trans·action).", + "Double référentiel d'affichage : compter en DU (relativiste) pour estimer et décider, mais consigner en Ğ1 (quantitatif) pour tout exercice comptable qui archive le passé.", + "Moteur de coefficients relatifs paramétrables appliqués aux échanges avec consentement explicite des deux parties : ancienneté, volume, solde net, pénibilité, et coefficients personnalisés — jamais imposés globalement (protocole, pas législation).", + "Indicateurs locaux de bassin : M/N, nombre de créateurs monétaires, moyenne de DUs du bassin, cap vers la stabilité monétaire (~3.850 DUs) — cf. prototype grateWizard.", + "Module « budget de gratitude » : échelles de mots-clés avec pourcentages ou DUs, option limite de portefeuille vs calcul de flux à reverser ; visualisation des écarts entre échelle théorique et réalité vécue.", + "Agrégation anonyme des échelles individuelles produisant une cartographie des besoins et envies (« appel d'offres » / « prod bounty » permanent) consultable par les équipes de production.", + "Structure multi-groupes native : chaque groupe porte sa propre finalité déclarée ; pas de fusion forcée, outils de comparaison des effets entre groupes et de scission non conflictuelle.", + "Périmètres de décision définis par chacun : « chacun définit son périmètre et ses moments » — pouvoir exclure des espaces (le foyer, le don non mesuré) de toute mesure.", + "Mécanisme de tirage au sort avec plafond (type boîte à gratitudes) pour rétribuer des tâches sans débat sur leur valeur — transposable en module de répartition gamifiée.", + "Séquences ludiques collectives pour positionner des biens et services courants sur des échelles de valeurs partagées (construction collective du référentiel).", + "Fiche d'interopérabilité par groupe : règles, coefficients et pratiques explicites pour créer compatibilités et passerelles entre groupes (éviter les « larmes du pot de confiture »).", + "Identité fondée sur une toile de confiance à intégrité hybride (certification humaine + cryptographique) plutôt qu'autorité centrale ou biométrie.", + "Caisses collectives instituantes (accueil, devs/forgerons, caisses thématiques) comme objets de première classe recevant des dons fléchés.", + "Neutralité politique et transparence opensource comme contraintes de conception : aucune étiquette, « tout est transparent et coule opensource »." + ], + "citations": [ + "« à chaque monnaie son économie »", + "« Mon économie me le rendra »", + "« couvrir nos besoins pour vivre et nourrir nos plaisirs de vivre »", + "« Je ne vends plus, je donne. […] Je n'achète plus, je reçois. […] Je ne paye plus, je mesure. »", + "« Une mesure décentralisée et relativiste, pour des milliards d'estimations, toutes légitimes. »", + "« Chercher à constituer une seule communauté est une illusion, c'est même selon moi une grosse erreur stratégique. »" + ] + }, + { + "resume": "Cette tranche finale couvre la partie opérationnelle du livre et sa conclusion. Yvv part de l'hygiène numérique comme « simple pratique élémentaire » d'un peuple affranchi, puis défend la nécessité des institutions contre la tentation anarchiste : une démocratie a besoin de symboles pour « penser et agir à grande échelle », mais structure ne signifie jamais concentration — on peut « structurer une décentralisation systématique ». Plutôt que de reprendre les institutions existantes (piège révolutionnaire), il propose de créer des « symétries » : des institutions populaires miroirs, comme une maison du peuple instituée face à une Mairie sourde, ou un « Institut de la nomination » qui qualifie postes, profils et candidats sans nommer lui-même. Les communs (l'eau en premier) doivent être protégés de la prédation des transferts de compétence ; la SCIC permet une gestion où la Mairie et chaque habitant détiennent une part et une voix. La rétribution des mandats peut être « instituée, pilotée par le peuple » — « tout cela nécessite d'être très bien outillé ».\n\nSuit une démonstration du pouvoir monétaire : depuis la loi Pompidou-Giscard (1973) et la FED (1913), la création monétaire appartient aux banques ; la monnaie-dette impose de rembourser des intérêts jamais créés — « en permanence nous devons collectivement rendre plus de monnaie qu'il n'en existe ! » — d'où prédation programmée, faillites, croissance obligatoire (« cavalerie ») et chômage régulé (NAIRU). Il ne s'agit pas de changer l'économie mais d'en « créer une » dans un autre référentiel : la monnaie libre issue de la TRM, où chaque humain forge quotidiennement un dividende universel égal, devient « le banquier de lui-même, mais aussi celui de tous », et où la masse monétaire est un commun. La Ğ1 (June), Duniter, Césium, Ğchange en sont la preuve de concept ; reste à créer les filières (dynamiques ennemies « DCD » : drainage–concentration–dépendance ; dynamiques amies : irrigation, décentralisation, autonomie).\n\nSur la légitimité : celle des élus n'est que théâtre (ordre du jour possédé par le Maire/Président, « délibérations » factices, « panoplie de l'esquive ») ; le peuple, lui, est parfaitement en droit « non pas de réclamer » une démocratie « mais de la déclarer ». Ne compter ni sur les élus ni sur les élections ; il propose le vote « NUL ! » massif avec consignation populaire des dépouillements publics. Les « feux de la rampe » archivent contradictions, nominations et liaison discours/votes ; la punition démocratique est le déshonneur et la mémoire vive. Le lobbying populaire oppose aux textes de loi « un jugement construit par le peuple » ; le boycott cible le « haut de bilan ». Le RIC est disqualifié par son clivage binaire (« tir à la corde »), mais un « RIC permanent » interne peut structurer la démocratie naissante ; le premier RIC à lancer serait une constituante citoyenne non élue (ateliers constituants de Chouard, tirage au sort), avec une constitution minimaliste sous rasoir d'Ockham, « écrite au fil de l'eau ». Les représentants sont ambassadeurs et négociateurs, jamais décideurs : mandats spécifiques, sous contrôle, non attachés à un statut.\n\nMenaces : agressions médiatiques, noyade administrative, verrous législatifs, leurres (la « petite concession »), épuisement, disruption (répondre par l'épochè), et surtout le contrôle du peuple par le peuple (pass sanitaire) qui « sonnerait le glas » de toute société. Expansion : pas de conquête mais adhésion volontaire, agrégation en grappes, rendre désirable — « une démocratie ne fait pas la quête ». Causes sans piège : la vitalité et l'émancipation ; exemple de la « zone franche active » accueillant les peuples en guerre. Culture : « armer les esprits » (histoire, philosophie, logique, rhétorique, sciences), construire ses jugements, mode expérimental (tester, mesurer, droit de se tromper), éducation populaire multi-formats. L'intelligence collective est à développer sans nier les intelligences individuelles (expérience des échecs de la fouloscopie : vote par coup, modalités votes secrets vs ouverts, question ouverte du poids des compétences aguerries).\n\nLe final donne la feuille de route des développeurs : « concevoir et développer un système visuel de décisions et d'acquisition collective des expériences », « définition incrémentielle de la décision, des droits et des usages », « token d'intégrité et de viabilité », dataviz en direct, IA réorientée vers la recherche des biais décisionnels, logiciels libres et chiffrés, économie relativiste. Le numérique sert à dépasser les échelles locales et les clivages binaires — mais « pour faire société, le seul moyen est de physiquement se rencontrer ». Pas de guide sinon soi-même ; leadership toléré s'il est « circonscrit, maîtrisé et révocable ». Clôture : « Alors qui ?… rêve une démocratie et la voit ici-bas ? Personne ? ».", + "theses": [ + "Une démocratie a besoin d'institutions (symboles pour penser le grand volume et la longue durée), mais instituées et habitées par le peuple : structure ne veut pas dire concentration, on peut « structurer une décentralisation systématique ».", + "Ne pas reprendre ni rectifier les institutions existantes (piège révolutionnaire) : créer des « symétries », institutions miroirs populaires dont on ne peut perdre « us et usufruit ».", + "La légitimité des élus est auto-accordée et théâtrale ; le peuple est légitime pour déclarer — et non réclamer — une démocratie, sans compter sur les élus ni les élections.", + "La création monétaire est le premier des pouvoirs : la monnaie-dette programme mathématiquement prédation, croissance obligatoire (cavalerie) et chômage requis (NAIRU) ; la seule démarche est de créer une économie dans un autre référentiel monétaire (TRM/monnaie libre), pas de remplacer l'existante.", + "Le clivage binaire (référendum, RIC, tir à la corde) est à proscrire en toutes circonstances : la décision démocratique repose sur un jugement construit collectivement.", + "Les représentants sont ambassadeurs et négociateurs, jamais décideurs : décisions opérationnelles confiées sous contrôle, dans le cadre de mandats spécifiques non attachés à un statut.", + "Le contrat social est un ouvrage collectif de longue durée : sacralisé mais jamais immuable, minimaliste (rasoir d'Ockham), mesuré en continu, révisable, cherchant « l'élégance algorithmique de lois dynamiques qui s'adaptent ».", + "Une démocratie ne conquiert pas : elle se rend désirable et s'étend par adhésion et agrégation, en grappes de petites échelles en liaison permanente ; « une démocratie ne fait pas la quête ».", + "Le numérique libre, chiffré et décentralisé est la condition de possibilité de la décision du peuple à grande échelle (« toi seul pourras coder, une démocratie ») — mais faire société exige la rencontre physique.", + "La pire menace est l'introduction dans les mœurs du contrôle du peuple par le peuple (pass sanitaire) : elle sonnerait le glas de toute tentative de refaire société.", + "L'intelligence collective est la capacité sur laquelle miser pour la décision, mais elle doit être développée sans nier les intelligences individuelles ni les compétences aguerries.", + "Seules causes mobilisatrices sans piège : la vitalité (diversité, joie de vivre) et l'émancipation (Autonomie – Équilibre – Liaison, démarche d'un peuple adulte)." + ], + "concepts": [ + { + "nom": "symétrie / institutions symétriques", + "definition": "Quand une institution établie refuse ou méprise un besoin du peuple, celui-ci institue lui-même une institution miroir qu'il habite et anime (maison du peuple face à la Mairie, Institut de la nomination face à l'appareil d'État), qui négocie ensuite selon son poids, le volume des personnes qui l'habitent." + }, + { + "nom": "Institut de la nomination", + "definition": "Institution symétrique nationale qui, sans pouvoir de nommer, qualifie les postes, les profils pertinents, les candidats et les nominations, pour dénoncer collusions, pantouflages et conflits d'intérêts." + }, + { + "nom": "feux de la rampe", + "definition": "Pratique de mise en lumière spectaculaire et rigoureuse (travail d'archiviste + travail rhétorique) des contradictions, promesses bafouées et doubles langages des élus et médias ; liaison entre discours tenus et votes effectués ; la sanction démocratique est le déshonneur et l'entretien d'une mémoire vive." + }, + { + "nom": "panoplie de l'esquive", + "definition": "Arsenal des élus pour éluder la décision partagée : droit de réserve (« devoir latent de la fermer »), ordre du jour possédé par le Président/Maire, délibérations factices, rejet de responsabilité via les « compétences », la solidarité territoriale ou la faute de l'État." + }, + { + "nom": "monnaie-dette", + "definition": "Référentiel monétaire actuel où toute monnaie naît d'un crédit dont les intérêts ne sont jamais créés : il faut collectivement rembourser plus de monnaie qu'il n'en existe, d'où prédation, faillites et croissance monétairement obligatoire." + }, + { + "nom": "cavalerie", + "definition": "Fuite en avant exponentielle de création monétaire nouvelle nécessaire pour éviter la faillite générale du système de monnaie-dette ; source génétique de la religion de la croissance." + }, + { + "nom": "TRM / monnaie libre", + "definition": "Théorie Relative de la Monnaie : met chaque être humain au cœur de la création monétaire, sans discrimination géographique ni calendaire ; garantit une symétrie dans l'espace et le temps via trois libertés fondamentales (accès aux ressources, produire des valeurs, échanger/estimer les valeurs) plus la liberté de choisir sa monnaie." + }, + { + "nom": "dividende (DU)", + "definition": "Part de monnaie créée chaque jour par chacun, invariante relativement à la masse totale et au nombre d'utilisateurs ; unité de mesure et valeur de référence universelle, relativiste et tangible ; chacun devient « le banquier de lui-même, mais aussi celui de tous »." + }, + { + "nom": "toile de confiance", + "definition": "Mécanisme d'authentification des créateurs de monnaie libre géré par Duniter (forge des DU journaliers sur blockchain)." + }, + { + "nom": "DCD : drainage – concentration – dépendance", + "definition": "Acronyme des trois dynamiques économiques ennemies, contre lesquelles se battre en promouvant les dynamiques inverses : irrigation, décentralisation, autonomie." + }, + { + "nom": "RIC permanent", + "definition": "Réappropriation interne du référendum d'initiative citoyenne : une démocratie naissante peut penser et mettre en œuvre une sorte de RIC permanent pour structurer par elle-même son fonctionnement — tout en refusant le RIC institutionnel pour son clivage binaire (« tir à la corde »)." + }, + { + "nom": "petite concession", + "definition": "Technique de pouvoir : introduire une dérive flagrante dans un texte, la retirer sous pression et consacrer ce retrait comme victoire démocratique, pour faire avaler le reste du texte." + }, + { + "nom": "zone franche active", + "definition": "Territoire démocratique auto-déclaré où les peuples dits ennemis peuvent se rencontrer et discuter sous protection populaire ; rôle d'hôte structuré, soutenable et de qualité, avec pacte de non-agression négocié." + }, + { + "nom": "définition incrémentielle de la décision, des droits et des usages", + "definition": "Principe de conception numérique : la décision, les droits et les usages ne sont pas préfigurés mais définis progressivement, au fil des besoins, à l'image de la constitution écrite « au fil de l'eau » sous rasoir d'Ockham." + }, + { + "nom": "token d'intégrité et de viabilité", + "definition": "Jetons de mobilisations politiques citoyennes : primitive numérique proposée aux développeurs pour outiller la confiance et la mobilisation dans le système de décision du peuple." + }, + { + "nom": "espaces de non-pouvoir", + "definition": "Tous les lieux et moments où le pouvoir ne s'exerce pas ; un pouvoir populaire consiste à leur donner de l'ampleur et de la densité." + }, + { + "nom": "vents contraires", + "definition": "Dynamiques inhibitrices internes au peuple lui-même : usure, contentement, compromis, impatience, reproduction mimétique des structures que l'on a voulu quitter." + }, + { + "nom": "peuple adulte", + "definition": "Peuple qui construit sa société sans juger les vies privées, cultive l'intelligence et les connaissances, construit ses jugements, ne consomme plus les opinions, et s'en remet à sa propre sagesse collective pour décider de son sort." + } + ], + "mecanismes_decision": [ + "Institutions symétriques : instituer un miroir populaire quand l'institution établie résiste (maison du peuple face à la Mairie ; Institut de la nomination qui qualifie postes, profils, candidats sans nommer) ; leur poids de négociation dépend du volume des personnes qui les habitent.", + "Gestion des communs via SCIC : une part et une voix pour la Mairie et pour chaque habitant qui le souhaite ; transfert de la part communale au moment de l'abandon de compétence (ex. eau, butoir 2026).", + "Rétribution des mandats « instituée, pilotée par le peuple, et non pas déléguée » — « tout cela nécessite d'être très bien outillé ».", + "Représentation sans décision : ambassadeurs et négociateurs ; « les décisions opérationnelles de terrain peuvent être confiées, sous contrôle, dans le cadre de mandats spécifiques, et ne pas être attachées à un statut ».", + "Constituante citoyenne non élue (ateliers constituants, tirage au sort, leitmotiv de fixer les limites des pouvoirs) ; constitution minimaliste sous rasoir d'Ockham, écrite au fil de l'eau, sans préfigurer les besoins.", + "Refus du clivage binaire référendaire (RIC = tir à la corde, un gagnant un perdant), mais « RIC permanent » interne pour structurer son propre fonctionnement ; premier RIC institutionnel à lancer : une constituante citoyenne.", + "Vote « NUL ! » massif au second tour (participation >80%, <20% de voix exprimées) + consignation populaire des dépouillements publics : compter, noter les inscriptions, diffuser — pour créer une trace indélébile de déchéance de légitimité électorale.", + "Feux de la rampe : archivage rigoureux (archiviste + rhétorique) des contradictions ; liaison systématique entre discours tenus et votes effectués (encore à main levée) ; punition par le déshonneur proportionné et la mémoire vive.", + "Lobbying populaire : cibler les moments clés de la décision législative, éclairer les textes avec le tissage des liaisons d'affaires, leur opposer « un jugement construit par le peuple et non les élus » ; think tanks maison à petite échelle, en liaison les uns avec les autres.", + "Boycott populaire ciblé sur le haut de bilan (valorisation boursière, fusions-acquisitions), campagnes de communication synchronisées sur un moment précis.", + "Service public : réflexion et décisions de gestion confiées au peuple en deux cercles — toutes les personnes impliquées en premier lieu, toutes celles qui s'intéressent au domaine en second lieu ; financement et fonctionnement à l'initiative du terrain.", + "Décision par agrégation de votes type fouloscopie/échecs : chaque option est un coup, le plus voté l'emporte ; distinguer les modalités votes « secrets » (sans voir le choix des autres) et « ouverts » ; question ouverte : donner leur juste poids aux compétences aguerries.", + "Contrat social vivant : protocoles de décision expérimentés, contrat posé comme préalable aux conflits, observation et mesure constantes, révisions et ajustements sans embonpoint.", + "TRM : chacun est décisionnaire « à proportion, de l'affectation des devises créées » — le capital monétaire est un commun.", + "Leadership accepté seulement s'il est « circonscrit, maîtrisé et révocable », égos contenus ; jamais de guide ni de parti — fédération par organes institués et par une devise." + ], + "production_collective": [ + "Créer une économie complète en monnaie libre : penser filières, approvisionnements, besoins directs et intermédiaires, bouts de chaînes et relations (conflictuelles ou synergiques) avec la zone économique euro ; se réapproprier l'estimation relativiste des valeurs (ne plus « consommer des prix »).", + "SCIC (Société Coopérative d'Intérêt Collectif) pour gérer les communs comme l'eau : forme sociale existante, une part/une voix par habitant et par collectivité.", + "Maison du peuple communale auto-instituée ; assemblée communale ou conseils de quartier complétant le conseil municipal mais « dotés de prérogatives opérationnelles ».", + "Zone franche active : capacité d'accueil connue et organisée, rôle d'hôte structuré, soutenable et de qualité pour les peuples victimes de guerre, mémoire vivante des peuples en exil.", + "Réseaux locaux mettant « activement face à face, en direct, des besoins et des ressources » (Solaris, groupes Telegram/Signal/Matrix) — limités faute d'outils pour « qualifier » et « mapper ».", + "Éducation populaire multi-formats pour armer les esprits : conférences gesticulées, ateliers, cafés-philo, capsules vidéo, enquêtes, livres — chacun transmet ce qu'il maîtrise, sous l'égide du plaisir et de l'envie.", + "Développement bénévole des outils de la monnaie libre (Duniter, Césium, Ğchange, forgerons de nœuds), avec retour sur investissement souhaitable via l'économie créée.", + "Travail collectif d'archiviste-rhétoricien pour les feux de la rampe (consignation des votes, montages, intégrales).", + "Expansion en réseau : unités de petite échelle qui s'agrègent « en grappes, elles-mêmes en liaison permanente », fédérées par des organes institués et une devise démocratique.", + "Système immunitaire collectif : « concevoir et mettre en œuvre un système immunitaire pour un nouvel écosystème politique et économique »." + ], + "implications_outil": [ + "Mandat fondateur explicite pour libreDecision : « concevoir et développer un système visuel de décisions et d'acquisition collective des expériences » — heuristique et dataviz « dans la perspective de traitements visuels et en direct ».", + "Définition incrémentielle de la décision, des droits et des usages : périmètres, rôles et processus non préfigurés, définis au fil de l'eau, versionnés, minimalistes (rasoir d'Ockham).", + "Proscrire le binaire oui/non (anti-RIC, anti tir à la corde) : modes de délibération qui construisent un jugement, dépassent les clivages binaires, cherchent d'autres référentiels ; le numérique existe précisément « pour dépasser les échelles locales […] et pour dépasser les clivages binaires ».", + "Primitives de confiance : « token d'intégrité et de viabilité, jetons de mobilisations politiques citoyennes » ; toile de confiance (authentification humaine type Ğ1).", + "IA strictement repositionnée « au profit de la recherche des biais, dans les traitements décisionnels » — jamais d'anticipation algorithmique prescriptive.", + "Deux cercles de légitimité paramétrables par périmètre de décision : les personnes impliquées (premier lieu) puis toutes celles qui s'intéressent au domaine (second lieu).", + "Mandats outillés : spécifiques, à périmètre défini, sous contrôle, révocables, détachés de tout statut ; rétribution des mandats pilotée par le collectif (« nécessite d'être très bien outillé » = fonction de l'outil).", + "Modalité de vote secret vs ouvert comme paramètre de chaque décision (leçon fouloscopie) ; chantier ouvert sur la pondération des compétences aguerries sans nier l'égalité de principe.", + "Mémoire vive et traçabilité : consignation des votes, liaison engagements/discours/décisions, archivage des positions (feux de la rampe internes) ; le déshonneur documenté comme seule sanction.", + "Contrat social du collectif comme document vivant : sacralisable mais révisable, observé et mesuré en continu, révisions sans embonpoint ; support d'un « RIC permanent » interne où la garantie porte sur qui rédige l'initiative.", + "Intégration économie/monnaie libre : DU comme unité de mesure, affectation des devises décidée à proportion par chacun, mise en face-à-face qualifiée et « mappée » des besoins et des ressources (manque identifié des collectifs existants).", + "Exigences techniques doctrinales : open source, chiffrement, décentralisation structurée (empêchement des concentrations), aucune donnée personnelle centralisée — hygiène numérique comme condition.", + "Architecture en grappes fédérées : agrégation d'unités petite échelle en liaison permanente, jamais une plateforme centrale ; l'outil sert la logistique et la décision, pas le lien social — « pour faire société, le seul moyen est de physiquement se rencontrer ».", + "Boucles expérimentales intégrées : « évaluer les expériences, avec la curiosité du chercheur, et valoriser le droit de se tromper » ; tester de façon cyclique et agile, « ne pas détenir de vérité, plutôt mesurer »." + ], + "citations": [ + "« Tu ne veux pas – Ô Institution – tu ne peux pas ? Tant pis, je crée une symétrie »", + "En permanence nous devons collectivement rendre plus de monnaie qu'il n'en existe !", + "Dès lors qu'on se met à désirer une démocratie […] nous sommes parfaitement en droit et en position, non pas de la réclamer, mais de la déclarer.", + "Une démocratie ne fait pas la quête.", + "Concevoir et développer un système visuel de décisions et d'acquisition collective des expériences.", + "Derrière l'écran fumé, tel un bon génie / toi seul pourras coder, une démocratie." + ] + }, + { + "resume": "Cette tranche ouvre « Mon nom est personne » par un prologue consacré à l'anonymat de l'auteur, geste politique délibéré : le livre est « orphelin », nul ne peut se l'approprier ni l'incarner, afin que seul le contenu compte — « ce livre n'est à personne, ce qu'il devient appartient à tout le monde ». L'édition préfigure la doctrine : licence Creative Commons, original sur IPFS, portefeuille June (clé Ğ1) pour valoriser le PDF en DUs, pseudo « @personne » offert à l'adoption. L'auteur revendique une légitimité construite par le travail de conception, non par un statut, et pratique la « neutralité axiomatique » dans l'observation (pas dans les propositions). L'introduction constate l'absence de lieux pour une réflexion collective de société : la politique locale est un huis clos, les Conseils sont désertés car ils ne s'adressent pas à la population ; « on ne délègue pas, on abandonne ». Conclusion pénible : nous devons nous-mêmes changer la donne — or « je ne suis… personne ». La « Trame d'un raisonnement » pose les outils. Le ğ(x) (« G libre de x », clin d'œil à l'incomplétude de Gödel, hommage à Stéphane Laborde, à la TRM et à la monnaie libre) désigne une proposition non encore observée mais dont on ne peut dire qu'elle n'existe pas : c'est le statut du projet du livre, « un ğ libre de (x) politique ». Le chapitre « Référentiel » fait du changement de référentiel la clé de la créativité politique : comme en physique (Ptolémée→Newton→Einstein), un référentiel n'est pas le monde, il en détermine une représentation ; toute impasse est un appel à une nouvelle géométrie. « Bascule » décrit une société en déséquilibre/passage et exige que tout sujet stratégique (géo-ingénierie, ADN, pensée et mémoire) soit mis « sous les feux de la rampe populaire » et soumis à la « sagesse populaire ». « Peur et culpabilisation » analyse le couple moteur de l'asservissement : la peur soumet à une protection, la culpabilisation transforme la peur individuelle en contrôle des sujets par eux-mêmes ; culture ancestrale du sauveur dont il faut s'affranchir jusqu'à l'idée même. Graeber et Wengrow attestent que la capacité d'imaginer des formes très variées de sociétés est intrinsèque à la nature humaine. Trois fausses pistes : laisser peur et culpabilisation régir nos vies, croire à une solution simple, croire à une personnalité providentielle. « Illusion et coercition » : depuis Machiavel, le pouvoir n'a d'autre propos que lui-même et use de deux familles de moyens, la force et l'illusion (Goebbels : bourrage de crâne ; Bernays : fabrication de croyances par l'émotion ; Chomsky : fabrique du consentement). Dans le référentiel du rapport de forces, les peuples perdent toujours (Kronstadt, mouvement libertaire espagnol — l'autogestion mutualiste et coopérative la plus aboutie d'Europe, écrasée —, Révolution française où le peuple n'a rien obtenu). L'État est « monopole de la force et de l'imposition » ; l'issue n'est pas la révolution mais le changement de référentiel. « Plaisir et rêve » propose le couple d'émancipation symétrique : le rêve éveillé, tenace, distinct de l'espoir (fixé sur un résultat) et de l'illusion (tromperie) ; l'utopie est redéfinie comme « le projet qui n'a pas encore trouvé son lieu dans l'espace-temps ». « Clivages binaires » : le clivage binaire tue la réflexion, le parti politique tue la politique ; il faut « raisonner curseur » — structurer toute question comme un moniteur de sono, en grandes lignes graduées et boutons de réglage, évaluer sur plusieurs critères dont les volumes, ajuster, recommencer (démonstration par la transposition migrants/touristes : question de volume et de qualité d'hospitalité, bascule quand la population touristique dépasse la moitié). Refus catégorique du religieux comme variable structurelle de la décision. La « Métaphore fractale » lit les âges d'une civilisation comme ceux d'une vie : peuple enfant (religions), peuple ado (depuis le XVIe siècle — c'est notre présent, gouvernants et gouvernés), peuple adulte — hypothèse jamais documentée, croyance que l'on peut raisonnablement décider d'adopter. Le peuple adulte « conçoit et bâtit sa capacité à la décision » : il ne délègue plus, il mandate ; il n'est pas consulté, il est instruit ; il négocie chaque virage ; chaque décision emporte une décision connexe d'allocation de ressources ; il décide qui décide, recrute et mandate les compétences à chaque jalon, des équipes pour réaliser et pour mesurer ; il contrôle le pouvoir qu'il désigne car « la confiance exclut le contrôle » ; la confiance est à géométrie variable, accordée progressivement sur le vécu ; l'autonomie n'est ni repli ni autarcie : elle négocie les interdépendances et évite les dépendances unilatérales.", + "theses": [ + "La délégation de pouvoir telle que pratiquée est un abandon ; aucune personnalité providentielle, aucun sauveur ne changera la donne — le peuple doit se donner lui-même, en tout premier lieu, les moyens de décider.", + "L'asservissement fonctionne par le couple peur + culpabilisation ; l'émancipation exige le couple symétrique plaisir + rêve, inscrit dans la durée.", + "Toute stratégie pensée dans le référentiel du rapport de forces avec l'État (monopole de la force et de l'imposition) est vouée à l'échec ; l'émancipation passe par un changement de référentiel, pas par une révolution.", + "Le clivage binaire tue la réflexion et le parti politique tue la politique ; il faut remplacer le pour/contre par un raisonnement en curseurs multi-critères (dont les volumes).", + "Notre civilisation est un « peuple ado » ; devenir un « peuple adulte » — qui prend la décision, mandate, est instruit, négocie chaque virage, contrôle le pouvoir qu'il désigne — est un ğ(x) politique : non encore observé, mais dont on ne peut dire qu'il n'existe pas.", + "Les décisions stratégiques (santé publique, géo-ingénierie, ADN, pensée et mémoire) doivent être mises sous les feux de la rampe populaire et soumises à la sagesse populaire, jamais retranchées dans un huis clos.", + "La confiance exclut le contrôle et réciproquement : le peuple contrôle le pouvoir qu'il mandate (car il ne lui fait pas confiance) et ne se contrôle pas lui-même (car il se fait confiance).", + "L'anonymat et l'impersonnalité (« mon nom est personne ») protègent le contenu de l'appropriation, de l'ad hominem et de la figure du sauveur : on juge les idées, jamais leur auteur.", + "La capacité d'imaginer et d'expérimenter des formes très variées de sociétés est intrinsèque à la nature humaine (Graeber & Wengrow) — nous l'avons oubliée, pas perdue.", + "Un référentiel n'est pas le monde : quand on croit être dans une impasse, c'est un appel à une nouvelle géométrie." + ], + "concepts": [ + { + "nom": "ğ(x) / G libre de x", + "definition": "Symbole (référence à l'incomplétude de Gödel) désignant une valeur ou proposition non encore observée mais dont on ne peut pas dire qu'elle n'existe pas ; l'utopie de société est « un ğ libre de (x) politique »." + }, + { + "nom": "changement de référentiel", + "definition": "Outil capital (emprunté à Stéphane Laborde et à l'histoire des sciences) : un référentiel n'est pas le monde, il en détermine une représentation et une mathématique de ses lois dynamiques ; en changer révèle des lois invisibles et dissout les impasses ; ce n'est pas voir une facette de plus, c'est voir toutes les facettes autrement." + }, + { + "nom": "bascule", + "definition": "Triple sens : chute (vertical), équilibre fragile (balance, horizontal), oscillation rythmique ; désigne le passage de société en cours, mutation plus que transformation, sans le fatalisme du « mur »." + }, + { + "nom": "peur et culpabilisation", + "definition": "Couple le plus puissant de l'asservissement : la peur soumet à une protection, la culpabilisation referme le ciseau moral en faisant passer de la peur individuelle à la peur collective, avec contrôle des sujets par eux-mêmes." + }, + { + "nom": "plaisir et rêve", + "definition": "Couple symétrique de l'émancipation : le plaisir comme ressort primitif moteur et créateur, allié au rêve pour rendre possible et agréable la durée." + }, + { + "nom": "rêve (de jour)", + "definition": "Rêve éveillé, tenace, que l'on peut concevoir et mettre en œuvre ; projette un cheminement lent et progressif autant qu'un résultat ; distinct de l'espoir (fixé sur un résultat qui ne dépend pas de nous) et de l'illusion (tromperie)." + }, + { + "nom": "utopie (redéfinie)", + "definition": "Non pas l'atopie (sans lieu) mais « le projet qui n'a pas encore trouvé son lieu dans l'espace-temps » ; sujette à réalisation, elle se transforme en rêve collectif, un encours." + }, + { + "nom": "clivage binaire", + "definition": "Fléau qui tue la réflexion : simplification outrancière et amalgames pour former deux camps (pour/contre, pro/anti) où le seul comportement permis est de prendre parti ; à distinguer des polarités, réelles, qu'il faut apprivoiser et concilier." + }, + { + "nom": "raisonner curseur", + "definition": "Structurer une question politique à la façon d'un moniteur de sono : grandes lignes graduées (critères) et boutons de réglage ; évaluation multi-critères incluant les volumes ; déplacer les curseurs, évaluer, ajuster, recommencer." + }, + { + "nom": "métaphore fractale des âges", + "definition": "Lire les âges d'une civilisation comme ceux d'une vie humaine, motif reproduit à l'échelle supérieure : peuple nouveau-né (paganismes), peuple enfant (religions monothéistes), peuple ado (depuis le XVIe, notre présent), peuple adulte (hypothèse)." + }, + { + "nom": "peuple adulte", + "definition": "Peuple qui conçoit et bâtit sa capacité à la décision et assume ses décisions au contact direct de leurs impacts : il ne délègue plus, il mandate ; il n'est pas consulté, il est instruit ; il ne cède pas un consentement, il négocie ; il construit son propre système d'autorité." + }, + { + "nom": "feux de la rampe populaire", + "definition": "Exigence de transparence radicale : mettre à la vue et au su de tous les programmes et décisions touchant aux fondements (géo-ingénierie, ADN, pensée, mémoire), avec instruction populaire au-delà de l'information." + }, + { + "nom": "sagesse populaire", + "definition": "Capacité du plus grand nombre à arbitrer bénéfices et risques ; les décisions des Comités de direction et les décisions stratégiques de santé publique doivent en subir le joug." + }, + { + "nom": "confiance / contrôle (oxymore)", + "definition": "« La confiance exclut le contrôle, et réciproquement » ; la confiance est à géométrie variable, délicate et vivante, repose sur le vécu, s'accorde progressivement et ne se donne jamais a priori." + }, + { + "nom": "autonomie", + "definition": "Leitmotiv du peuple adulte ; ni repli sur soi ni autarcie ; ne nie pas l'interdépendance mais la négocie pour son bénéfice et évite les dépendances unilatérales." + }, + { + "nom": "monnaie libre / TRM / DU", + "definition": "Théorie Relative de la Monnaie de Stéphane Laborde et sa définition d'une monnaie libre (Ğ1/June, Dividende Universel) ; domaine stratégique de la création monétaire, référence fondatrice du livre." + }, + { + "nom": "aubaine / factorisation collective", + "definition": "Chez Proudhon : la valeur décuplée du travail collectif (« factorisation collective ») que le propriétaire s'approprie ; « la propriété c'est le vol » vise ce vol-là et l'origine par la force, pas la propriété du foyer." + }, + { + "nom": "neutralité axiomatique", + "definition": "Pratique d'observation et d'analyse affranchie de l'aspiration idéologique — appliquée aux constats, pas aux propositions." + }, + { + "nom": "servitude volontaire + variable de Milgram", + "definition": "Acceptation de l'asservissement (La Boétie), complétée par la soumission culturelle à une autorité cohérente et reconnue (Milgram) et les dynamiques de conformité de groupe (Asch) — à connaître pour ne pas les laisser aux mains des dirigeants, sans en tirer l'incapacité populaire à décider." + } + ], + "mecanismes_decision": [ + "Mandater au lieu de déléguer : recruter et mandater les personnes compétentes à chaque étape et jalon de la décision, avec des équipes pour réaliser et des équipes pour mesurer.", + "Méta-décision : décider par qui et comment sera prise la décision, comment et par qui elle sera évaluée et assumée.", + "Instruction populaire préalable : le peuple n'est pas consulté, il est instruit — les options doivent être présentées, qualifiées, évaluées et illustrées par les meilleures compétences dans différentes disciplines.", + "Négociation continue : le peuple ne cède pas un consentement, il négocie chaque virage.", + "Décision connexe obligatoire sur les ressources : chaque décision implique une allocation de ressources, donc une décision incontournable sur ces ressources ; assumer les décisions au contact direct de leurs impacts.", + "Contrôle du pouvoir mandaté : le peuple contrôle le pouvoir qu'il désigne et affecte, dans une mesure qu'il aura décidée ; il ne se contrôle pas lui-même (confiance exclut contrôle).", + "Raisonnement curseur : décomposer toute question en grandes lignes (critères : impacts logistiques, économiques, qualité d'accueil, qualité de vie, valeur symbolique, culturelle…) et curseurs de réglage incluant les volumes ; cycle déplacer-évaluer-ajuster-recommencer ; empêche amalgames et formation de camps.", + "Feux de la rampe populaire : transparence obligatoire, à la vue et au su de tous, sur tout sujet stratégique (géo-ingénierie, ADN, pensée, mémoire) ; les décisions des Comités de direction subissent le joug de la sagesse populaire.", + "Soumission des décisions stratégiques de santé publique à la sagesse populaire, avec consultation du peuple sur les différentes options qualifiées.", + "Confiance à géométrie variable : accordée progressivement, partiellement selon les contextes, fondée sur le vécu, jamais donnée a priori ni à l'aveugle.", + "Changement de référentiel comme méthode : toute impasse décisionnelle est un appel à reformuler le problème dans un autre référentiel.", + "Suggestion de fusionner les scrutins présidentiels et communaux (moments de mobilisation politique aux portées de réflexion différentes).", + "Laïcité structurelle : aucune considération religieuse ne doit devenir variable structurelle de la décision de société.", + "Refus du référentiel du rapport de forces : aucune stratégie de renversement par la force ; « Cessons nos illusions. Mais ne provoquons aucune coercition. »" + ], + "production_collective": [ + "La monnaie libre Ğ1/June et le Dividende Universel (TRM de Stéphane Laborde) comme infrastructure de valorisation — le livre lui-même est valorisé en DUs via un portefeuille June.", + "Le commun numérique anti-appropriation : œuvre orpheline sous Creative Commons, stockée sur IPFS, auto-éditée, dont « ce qu'il devient appartient à tout le monde » ; le pseudo @personne offert à l'adoption collective.", + "L'autogestion mutualiste et coopérative, référence historique au « moviment libertari » espagnol (l'aventure la plus aboutie d'Europe) et aux soviets originels comme cellules locales du peuple.", + "La factorisation collective du travail (Proudhon) : la valeur décuplée de chaque paire de bras appartient au collectif, son appropriation privée est l'« aubaine » volée.", + "Les médias indépendants et libres (webTV, podcasts, PeerTube) comme contributions nécessaires bien que marginales.", + "Chacun définit sa propre stratégie et ses plans d'action, à sa propre échelle — pas de stratégie centrale imposée, le livre est une graine.", + "La pratique populaire de la philosophie et de la poésie : créer ses propres maximes et synthèses, sans en faire des psaumes.", + "Le rêve collectif comme « encours » : l'utopie devenue objet de réalisation crée une trajectoire qui n'est pas celle que les probabilités dictent." + ], + "implications_outil": [ + "Remplacer le vote binaire pour/contre par une UX de curseurs multi-critères : chaque question décomposée en grandes lignes graduées et boutons de réglage (dont les volumes/quantités), avec cycle itératif déplacer-évaluer-ajuster-recommencer — jamais de simple oui/non par défaut.", + "Modéliser la décision comme un processus jalonné à rôles séparés et mandatés : instruction, décision, réalisation, mesure/évaluation — avec la méta-décision (qui décide de quoi, qui évalue, qui assume) elle-même décidable par le collectif.", + "Attacher obligatoirement à chaque décision une allocation de ressources explicite et le suivi de ses impacts (le collectif assume ses décisions au contact direct des impacts).", + "Imposer une phase d'instruction avant tout arbitrage : options présentées, qualifiées, évaluées et illustrées par des compétences pluridisciplinaires — l'utilisateur est instruit, pas seulement consulté.", + "Outiller le contrôle des mandatés (reddition de comptes, mesure, révocation) paramétré par le collectif, tout en évitant tout mécanisme de contrôle des membres entre eux : contrôle du pouvoir mandaté, confiance au collectif.", + "Confiance à géométrie variable : habilitations progressives, contextuelles, fondées sur l'historique vécu, ajustables en finesse, jamais acquises a priori — pas de score de confiance global et définitif.", + "Dépersonnalisation des propositions (anonymat/pseudonymat type « @personne ») pour neutraliser l'ad hominem, la stigmatisation et la figure du sauveur : on débat du contenu, pas de l'auteur.", + "Bannir les structures qui fabriquent des camps : pas de partis, pas d'étiquettes pro/anti, séparation des sujets pour empêcher les amalgames (une question = un réglage distinct).", + "Permettre la reformulation d'une même question dans plusieurs référentiels (vues alternatives d'un même problème) comme outil de sortie d'impasse.", + "Transparence par défaut (« feux de la rampe ») : pas de huis clos, décisions, options et arbitrages bénéfices/risques visibles de tous, avec instruction populaire intégrée.", + "Consentement jamais cédé une fois pour toutes : toute décision est renégociable « à chaque virage » (révision, réévaluation périodique intégrées au cycle de vie de la décision).", + "Prévoir l'intégration de la monnaie libre (Ğ1/DU) pour la valorisation et l'allocation de ressources ; exclure structurellement toute variable religieuse des processus de décision." + ], + "citations": [ + "Ce livre n'est à personne, ce qu'il devient appartient à tout le monde.", + "On procède par délégation de pouvoir, mais on ne délègue pas, on abandonne.", + "Le peuple adulte prend la décision. Il ne délègue plus, il mandate. Il n'est pas consulté, il est instruit. Il ne cède pas un consentement, il négocie. Chaque virage.", + "la confiance exclut le contrôle, et réciproquement. Confiance et contrôle sont un oxymore irréconciliable.", + "Le clivage binaire est un fléau contre la réflexion, un ennemi fatal : il la tue. De même, le parti politique tue la politique.", + "Les décisions des Comités de direction doivent subir le joug de la sagesse populaire." + ] + }, + { + "resume": "Cette tranche clôt la partie théorique et ouvre la partie « Concrètement ». L'auteur achève la métaphore du peuple adulte : l'émancipation vis-à-vis de la nation mère est « une succession, pourquoi en faire une sécession ? ». Un peuple adulte choisit lui-même son mythe fondateur, un « rêve lucide » qu'il réalise sans demander la permission. Contre l'obsession de « sauver le monde », il propose de secourir et construire, de « se préparer » par un transfert d'intention au sein des activités existantes — une « bascule horizontale » vers l'autonomie. La résilience est érigée en « demi-déesse » protectrice de la vitalité.\n\nLe rêve est nommé : créer une démocratie, « première mondiale, durable, fractale, désirable ». Thèse centrale : aucune démocratie n'a jamais existé ; la France vit sous une « oligarchie électorale ». Les piliers convenus (constitution, élection, liberté d'expression, séparation des pouvoirs, médias comme 4e pouvoir) omettent la clé de voûte : la décision. « Privé de décision, une personne n'a aucun pouvoir, un peuple non plus. » Il cite Sieyès (1789) opposant explicitement État représentatif et démocratie. La bataille sémantique est stratégique : redonner au mot démocratie son sens, sans adjectif, c'est « retrouver la capacité de penser ».\n\n« Qui décide de quoi ? » : tout le monde ne peut pas décider de tout, évidence. Le vrai pouvoir est en amont : définir et contrôler qui décide de quoi. Trois vecteurs de périmètres : dynamique géographique/topologique (bassins de vie, commune pivot, couches d'oignon, priorité par défaut à la petite échelle, montée d'échelle décidée par le peuple) ; cartographie thématique (passer du bitmap au vectoriel : visualiser liaisons et interdépendances, qualifier un sujet plutôt que le titrer, distinguer symptômes et causes, chaque collectif construit et partage sa carte) ; échelle des enjeux et priorités (court/long terme, niveaux d'urgence, volumes ; identifier qui est concerné en premier lieu puis qui se sent concerné). La frontière est repensée comme peau : fonction d'identité et d'interface ; la démocratie naît en écosystème dans la nation existante, tel un lichen, et plusieurs démocraties peuvent s'agréger librement.\n\nLe « mythe de l'élection » : l'élection n'est pas réfutée en bloc mais sévèrement dévalorisée, remise dans la boîte à outils pour les seuls problèmes d'échelle et de mandats, laissant l'espace aux votes. « A voté » rime avec « a renoncé ». Ligne rouge : noter les personnes (on évalue des choses, jamais des personnes).\n\n« Conditions de possibilités » : croire le peuple capable. Les critiques anti-démocratie (Brennan, Haidt) se trompent de référentiel : leurs travers sont ceux de l'oligarchie électorale, jamais d'une démocratie non expérimentée. Se soumettre à une volonté collective n'est envisageable que si mon raisonnement est réellement pris en compte de façon saisissable, si cette volonté est évolutive, si je peux y contribuer, si la décision est observable et rectifiable. Accepter un degré d'inefficacité collectivement choisi, sinon destin de « décision algorithmique ». La devise nationale est recyclée : Liberté→Autonomie, Égalité→Équilibre, Fraternité→Liaison — non plus des idéaux mais des « lois dynamiques », des boussoles.\n\n« Nouvelle carte » : le verrou historique de la démocratie était le volume. Le numérique change la donne : il permet « une décision distribuée et structurée, dans un très grand volume », granularité, agrégation, pondération, représentations visuelles manipulables par tout un peuple. Exemple de Taïwan (révolution des tournesols) : dossier découpé finement, micro-décisions, chacun navigue dans les points qui le concernent, pondère ses préférences, positionne ses curseurs, valide les agrégations d'un clic ; hiérarchie des enjeux, niveaux d'urgence et « cartographie de clôture » visibles de tous. Il propose de nommer cela « décision incrémentielle ». Le numérique doit être maîtrisé par le peuple (libre, chiffré, décentralisé), avec passerelles pour les sans-écran et lieux physiques de « brassage » : plus le numérique prend de place, plus il faut en donner à la relation humaine.\n\n« Pouvoirs régaliens » : la démocratie ne se substitue pas au roi, elle négocie ses relations avec lui. Énergie : l'économie mondiale repose toujours sur l'esclavage (la misère remplaçant le titre de propriété) ; question stratégique : comment se passer de « l'énergie des esclaves » — circuits courts, désindustrialisation alimentaire, filières artisanales. Impôts : pouvoir coercitif principal ; recours à l'art. 14 DDHC (consentir, suivre l'emploi, déterminer la quotité) ; mécanisme clé : recueillir des « engagements fermes » et ne déclencher l'action qu'une fois le quota atteint (seuils critiques). La force : contrat direct peuple-armée, stratégie de défense (aïkido, humus contre canopée). Le secret : institution-sacerdoce imaginée où le peuple affecte ce qui entre au Secret et décide de sa levée ; en attendant, discrétion et bascule des données personnelles vers le logiciel libre, le chiffrement et les organisations décentralisées.", + "theses": [ + "Aucune démocratie n'a jamais existé ; la France vit sous une « oligarchie électorale » ; le projet est une « première démocratie » : première mondiale, durable, fractale, désirable.", + "La clé de voûte de tout l'édifice des pouvoirs, omise de tous les piliers officiels, est la décision : privé de décision, un peuple n'a aucun pouvoir.", + "Le pouvoir premier n'est pas de tout décider ni de tout contrôler, mais de définir et contrôler en amont « qui décide de quoi » et comment la décision doit être prise.", + "Par défaut, la majorité des décisions doit être prise à la plus petite échelle (commune, bassin de vie) ; le changement d'échelle est lui-même une décision du peuple.", + "L'élection n'est pas la démocratie : c'est un outil technique à dévaloriser sévèrement et à réserver aux problèmes d'échelle et de mandats, pour redonner leur espace vital aux votes.", + "Le numérique est aujourd'hui la condition de possibilité d'une démocratie : il permet une décision distribuée et structurée en très grand volume — à condition d'être maîtrisé par le peuple (libre, chiffré, décentralisé).", + "Les critiques savantes de la démocratie (dictature de la majorité, incompétence populaire) se trompent de référentiel : elles décrivent les travers de l'oligarchie électorale, pas une démocratie jamais conçue ni expérimentée.", + "Une démocratie naît en écosystème dans une nation existante (succession, pas sécession) ; sa frontière est une peau, fonction d'identité et d'interface, non une ligne de conquête.", + "La devise doit être recyclée en lois dynamiques praticables : Liberté→Autonomie, Égalité→Équilibre, Fraternité→Liaison.", + "L'adhésion à la volonté collective est conditionnée : raisonnement réellement pris en compte de façon saisissable, volonté évolutive, contribution possible, décision observable et rectifiable.", + "Il faut accepter collectivement un degré d'inefficacité (droit à l'erreur, diversité), sinon le culte de l'efficacité mène inexorablement à la décision algorithmique.", + "L'économie mondiale repose encore sur l'esclavage (entretenu par la misère) ; un peuple qui s'émancipe doit affronter la question : comment se passer de l'énergie des esclaves." + ], + "concepts": [ + { + "nom": "oligarchie électorale", + "definition": "Nom exact du régime français actuel : le pouvoir dans les mains de quelques-uns, validés par l'élection ; régime qui n'a jamais été démocratique. À substituer mentalement au mot « démocratie » dans tout texte sur le régime en vigueur." + }, + { + "nom": "première démocratie", + "definition": "Le rêve-projet : une démocratie qui n'a jamais existé, « première mondiale, durable, fractale, désirable », définie par un peuple qui prend en charge et maîtrise la décision. Le mot démocratie doit rester sans adjectif restrictif." + }, + { + "nom": "la décision (clé de voûte)", + "definition": "Le pouvoir politique le plus générique et le plus en amont de tous les autres ; la grande oubliée des piliers officiels de la démocratie." + }, + { + "nom": "qui décide de quoi", + "definition": "Méta-pouvoir premier d'un peuple adulte : définir et contrôler en amont les périmètres et modalités de décision, distinct de vouloir tout décider ou tout contrôler (tentation pathologique)." + }, + { + "nom": "périmètres", + "definition": "Trois vecteurs pour définir les espaces de décision : dynamique géographique ou topologique, cartographie thématique, échelle des enjeux et des priorités (plus la dynamique des volumes, essentiellement technique, objet de réglages)." + }, + { + "nom": "bassin de vie", + "definition": "Périmètre géographique naturel de première proximité (topologie, reliefs, quartiers, maillage de points de vie quotidienne) ; échelle supérieure à la commune, elle-même pivot topologique." + }, + { + "nom": "rapprochement et petite échelle", + "definition": "Leitmotiv : toujours rapprocher les décisions de soi, du terrain — inversion radicale de la dynamique en vigueur. Pour chaque problème, chercher la plus petite échelle pour débuter, puis où l'augmenter avantageusement." + }, + { + "nom": "cartographie thématique vectorielle", + "definition": "Passer d'un classement en silos (bitmap) à une carte vectorielle des sujets : visualiser liaisons et interdépendances, qualifier un sujet (qualitativement et quantitativement) plutôt que lui affecter un titre, distinguer symptômes et causes ; chaque collectif construit la sienne, la partage et l'adapte." + }, + { + "nom": "frontière-peau", + "definition": "La frontière comme fonction et rôle : instituer une existence et constituer une interface ; imperméable mais perspirante ; identité qui unit plutôt que distingue. Une démocratie a sa propre peau au sein de la nation existante." + }, + { + "nom": "mythe de l'élection", + "definition": "L'élection comme meilleure ennemie d'une démocratie : menu imposé d'icônes marketing, pack idéologique cliqué tous les cinq ans. Outil à remettre dans la boîte à outils, utile seulement pour les problématiques d'échelle et de mandats." + }, + { + "nom": "décision incrémentielle", + "definition": "Nom proposé pour la prise de décision cartographique de grand volume : ni majoritaire, ni quadratique, ni consensuelle, ni compromise ; micro-positionnements pondérés agrégés progressivement." + }, + { + "nom": "cartographie de clôture", + "definition": "Représentation finale visible de tous des positions du peuple sur un dossier complexe : positions soutenues par un grand nombre, hiérarchie des enjeux, niveaux d'urgence (modèle Taïwan, révolution des tournesols)." + }, + { + "nom": "table de mixage / curseurs", + "definition": "Image récurrente du positionnement : lignes de réglages et curseurs que l'on pousse ou tire, contre le clivage binaire ; s'applique aux réflexions, décisions, enjeux et taux." + }, + { + "nom": "Autonomie – Équilibre – Liaison", + "definition": "Devise recyclée pour une démocratie naissant en France : traductions dynamiques de Liberté (autonomie plutôt que souveraineté), Égalité (équilibre plutôt qu'égalité de traitement qui maintient les inégalités), Fraternité (liaison : réciprocité dynamique, rétablissement possible, déploiement nodal, inclusivité). Des boussoles et lois dynamiques, non des idéaux." + }, + { + "nom": "rêve lucide", + "definition": "Projet de société élevé au rang de rêve : phare au-delà de l'intérêt et de la motivation, transformable en réalité (ce qui le distingue de l'utopie), mené avec le cœur autant que la tête, en pleine lucidité sur ce qui est." + }, + { + "nom": "conditions de possibilités", + "definition": "Les croyances comme conditions très concrètes du possible : blocages à lever ou requis. D'abord cesser de croire le peuple incapable ; croire, miser, décider d'y croire (acte de foi) et s'instruire." + }, + { + "nom": "décision algorithmique", + "definition": "Destinée inexorable d'une société qui fait de l'efficacité le leitmotiv ultime de la décision ; repoussoir justifiant l'acceptation collective d'un degré d'inefficacité." + }, + { + "nom": "seuils critiques / engagements fermes", + "definition": "Mécanisme d'action collective : recueillir le volume des engagements fermes et ne déclencher le geste qu'une fois le quota atteint ; sinon partie remise. Force du volume, surprise et débordement, sans victime." + }, + { + "nom": "brasser / brassage", + "definition": "Fonction des lieux physiques adossés aux protocoles numériques : brasser les groupes d'affinités, les idées, les points de vue, les résultats d'expériences — fermentation dont la levure se cultive dans la durée, brassin après brassin." + }, + { + "nom": "épine dorsale numérique", + "definition": "Système nerveux d'une démocratie contemporaine : capacités de traitement de volume et de complexité, représentation, intégrité unitaire et chiffrement, granularité, suivi dans la durée, partage — avec passerelles obligatoires pour ceux qui n'ont aucune relation à l'écran." + }, + { + "nom": "énergie des esclaves", + "definition": "Fondement occulté de l'économie mondiale (l'esclavage perpétué par la misère profonde) ; question stratégique fondatrice : comment remplacer, ou nous passer de l'énergie des esclaves ?" + }, + { + "nom": "stratégie de l'humus", + "definition": "Contre-modèle de puissance face à la stratégie de la canopée : écosystème intriqué qui reste dans l'ombre et devient indispensable sans chercher à l'être ; image du lichen qui repousse toujours." + }, + { + "nom": "Institution du Secret", + "definition": "Fiction régulatrice : le secret comme sacerdoce à vie (sans retour à la vie civile), ne donnant aucun pouvoir ; le peuple affecte ce qui entre au Secret et décide de sa levée ; consultation par mandatés selon des protocoles rapides." + } + ], + "mecanismes_decision": [ + "Méta-décision constitutive en amont : le peuple se donne les moyens de décider « qui décide de quoi » et « comment la décision doit être prise », avant toute décision de fond.", + "Subsidiarité par défaut : la majorité des décisions prise à petite échelle (commune pivot, bassin de vie) ; en changer seulement lorsque c'est nécessaire, plus simple, plus intelligent, et surtout lorsque le peuple le décide. Construction des échelles supérieures par couches d'oignon ou agrégation.", + "Périmétrage sur trois vecteurs : géographique/topologique, cartographie thématique (avec liaisons et interdépendances visibles), échelle des enjeux et priorités (stratégie court/long terme + niveaux d'urgence) ; les volumes comme simples réglages techniques.", + "Qualification des sujets : qualifier (qualitatif + quantitatif) plutôt que titrer, distinguer symptômes et causes, mettre en œuvre des contextes à explorer et mesurer des effets recherchés.", + "Identification des cercles concernés : qui est concerné en tout premier lieu, puis qui se sent concerné en second lieu — pour cesser de théoriser sur les autres et positionner l'enjeu au plus juste (le minimiser allège la réflexion).", + "Votes à curseurs (table de mixage) : positions graduées et pondérées plutôt que clivage binaire oui/non.", + "Modèle Taïwan (révolution des tournesols) : découpage fin d'un dossier complexe en points ; chacun navigue dans les points qui le concernent, pondère ses préférences, positionne ses curseurs, valide les agrégations sur simple clic ; affichage public des positions soutenues, de la hiérarchie des enjeux, des niveaux d'urgence ; cartographie de clôture.", + "« Décision incrémentielle » : mode de décision collective ni majoritaire, ni quadratique, ni consensuel, ni compromis — agrégation de micro-positionnements individuels.", + "Élection reléguée au rang d'outil : réservée aux problématiques d'échelle et de mandats ; système numérique de mandats, de recrutement et de révocation « aussi souple que percutant ».", + "Conditions d'adhésion à la décision collective : je me soumets à la volonté du peuple seulement si mon raisonnement est réellement pris en compte d'une manière que chacun peut saisir, si la volonté est évolutive, si je peux contribuer à son évolution, si la décision est sujette à observation et rectification.", + "Engagements fermes à seuil critique : recueillir le volume des engagements, ne déclencher l'action collective qu'une fois le quota atteint ; sinon partie remise (surprise, devancement, submersion par le volume, sans victime).", + "Consentement à l'impôt (esprit art. 14 DDHC) : constater la nécessité de la contribution, la consentir librement, en suivre l'emploi, en déterminer quotité, assiette, recouvrement et durée ; envisager de retrancher des quote-parts pour les dépenses déclinées sans concession.", + "Institution du Secret : le peuple affecte ce qui entre dans le Secret et ce qui ne doit pas être dissimulé, décide si et quand le secret est levé ; consultation par personnes mandatées, en charge ou en mission, selon des protocoles rapides.", + "Réglage explicite du degré d'inefficacité collectivement accepté (droit à l'erreur, diversité), pour échapper à la pente de la décision algorithmique.", + "Doubles canaux : protocoles numériques de décision + lieux physiques de brassage + passerelles fluides et casuistiques pour ceux qui n'ont aucun accès ou aucune relation à l'écran." + ], + "production_collective": [ + "Collectifs à petite échelle construisant chacun leur cartographie thématique, puis la faisant évoluer en la partageant et l'adaptant — manifestation d'une intelligence collective par créativité utilitariste.", + "Équipes productives limitées : « au-delà de douze, les complications commencent ».", + "Démocratie-écosystème : elle naît dans une nation existante comme un lichen sur son substrat ou un microbiote dans un corps (symbiose possible) ; plusieurs démocraties peuvent s'agréger à leur façon, quand elles le jugent bon.", + "Circuits courts et filières circulaires ; désindustrialisation prioritaire de l'alimentation : filières directes et artisanales, petites entreprises semi-artisanales de conservation et transformation, viande achetée uniquement en filière artisanale connue ; consacrer du temps à se renseigner avant un budget.", + "Lieux physiques de brassage : brasser groupes d'affinités, bandes, clans, communautés, camps et partis pris ; brasser idées, propositions et résultats d'expériences (fermentation, levure cultivée dans la durée).", + "Communauté numérique au service du peuple : développeurs, administrateurs systèmes, hackers et cryptologues comme « guerriers, samouraïs » ; appropriation populaire de la matière numérique, instruction collective au fonctionnement de la « magie numérique ».", + "Bascule des usages personnels hors des bigtech : logiciel libre, cloud opensource, organisations décentralisées et distribuées, chiffrement et clés privées ; geeks militants accompagnant les basculements.", + "Alliances avec les compétences en place : agents des collectivités locales (sens sincère du service public, gâchis actuel) comme alliés ; partenariat opérationnel entre corps d'armée et corps populaires (contrat direct peuple-armée, art. 12 DDHC).", + "Stratégie de l'humus : construire par soi-même légitimité, force et capacité ; devenir indispensable sans chercher à l'être ; force du peuple = son volume, mobilisé par débordement non létal." + ], + "implications_outil": [ + "Prévoir une couche méta (constituante) : avant tout vote de fond, l'outil doit permettre au collectif de décider « qui décide de quoi » et « comment la décision doit être prise » — périmètres, modalités, seuils sont eux-mêmes objets de décision.", + "Périmètres paramétrables sur trois axes : territorial (commune/bassin de vie, agrégation en couches d'oignon), thématique (carte vectorielle des sujets avec liaisons d'interdépendance, jamais de silos), enjeux (curseurs court/long terme, niveau d'urgence, volume) ; défaut systématique = la plus petite échelle, la montée d'échelle étant une décision explicite.", + "UX en curseurs et non en oui/non : micro-positionnements pondérés (table de mixage), navigation sélective dans un dossier finement découpé (chacun ne traite que les points qui le concernent), validation d'agrégations en un clic, restitution par « cartographie de clôture » (positions soutenues, hiérarchie des enjeux, niveaux d'urgence visibles de tous) ; nommer ce mode « décision incrémentielle ».", + "Distinguer deux cercles d'acteurs par sujet : « concernés en premier lieu » et « se sentant concernés » — avec des poids ou rôles différents dans le processus.", + "Traçabilité de la prise en compte : chaque raisonnement individuel doit être visiblement intégré « d'une manière que chacun peut saisir » ; toute décision est évolutive, observable, rectifiable (suivi dans la durée, droit à l'erreur) — condition de l'adhésion des minoritaires.", + "Module mandats distinct et secondaire : élections réservées aux problèmes d'échelle et de mandats ; mandats souples avec recrutement et révocation ; interdit absolu de noter les personnes (on évalue des choses, des propositions — jamais des personnes : « Alerte rouge »).", + "Mécanisme d'engagement conditionnel à seuil : recueil d'« engagements fermes », déclenchement de l'action uniquement au quota atteint, sinon partie remise — transposable en crowdaction/pledge à seuil critique.", + "Souveraineté technique non négociable : logiciel libre, architecture décentralisée/distribuée, chiffrement, clés privées, « intégrité unitaire » ; aucune dépendance bigtech ; ménager un espace intime intouchable pour chaque utilisateur.", + "Passerelles hors-écran obligatoires et articulation avec des lieux physiques de brassage : l'outil est une épine dorsale, pas une totalité (« plus le numérique prend de la place, plus il faut en donner à la relation humaine »).", + "Garde-fou anti-algorithmique : l'outil représente, agrège et éclaire mais ne décide jamais ; prévoir un réglage explicite du « degré d'inefficacité accepté » (temps de réflexion, non-optimisation) ; qualifier les sujets (symptômes/causes, effets recherchés mesurables) plutôt que de simples titres.", + "Vocabulaire de l'interface fidèle à l'auteur : « démocratie » sans adjectif, « votes » distincts des « élections », « périmètres », « curseurs », « cartographie », « décision incrémentielle », devise Autonomie–Équilibre–Liaison comme boussoles.", + "Limiter la taille des équipes productives (~12) avec alerte au-delà ; pour les budgets collectifs, transposer l'art. 14 : suivi de l'emploi des contributions, détermination collective de quotité, assiette et durée." + ], + "citations": [ + "Le pouvoir politique le plus générique et le plus en amont de tous les autres, c'est le pouvoir de la décision. Privé de décision, une personne n'a aucun pouvoir, un peuple non plus.", + "nous vivons sous le régime d'une oligarchie électorale, régime qui n'a jamais été démocratique", + "« A voté » rime irrémédiablement pour moi avec « a renoncé ».", + "ce qui définit une démocratie qui nait, c'est un peuple qui prend en charge et maîtrise la décision", + "Le numérique est – aujourd'hui – une condition de possibilité d'une démocratie, sans laquelle le rêve ne serait pas réaliste", + "On peut sans doute évaluer les choses ; mais les personnes ? Alerte rouge." + ] + }, + { + "resume": "SejeteralO (frontend Nuxt 4/Vue 3, CSS vanilla à variables, SVG natif sans lib de charting) est une plateforme où les habitants d'une commune votent sur la FORME de la courbe tarifaire de l'eau. Le cœur est la page citoyenne unique commune/[slug]/index.vue (~1900 lignes) : un grand graphique SVG interactif où deux courbes de Bézier cubiques (tranche « population » bleue, tranche « cas exceptionnels » orange) sont manipulées via 5 poignées draggables (souris + tactile). Le bulletin de vote est en réalité un point dans un espace continu à 6 dimensions (vinf, a, b, c, d, e — tous normalisés dans [0,1] sauf vinf) ; la géométrie des poignées contraint chaque paramètre à rester valide par construction (drag vertical seul pour a, horizontal seul pour b et c, 2D pour d,e). Le 7e paramètre, le prix au palier p0, n'est PAS voté : il est calculé en temps réel pour que les recettes totales égalent la cible fixée par la mairie (formule p0 = (Recettes − Σabo − Σβ₂)/Σα, intégrales Bézier résolues par Cardano + Newton-Raphson dans utils/bezier-math.ts, miroir TypeScript exact du moteur Python backend). Résultat : toute courbe dessinable est budgétairement faisable — la contrainte est encodée dans le moteur, pas dans une validation a posteriori. Le calcul est local et instantané pendant le drag (recompute() sur les vrais volumes des 363 foyers chargés au montage), avec fallback serveur débounced (150 ms) faisant autorité. La visualisation d'impact est triple : (1) bandeau « indicateurs clés » avec tags explicites « contrainte » (recettes cibles, palier) vs « calculé » (p0, prix médian) et une case verte surlignée « Mon prix au m³ » — le volume personnel du foyer est renvoyé par l'endpoint d'authentification et son prix est interpolé sur la courbe courante ; (2) table « Impact par volume » à volumes de référence (30/60/90/150/300/600 m³) comparant ancien vs nouveau prix, colorée rouge/vert ; (3) la population entière est superposée dans l'espace de décision : toggle « Voir les foyers » (un point par foyer posé SUR la courbe), histogramme des consommations, compteurs de foyers par tranche de 30 m³ imprimés sous l'axe X de chaque graphique. Une barre horizontale verte « prix médian » est elle-même draggable : la déplacer rétro-calcule la recette cible (lien bidirectionnel résultat→contrainte). Le toggle « Courbes des votes » affiche le faisceau semi-transparent des votes des autres citoyens derrière la courbe propre ; côté admin, VoteOverlayChart superpose tous les votes fins + la médiane en trait épais. L'agrégation est la médiane élément par élément des 6 paramètres (backend engine/median.py). L'onboarding est sans compte : un « code électeur » de 8 caractères, distribué par carte électorale, généré à l'import CSV des foyers ; il authentifie ET personnalise (retourne volume_m3). Une seconde méthode « Toile fiduciaire décentralisée » (identité Duniter) est affichée grisée « Bientôt disponible ». Le vote est révisable jusqu'à une deadline (compte à rebours j/h/m/s, bouton désactivé après clôture, « seul votre dernier vote sera pris en compte », date du dernier vote affichée). La séparation des rôles est nette : citizen / commune_admin / super_admin (middleware, logins séparés). L'admin fixe le cadre (recettes, abonnements, pmax, vmax, tarif différencié RS/RP/PRO, deadline), importe les foyers (preview avant import, template CSV, stats), publie une « courbe de référence » qui devient l'état initial de l'éditeur citoyen (ancrage), et édite un mini-CMS markdown affiché en bas de la page citoyenne. Fait notable : l'admin édite la même courbe via des SLIDERS par paramètre (mode expert), le citoyen via manipulation directe (mode novice) — deux UI sur le même modèle. La page citoyenne suit une pédagogie descendante : agir d'abord (graphique + sidebar auth/vote), comprendre ensuite (« Situation de départ », histogramme, baseline linéaire actuelle toujours affichée comme référence). Détails UX : axe X inversé avec libellé « Consommation (m³) → réduire » (le sens de lecture encode l'objectif écologique), zooms sémantiques nommés « Population » / « Cas exceptionnels », molette pour zoomer, DisplaySettings (taille texte A-/A/A+, densité graphiques via --chart-scale, 6 palettes light/dark avec variables SVG dédiées), thème SVG réactif via objet computed t. Limites : l'agrégation médiane par paramètre ne produit pas la « courbe médiane » (mapping non linéaire) et n'est jamais expliquée au citoyen ; les poignées drag-only sont inaccessibles clavier ; les 6 paramètres abstraits (a, b, c, d, e) restent affichés bruts dans un tableau peu parlant ; erreurs via alert().", + "theses": [ + "Une décision technique continue (une courbe tarifaire) peut devenir un bulletin de vote si on la réduit à un petit vecteur de paramètres (6 nombres) manipulé par gestes directs, jamais par saisie de chiffres", + "La faisabilité doit être encodée dans le moteur, pas dans la validation : le citoyen ne vote que sur les degrés de liberté, la variable d'équilibre (p0) est calculée en temps réel — toute option exprimable est budgétairement viable par construction", + "Le feedback d'impact doit être personnel ('Mon prix au m³') et instantané : moteur de calcul dupliqué client (drag fluide) + serveur (autorité), avec la population réelle affichée dans l'espace de décision", + "L'agrégation par médiane élément par élément résiste aux votes extrêmes et se rend visible : faisceau des votes des autres + courbe médiane épaisse", + "L'authentification peut être un simple code papier de 8 caractères qui porte à la fois l'identité et le profil de personnalisation (volume du foyer), sans compte ni email", + "Le vote doit être révisable jusqu'à la clôture — seul le dernier vote compte — avec deadline visible en compte à rebours", + "Admin et citoyen manipulent le même modèle avec deux interfaces : sliders paramétriques pour l'expert qui cadre, manipulation directe pour le citoyen qui choisit", + "Le statu quo (tarification linéaire actuelle) doit rester affiché en permanence comme baseline de comparaison" + ], + "concepts": [ + { + "nom": "Code électeur", + "definition": "Code de 8 caractères par foyer, généré à l'import des données et distribué par carte électorale ; authentifie sans compte et renvoie le volume du foyer pour personnaliser l'affichage d'impact" + }, + { + "nom": "Prix au palier (p0)", + "definition": "Variable d'équilibre calculée (jamais votée) pour que les recettes totales atteignent la cible : p0 = (Recettes − Σabo − Σβ₂)/Σα ; recalculée en local à chaque drag" + }, + { + "nom": "Courbe de référence (publiée)", + "definition": "Courbe que l'admin publie et qui devient l'état initial de l'éditeur de chaque citoyen — mécanisme d'ancrage du débat ; à défaut, la médiane des votes sert d'état initial" + }, + { + "nom": "Courbes des votes (faisceau)", + "definition": "Overlay semi-transparent des courbes votées par les autres citoyens derrière sa propre courbe (toggle) ; côté admin, tous les votes fins + médiane en trait épais" + }, + { + "nom": "Médiane élément par élément", + "definition": "Agrégation des votes : médiane composante par composante des 6 paramètres (vinf, a…e), pas moyenne — robuste aux extrêmes mais ne correspond pas à une courbe effectivement votée" + }, + { + "nom": "Contrainte / Calculé", + "definition": "Tags explicites sur le bandeau d'indicateurs distinguant les valeurs imposées par la mairie (recettes cibles, palier) des valeurs qui découlent du vote (p0, prix médian)" + }, + { + "nom": "Mon prix au m³", + "definition": "Case surlignée affichant le prix que la courbe en cours d'édition donnerait au foyer authentifié, interpolé sur son volume réel — le 'qu'est-ce que ça change pour moi' permanent" + }, + { + "nom": "Barre de prix médian draggable", + "definition": "Ligne horizontale sur le graphique qu'on peut déplacer verticalement : le geste rétro-calcule la recette cible correspondante — la contrainte devient elle-même négociable par manipulation du résultat" + }, + { + "nom": "Zooms sémantiques Population / Cas exceptionnels", + "definition": "Presets de zoom nommés d'après les segments de la décision (tranche 1 / tranche 2 de la courbe), plutôt que de simples +/−" + }, + { + "nom": "Toile fiduciaire décentralisée", + "definition": "Second mode d'authentification annoncé (identité numérique Duniter, web of trust), affiché grisé 'Bientôt disponible' — divulgation progressive d'une identité souveraine" + }, + { + "nom": "Impact par volume", + "definition": "Table comparant ancien prix vs nouveau prix à des volumes de référence (30–600 m³), colorée rouge (hausse) / vert (baisse), déclinée RP/PRO vs RS si tarif différencié" + }, + { + "nom": "Situation de départ", + "definition": "Section montrant l'état actuel (histogramme des consommations, tarif linéaire en vigueur) comme baseline permanente du choix" + } + ], + "mecanismes_decision": [ + "Vote continu paramétrique : le bulletin est un vecteur de 6 nombres derrière une manipulation directe de courbe (poignées SVG souris+tactile), agrégé par médiane élément par élément des votes actifs", + "Contrainte d'équilibre auto-satisfaite : le citoyen choisit la forme, le système résout la variable d'ajustement (p0) en temps réel — impossible de proposer une option infaisable", + "Vote révisable à dernier-vote-compte, avec deadline fixée par l'admin, compte à rebours affiché et clôture automatique (bouton désactivé, message 'Le vote est clos')", + "Publication d'une courbe de référence par l'admin comme point de départ commun de tous les éditeurs (ancrage explicite du débat)", + "Méta-décision par la barre médiane draggable : déplacer le résultat agrégé (prix médian) rétro-calcule la contrainte (recettes cibles) — exploration bidirectionnelle contrainte↔résultat", + "Séparation cadrage/choix : l'admin commune fixe les invariants (recettes, abonnements, bornes vmax/pmax, tarif différencié, deadline) et importe la population ; les citoyens ne votent que la forme dans ce cadre", + "Transparence de la distribution des préférences : faisceau des courbes votées visible par tout citoyen (toggle), overlay complet + médiane + liste des votes par foyer côté admin", + "Authentification par code foyer 8 caractères distribué physiquement, sans création de compte, un vote par foyer (dernier écrase le précédent)" + ], + "production_collective": [ + "La commune comme unité de décision : plateforme multi-communes (une page publique par slug), chaque mairie administre son instance (paramètres, foyers, contenu, deadline) via un rôle commune_admin délégué par un super_admin", + "Co-construction du tarif par les usagers réels : les données de facturation effectives (volume, statut RP/RS/PRO par foyer) sont importées par CSV et deviennent le substrat du calcul — chaque vote est évalué contre la population réelle, pas un modèle abstrait", + "Mini-CMS communal : la mairie rédige des pages markdown (présentation, explications) affichées sous l'outil de vote — le contexte délibératif est produit localement", + "Perspective d'identité décentralisée : l'authentification Duniter (toile de confiance) est annoncée comme alternative au code papier, esquissant une gouvernance sans autorité d'enrôlement centrale", + "Le résultat collectif (courbe médiane) est destiné à être publié comme nouvelle courbe de référence, bouclant un cycle proposition→vote→publication→révision" + ], + "implications_outil": [ + "Réduire toute décision continue à un petit espace paramétrique (≤6-7 dimensions) manipulé par gestes : chaque poignée contrainte géométriquement (axe vertical seul, horizontal seul, 2D borné) garantit la validité par construction, sans validation ni message d'erreur", + "Séparer degrés de liberté votés et variables d'équilibre calculées, et l'afficher : tags 'contrainte' vs 'calculé' sur chaque indicateur clé — le citoyen comprend ce qu'il contrôle et ce qui découle", + "Dupliquer le moteur de calcul : implémentation client (TS) miroir du backend pour le feedback instantané au drag, recompute serveur débounced (150-300 ms) faisant autorité au relâchement — pattern indispensable pour tout simulateur de décision temps réel", + "Personnaliser l'impact dès l'authentification : l'endpoint d'auth renvoie le profil (ici volume_m3) et l'UI maintient en permanence une case 'Mon prix' — répondre à 'qu'est-ce que ça change pour MOI' avant toute autre pédagogie", + "Superposer la population affectée dans l'espace de décision lui-même (points par foyer sur la courbe, compteurs par tranche sous l'axe, histogramme) plutôt que dans des stats séparées", + "Rendre l'agrégation visible et tangible : toggle 'voir les autres votes' (faisceau translucide) + agrégat en trait épais + badge 'N vote(s)' ; MAIS expliquer la règle d'agrégation (la médiane par paramètre est muette ici — défaut à corriger dans un outil générique)", + "Onboarding zéro-compte par code court distribué hors-ligne, porteur d'identité + profil ; prévoir la montée vers une identité forte (web of trust) en divulgation progressive ('Bientôt disponible' grisé)", + "Toujours montrer le statu quo comme baseline comparable (section 'Situation de départ' + colonne 'Actuel' dans la table d'impact, rouge/vert)", + "Structure de page 'agir d'abord, comprendre ensuite' : l'outil interactif + le bloc vote en haut, les explications, données de départ et CMS en dessous", + "Vote révisable jusqu'à deadline avec compte à rebours — abaisse le coût psychologique du premier vote (on peut revenir)", + "Deux interfaces sur le même modèle : sliders paramétriques nommés pour l'admin/expert, manipulation directe pour le citoyen — réutiliser le même moteur et les mêmes bornes", + "Points trop spécifiques à l'eau à ne pas transposer tels quels : la paramétrisation Bézier bi-tranche (vinf/a-e), les intégrales de recette, les statuts RP/RS/PRO, l'axe X inversé '→ réduire' (nudge écologique lié à la consommation), les volumes de référence 30-600 m³ ; à généraliser en 'espace de politique paramétré + contrainte de conservation + profil personnel d'impact'", + "Limites d'accessibilité à corriger en générique : drag-only sans alternative clavier/champ numérique, paramètres abstraits (a,b,c,d,e) affichés bruts sans libellés métier, erreurs via alert()" + ], + "citations": [ + "Deplacez les poignees pour ajuster la courbe. Le prix au palier s'ajuste pour equilibrer les recettes. La barre verte indique le prix median — deplacez-la pour changer les recettes cibles.", + "Dessinez votre courbe de tarification idéale et participez aux choix de votre commune.", + "Mon prix au m³", + "Votre vote a été soumis avec succès. Vous pouvez revenir à l'éditeur pour modifier votre choix à tout moment (seul votre dernier vote sera pris en compte).", + "Toile fiduciaire decentralisee — Authentification avec identite numerique Duniter — Bientot disponible", + "Consommation (m³) → reduire", + "Ajustez les parametres de la courbe Bezier, puis publiez-la. C'est la courbe initiale que les citoyens voient." + ] + } + ] +} \ No newline at end of file diff --git a/frontend/app/engine/index.ts b/frontend/app/engine/index.ts new file mode 100644 index 0000000..41105cb --- /dev/null +++ b/frontend/app/engine/index.ts @@ -0,0 +1,11 @@ +/** + * Pure voting-formula engine — TypeScript port of backend/app/engine/. + * Python is the reference implementation; the vitest suite in + * frontend/tests/engine/ mirrors the backend pytest suite. + */ + +export { wotThreshold, smithThreshold, techcommThreshold } from './threshold' +export { nuancedResult, LEVEL_LABELS, NUM_LEVELS } from './nuanced' +export type { NuancedResult } from './nuanced' +export { parseModeParams, formatModeParams } from './modeParams' +export type { ModeParams } from './modeParams' diff --git a/frontend/app/engine/modeParams.ts b/frontend/app/engine/modeParams.ts new file mode 100644 index 0000000..79a0beb --- /dev/null +++ b/frontend/app/engine/modeParams.ts @@ -0,0 +1,163 @@ +/** + * Mode-params DSL: parse and format compact parameter strings. + * + * Exact TypeScript port of backend/app/engine/mode_params.py + * (Python is the reference implementation for parsing; the Python + * module has no formatter, so formatModeParams() is defined as the + * canonical inverse of parseModeParams()). + * + * A mode-params string encodes voting formula parameters in a compact format. + * Example: "D30M50B.1G.2T.1" + * + * Supported codes: + * D = duration_days (int) + * M = majority_pct (int, 0-100) + * B = base_exponent (float) + * G = gradient_exponent (float) + * C = constant_base (float) + * S = smith_exponent (float) + * T = techcomm_exponent (float) + * N = ratio_multiplier (float) + * R = is_ratio_mode (bool, 0 or 1) + * + * Values may start with a dot for decimals < 1, e.g. "B.1" means base_exponent=0.1. + */ + +export interface ModeParams { + duration_days: number + majority_pct: number + base_exponent: number + gradient_exponent: number + constant_base: number + smith_exponent: number | null + techcomm_exponent: number | null + ratio_multiplier: number | null + is_ratio_mode: boolean +} + +type CodeType = 'int' | 'float' | 'bool' + +// Ordered list of recognised codes and their target keys + types +const CODES: Record = { + D: { key: 'duration_days', type: 'int' }, + M: { key: 'majority_pct', type: 'int' }, + B: { key: 'base_exponent', type: 'float' }, + G: { key: 'gradient_exponent', type: 'float' }, + C: { key: 'constant_base', type: 'float' }, + S: { key: 'smith_exponent', type: 'float' }, + T: { key: 'techcomm_exponent', type: 'float' }, + N: { key: 'ratio_multiplier', type: 'float' }, + R: { key: 'is_ratio_mode', type: 'bool' }, +} + +// Regex: a single uppercase letter followed by a numeric value (int or float, +// possibly starting with '.' for values like .1 meaning 0.1) +const PARAM_RE = /([A-Z])(\d*\.?\d+)/g + +function getDefaults(): ModeParams { + return { + duration_days: 30, + majority_pct: 50, + base_exponent: 0.1, + gradient_exponent: 0.2, + constant_base: 0.0, + smith_exponent: null, + techcomm_exponent: null, + ratio_multiplier: null, + is_ratio_mode: false, + } +} + +/** + * Parse a mode-params string into a parameter object. + * + * Defaults are applied for any code not present in the string. + * + * @param paramsStr - Compact parameter string, e.g. "D30M50B.1G.2T.1" + * @returns Parsed parameters with defaults for codes not found + * @throws Error if an unrecognised code letter is found + */ +export function parseModeParams(paramsStr: string): ModeParams { + const result = getDefaults() + + if (!paramsStr || !paramsStr.trim()) { + return result + } + + for (const match of paramsStr.matchAll(PARAM_RE)) { + const code = match[1]! + const rawValue = match[2]! + + const entry = CODES[code] + if (!entry) { + throw new Error(`Code de parametre inconnu : '${code}'`) + } + + const { key, type } = entry + + if (type === 'int') { + // Python: int(float(raw)) truncates toward zero + ;(result as Record)[key] = Math.trunc(parseFloat(rawValue)) + } else if (type === 'float') { + ;(result as Record)[key] = parseFloat(rawValue) + } else { + ;(result as Record)[key] = parseFloat(rawValue) !== 0.0 + } + } + + return result +} + +/** + * Format a numeric value in DSL form: 0.1 -> ".1", 1 -> "1", 2.5 -> "2.5". + */ +function formatValue(value: number): string { + const s = String(value) + return value > 0 && value < 1 ? s.replace(/^0\./, '.') : s +} + +/** + * Format a parameter object back into a canonical mode-params string. + * + * Canonical form matches the strings used across the project + * (e.g. "D30M50B.1G.2S.1"): D, M, B, G always present in that order, + * then C when non-zero, then S/T/N when set, then R1 when ratio mode. + * Round-trip: parseModeParams(formatModeParams(p)) is identical to p. + * + * @param params - Parameters to encode (missing keys take defaults) + * @returns Compact string, e.g. "D30M50B.1G.2" + */ +export function formatModeParams(params: Partial = {}): string { + const p = getDefaults() + for (const key of Object.keys(p) as Array) { + const value = params[key] + if (value !== undefined) { + ;(p[key] as ModeParams[typeof key]) = value + } + } + + const parts: string[] = [ + `D${formatValue(p.duration_days)}`, + `M${formatValue(p.majority_pct)}`, + `B${formatValue(p.base_exponent)}`, + `G${formatValue(p.gradient_exponent)}`, + ] + + if (p.constant_base !== 0) { + parts.push(`C${formatValue(p.constant_base)}`) + } + if (p.smith_exponent !== null) { + parts.push(`S${formatValue(p.smith_exponent)}`) + } + if (p.techcomm_exponent !== null) { + parts.push(`T${formatValue(p.techcomm_exponent)}`) + } + if (p.ratio_multiplier !== null) { + parts.push(`N${formatValue(p.ratio_multiplier)}`) + } + if (p.is_ratio_mode) { + parts.push('R1') + } + + return parts.join('') +} diff --git a/frontend/app/engine/nuanced.ts b/frontend/app/engine/nuanced.ts new file mode 100644 index 0000000..2ed045e --- /dev/null +++ b/frontend/app/engine/nuanced.ts @@ -0,0 +1,109 @@ +/** + * Six-level nuanced vote evaluation. + * + * Exact TypeScript port of backend/app/engine/nuanced_vote.py + * (Python is the reference implementation). + * + * Levels: + * 0 - CONTRE + * 1 - PAS DU TOUT + * 2 - PAS D'ACCORD + * 3 - NEUTRE + * 4 - D'ACCORD + * 5 - TOUT A FAIT + * + * Adoption rule: + * The sum of votes at levels 3 + 4 + 5 must be >= thresholdPct% of total votes. + * A minimum number of participants is also required. + */ + +export const LEVEL_LABELS: Record = { + 0: 'CONTRE', + 1: 'PAS DU TOUT', + 2: "PAS D'ACCORD", + 3: 'NEUTRE', + 4: "D'ACCORD", + 5: 'TOUT A FAIT', +} + +export const NUM_LEVELS = 6 + +/** Result shape mirrors the Python dict returned by evaluate_nuanced(). */ +export interface NuancedResult { + total: number + per_level_counts: Record + positive_count: number + positive_pct: number + threshold_met: boolean + min_participants_met: boolean + adopted: boolean +} + +/** + * Round to 2 decimals with ties-to-even, mirroring Python's round(x, 2). + */ +function round2(value: number): number { + const scaled = value * 100 + const floor = Math.floor(scaled) + const diff = scaled - floor + let rounded: number + if (diff > 0.5) { + rounded = floor + 1 + } else if (diff < 0.5) { + rounded = floor + } else { + rounded = floor % 2 === 0 ? floor : floor + 1 + } + return rounded / 100 +} + +/** + * Evaluate a nuanced vote from a list of individual vote levels. + * + * @param votes - List of vote levels (each 0-5), one entry per voter + * @param thresholdPct - Minimum percentage of positive votes (levels 3-5) for adoption + * @param minParticipants - Minimum number of participants required for validity + * @returns Aggregated result (counts, percentage, adoption flags) + * @throws Error if any vote value is outside the 0-5 integer range + */ +export function nuancedResult( + votes: number[], + thresholdPct: number = 80, + minParticipants: number = 59, +): NuancedResult { + // Validate vote levels (integer check added: JS has no KeyError safety net) + for (const v of votes) { + if (!Number.isInteger(v) || v < 0 || v > 5) { + throw new Error(`Niveau de vote invalide : ${v}. Les niveaux valides sont 0-5.`) + } + } + + const total = votes.length + + const perLevelCounts: Record = {} + for (let level = 0; level < NUM_LEVELS; level++) { + perLevelCounts[level] = 0 + } + for (const v of votes) { + perLevelCounts[v] = (perLevelCounts[v] ?? 0) + 1 + } + + // Positive = levels 3 (NEUTRE), 4 (D'ACCORD), 5 (TOUT A FAIT) + const positiveCount = (perLevelCounts[3] ?? 0) + (perLevelCounts[4] ?? 0) + (perLevelCounts[5] ?? 0) + + const positivePct = total > 0 ? (positiveCount / total) * 100.0 : 0.0 + + const thresholdMet = positivePct >= thresholdPct + const minParticipantsMet = total >= minParticipants + const adopted = thresholdMet && minParticipantsMet + + return { + total, + per_level_counts: perLevelCounts, + positive_count: positiveCount, + positive_pct: round2(positivePct), + threshold_met: thresholdMet, + min_participants_met: minParticipantsMet, + adopted, + } +} diff --git a/frontend/app/engine/threshold.ts b/frontend/app/engine/threshold.ts new file mode 100644 index 0000000..87b895f --- /dev/null +++ b/frontend/app/engine/threshold.ts @@ -0,0 +1,110 @@ +/** + * WoT members threshold formula for binary votes. + * + * Exact TypeScript port of backend/app/engine/threshold.py, + * smith_threshold.py and techcomm_threshold.py (Python is the + * reference implementation — same guards, same ceil rounding). + * + * Core formula: + * Result = C + B^W + (M + (1-M) * (1 - (T/W)^G)) * max(0, T - C) + * + * Where: + * C = constantBase + * B = baseExponent + * W = wotSize (corpus of eligible voters) + * T = totalVotes (for + against) + * M = majorityRatio (majorityPct / 100) + * G = gradientExponent + * + * Inertia behaviour: + * - Low participation (T << W) -> near-unanimity required + * - High participation (T -> W) -> simple majority M suffices + * + * Reference test case: + * wotSize=7224, votesFor=97, votesAgainst=23 (total=120) + * params M50 B.1 G.2 => threshold=94, adopted (97 >= 94) + */ + +/** + * Compute the minimum number of *for* votes required for adoption. + * + * A vote passes when `votesFor >= wotThreshold(...)`. + * + * @param wotSize - Size of the eligible voter corpus (WoT members) + * @param totalVotes - Number of votes cast (for + against) + * @param majorityPct - Majority percentage (0-100). 50 = simple majority at full participation + * @param baseExponent - B in the formula; B^W is a vanishingly small offset when W is large + * @param gradientExponent - G controls how fast the super-majority decays toward M + * @param constantBase - C, a fixed additive floor on the threshold + * @returns The ceiling of the computed threshold + */ +export function wotThreshold( + wotSize: number, + totalVotes: number, + majorityPct: number = 50, + baseExponent: number = 0.1, + gradientExponent: number = 0.2, + constantBase: number = 0.0, +): number { + if (wotSize <= 0) { + throw new Error('wotSize doit etre strictement positif') + } + if (totalVotes < 0) { + throw new Error('totalVotes ne peut pas etre negatif') + } + if (majorityPct < 0 || majorityPct > 100) { + throw new Error('majorityPct doit etre entre 0 et 100') + } + + const C = constantBase + const B = baseExponent + const W = wotSize + const T = totalVotes + const M = majorityPct / 100.0 + const G = gradientExponent + + // Guard: if no votes, threshold is at least ceil(C + B^W) + if (T === 0) { + return Math.ceil(C + B ** W) + } + + // Core formula + const participationRatio = T / W + const inertiaFactor = 1.0 - participationRatio ** G + const requiredRatio = M + (1.0 - M) * inertiaFactor + const result = C + B ** W + requiredRatio * Math.max(0.0, T - C) + + return Math.ceil(result) +} + +/** + * Smith sub-WoT threshold criterion. + * + * Formula: ceil(smithWotSize ^ S) + * + * @param smithWotSize - Number of active Smith members (forgerons) + * @param exponent - S in the formula + * @returns Minimum Smith votes required + */ +export function smithThreshold(smithWotSize: number, exponent: number = 0.1): number { + if (smithWotSize <= 0) { + throw new Error('smithWotSize doit etre strictement positif') + } + return Math.ceil(smithWotSize ** exponent) +} + +/** + * Technical Committee threshold criterion. + * + * Formula: ceil(cotecSize ^ T) + * + * @param cotecSize - Number of Technical Committee members + * @param exponent - T in the formula + * @returns Minimum TechComm votes required + */ +export function techcommThreshold(cotecSize: number, exponent: number = 0.1): number { + if (cotecSize <= 0) { + throw new Error('cotecSize doit etre strictement positif') + } + return Math.ceil(cotecSize ** exponent) +} diff --git a/frontend/app/lexicon.ts b/frontend/app/lexicon.ts new file mode 100644 index 0000000..4e2ed18 --- /dev/null +++ b/frontend/app/lexicon.ts @@ -0,0 +1,251 @@ +// ───────────────────────────────────────────────────────────── +// libreDecision v2 — lexicon: SINGLE source of French UI labels. +// Tone rules (BLUEPRINT-V2.md): +// 1. tutoyer la personne dans ses gestes ; +// 2. actes collectifs à l'infinitif (« Adopter notre Pacte ») ; +// 3. sujet grammatical des phrases générées = les personnes ou le collectif, +// JAMAIS la formule ou le système. +// The anti-lexicon vitest test (tests/lexicon.spec.ts) enforces FORBIDDEN_UI_TERMS +// over every UI-visible string (lexicon + .vue templates). +// ───────────────────────────────────────────────────────────── + +import type { + DecisionRoute, + DecisionStatus, + InertiaPreset, + NominationMethod, + NuancedValue, + VoteMethod, + Weight, + Reversibility, +} from '~/types/domain' + +/** Words that must NEVER appear in UI-visible text (case-insensitive, accent-aware). + * They may exist as code identifiers — the test only scans UI strings. */ +export const FORBIDDEN_UI_TERMS = [ + 'déléguer', + 'délégation', + 'délégué', + 'dette', + 'retard', + 'en souffrance', + 'corpus', + 'snapshot', + 'verdict', + 'triage', + 'oligarchie', + 'peuple adulte', +] as const + +// ── Routes — first person: the path is a gesture, not a category ── +export const ROUTE_LABELS: Record = { + solo: 'Je décide', + mandate: 'Je décide, sous mandat', + transmit: 'Je transmets', + advice: "J'écoute, puis je décide", + collective: 'Nous décidons', + record: 'Je le consigne', +} + +/** Short pill forms (registry filters). */ +export const ROUTE_SHORT: Record = { + solo: 'seul', + mandate: 'sous mandat', + transmit: 'transmis', + advice: 'sur avis', + collective: 'ensemble', + record: 'consigné', +} + +export const ROUTE_ICONS: Record = { + solo: 'i-lucide-zap', + mandate: 'i-lucide-key-round', + transmit: 'i-lucide-send', + advice: 'i-lucide-message-circle', + collective: 'i-lucide-users', + record: 'i-lucide-notebook-pen', +} + +export const ICONS = { + urgent: 'i-lucide-siren', + alreadyDecided: 'i-lucide-book-open-check', + recurrence: 'i-lucide-repeat', + parametric: 'i-lucide-sliders-horizontal', + crystallize: 'i-lucide-stamp', +} as const + +// ── Decision states — plain French pills ── +export const STATUS_LABELS: Record = { + draft: 'préparation', + advice: 'avis', + objection: 'objection', + framing: 'formulation', + voting: 'vote', + adopted: 'en vigueur', + rejected: 'rejeté', + revoked: 'révoqué', + transmitted: 'transmis', + closed: 'clos', +} + +/** Session display state (frozen lives on VoteSession, shown as a decision badge). */ +export const FROZEN_LABEL = 'figé' +export const FROZEN_BANNER = 'Votes figés — en attente de cristallisation' + +// ── Weight & reversibility — the tunnel chips ── +export const WEIGHT_LABELS: Record = { + light: 'rien de lourd', + binding: 'du temps ou des ressources', + structural: 'la structure, les règles, du long terme', +} +export const REVERSIBILITY_LABELS: Record = { + easy: 'facilement réversible', + costly: 'coûteux à défaire', + irreversible: 'irréversible', +} + +// ── Nuanced scale — never « CONTRE » ── +export const NUANCED_LABELS: Record = { + 0: 'Pas du tout', + 1: 'Plutôt non', + 2: 'Réservé', + 3: 'Plutôt oui', + 4: 'Oui', + 5: 'Tout à fait', +} + +// ── Vote methods ── +export const METHOD_LABELS: Record = { + consent: 'Consentement', + binary: 'Pour / contre', + nuanced: 'Vote nuancé', + parametric: 'Réglage collectif', + election: 'Élection', +} +export const BINARY_COST = 'deux camps, un perdant' +export const BINARY_DESCRIPTION = "l'outil de dernier recours des très grands corps" + +// ── The review — l'épreuve du réel ── +export const REVIEW_TITLE = "L'épreuve du réel" +export const REVIEW_QUESTION = 'Le réel a-t-il suivi ?' +export const REVIEW_VERDICTS = { + confirmed: 'Ça tient', + revise: 'À revoir', + revoke: 'À révoquer', +} as const + +// ── Windows, assent, perimeter ── +export const WINDOW_OK = 'Ça me va' +export const WINDOW_OBJECT = "J'objecte" +export const ASSENT_MISSING = 'Il manque un accord explicite' +export const BOUNDARY_SUSPENDED = 'compte à rebours suspendu — frontière contestée' +export const CONCERN_FIRST = 'Concerné·e en premier lieu' +export const CONCERN_SECOND = "Concerné·e en second lieu — s'est déclaré·e" +export const CONCERN_ME = 'Ça me concerne' +export const PERIMETER_OVERFLOW = "Le périmètre déborde — élargis d'un cran" +export const SCOPE_WIDEN = "Élargir d'un cran" +export const SCOPE_KEEP = 'Maintenir — en motivant publiquement' + +// ── Parametric — le réglage collectif ── +export const PARAMETRIC_ALT = 'C\'est un réglage (montant, taux, répartition) — décidez au curseur' +export const PARAM_CONSTRAINT_LABEL = 'contrainte' +export const PARAM_DERIVED_LABEL = 'calculé' +export const IMPACT_CARD_TITLE = 'Pour moi' +export const IMPACT_DISCLAIMER = 'estimation sur données déclarées' +export const EXPLORE_LABEL = 'Explorer' +export const EXPLORE_DISCLAIMER = 'exploration — ne compte pas' +export const CRYSTALLIZE_ACTION = 'Cristalliser' +export const CRYSTALLIZE_CARD = 'Les votes sont figés — cristallise la médiane' +export const MEDIAN_EXPLANATION = + 'Le collectif retient la médiane de chaque curseur — robuste aux extrêmes, ' + + "et c'est une position que chacun aurait pu proposer." +export const BIMODAL_BANNER = (paramLabel: string) => + `Deux positions distinctes se dessinent sur ${paramLabel} — scinder la question ou changer de référentiel ?` +export const CRYSTALLIZE_CHOICES = ['Cristalliser quand même', 'Scinder', 'Reformuler'] as const + +// ── Record & observatory ── +export const RECORD_ALT = 'Déjà tranché — je le consigne' +export const RECORD_HOW = "Comment ça s'est décidé ?" +export const OBSERVATORY_TITLE = "L'Observatoire" +export const OBSERVATORY_SUBTITLE = 'comment nous décidons, dans les faits' +export const REVOKED_SECTION = "Révoquées — ce qu'on en a appris" +export const MATURATION_CARD = 'Cette pratique est mûre — protocolise-la en quelques phrases' + +// ── Élection ── +export const BLANK_VOTE = 'Vote blanc' +export const ELECTION_RULE = + "La personne la plus désignée est nommée ; en cas d'égalité, vous départagez — jamais l'outil." + +// ── Urgence ── +export const URGENT_TOGGLE = "C'est urgent" +export const URGENT_BADGE = 'Décidé en urgence — le collectif ratifie' +export const URGENT_REFUSED = "Irréversible : l'urgence ne peut pas contourner le collectif." + +// ── Fil / Aujourd'hui ── +export const FEED_HEADER = 'À toi de décider' +export const CAPTURE_PLACEHOLDER = "Qu'est-ce que tu décides ?" +export const DOSSIER_COMPLETE_CARD = 'Le dossier est complet — clore et publier la cartographie de clôture' + +// ── Fiche / tunnel ── +export const PATH_CARD_TITLE = 'Le chemin' +export const CHOOSE_OTHERWISE = 'Je choisis autrement' +export const WHY_DISCLOSURE = 'Pourquoi ?' +export const ENGAGES_LABEL = 'Ce que ça engage' +export const BASELINE_PREFIX = "Aujourd'hui :" +export const BASELINE_ARROW = "Aujourd'hui → Proposé" +export const INSTRUCT_BLOCK = "S'instruire" +export const REOPEN_HANDLE = 'Remettre en question' +export const ADOPTED_STAMP = 'Décidé' +export const ADOPTED_TOAST = "C'est décidé — et révisable, comme tout ici." +export const VOTE_PRIVACY = "Ton vote est remplaçable — ton historique n'est visible que de toi." +export const WHO_VOTES = (n: number, date: string) => + `Qui vote : ${n} personne${n > 1 ? 's' : ''} — liste arrêtée le ${date}` + +// ── Mandats ── +export const MANDATES_TITLE = 'Mandats — les pouvoirs confiés' +export const MANDATE_EXERCISE = 'Exercice du mandat' +export const MANDATE_SPOTLIGHT = 'Feux de la rampe' +export const MANDATE_SPOTLIGHT_SUB = 'toutes les décisions prises sous ce mandat — contestables' +export const MANDATE_CLAIM = 'Réclamer un mandat' +export const MANDATE_REVOKE = 'Demander la révocation' +export const NOMINATION_LABELS: Record = { + 'ratified-self': 'Auto-désignation ratifiée', + 'election-no-candidate': 'Élection sans candidat', + 'nuanced-vote': 'Vote nuancé', + consent: 'Consentement', + draw: 'Tirage au sort', + rotation: 'Rotation', +} + +// ── Textes / Pacte ── +export const PACT_BADGE = 'les règles du jeu' +export const PACT_SUBTITLE = 'Notre contrat social — sacralisé, jamais immuable' +export const INERTIA_LABELS: Record = { + low: 'inertie faible', + standard: 'inertie standard', + high: 'inertie haute', + max: 'inertie maximale', +} +export const FIRST_DECISION = 'Adopter notre Pacte' +export const CREATE_SIGNATURE = 'Osons, nous verrons' +export const OPENING_DREAM = "Une démocratie qui n'a jamais existé — à votre échelle." +export const CLOSING_LINE = 'La ville change, le puits demeure.' + +// ── Preuve / gravure / données ── +export const PROOF_LOCAL = 'empreinte locale — démo' +export const SECRET_DISPLAY = "secret d'affichage — démo" +export const LINEAGE_PREFIX = 'essaimé de' +export const WORKSHOP_MODE = 'mode atelier' +export const WORKSHOP_RECORDED = (by: string, forWhom: string, date: string) => + `saisi par ${by} pour ${forWhom} — atelier du ${date}` +export const ATTRIBUTES_HINT = + 'servent uniquement ta carte Pour moi, jamais montrés aux autres' +export const SYNC_NOTE = + 'Un serveur pourra se brancher plus tard, tes données ne changent pas de forme.' +export const NO_PROTOCOL_BANNER = 'Aucun protocole — crée-le ou décide sur avis' + +// ── Génération des phrases de seuil (sujet = le collectif, jamais la formule) ── +export const thresholdSentence = (W: number, T: number, threshold: number) => + `${T.toLocaleString('fr-FR')} votant${T > 1 ? 's' : ''} sur ${W.toLocaleString('fr-FR')} : ` + + `à si faible participation, il faut presque l'unanimité — ${threshold.toLocaleString('fr-FR')} pour. ` + + 'Plus de monde vote, plus le seuil descend.' diff --git a/frontend/app/types/domain.ts b/frontend/app/types/domain.ts new file mode 100644 index 0000000..70b1996 --- /dev/null +++ b/frontend/app/types/domain.ts @@ -0,0 +1,521 @@ +// ───────────────────────────────────────────────────────────── +// libreDecision v2 — domain core (BLUEPRINT-V2.md, docs/dev/) +// Local-first, sync-ready. English identifiers, French UI via app/lexicon.ts. +// INVARIANT 1: THE DECISION IS THE PIVOT — mandates, clauses, revocations, +// ratifications and dossier elements are chained specializations of it. +// INVARIANT 2: that unification is an architectural fact, NEVER UI vocabulary. +// INVARIANT 3: no aggregate/score on a person, anywhere — we evaluate things. +// INVARIANT 4: no computation ever produces adopted content without a dated +// human gesture (parametric crystallization, election tie-break, dossier close). +// ───────────────────────────────────────────────────────────── + +export type Id = string // UUIDv7 — time-sortable, sync-ready +export type ISODate = string // ISO 8601 UTC +export type Json = string | number | boolean | null | Json[] | { [k: string]: Json } + +// Every entity: collective scoping + sync clock + soft delete. +// Sync-ready rules: never a physical delete (archivedAt); +// Vote/Advice/Objection/Assent append-only (trivial merge); the rest LWW by updatedAt. +export interface Entity { + id: Id + collectiveId: Id + createdAt: ISODate + updatedAt: ISODate + archivedAt?: ISODate +} + +// ── The tenant ─────────────────────────────────────────────── +export type CollectiveTemplate = + | 'blank' // « Page blanche — observatoire d'abord » : one-clause Pact + A1, Consent protocol ONLY + | 'informal' + | 'association' + | 'cooperative' + | 'commune' + | 'free-currency' // Ğ1 heritage — ONLY template where protocolByRange.large = inertial binary + | 'symmetric' // « Institution symétrique » — the founding militant gesture (MNP) + +export interface Collective { + id: Id + slug: string + name: string + color: string // hex accent + icon: string // i-lucide-* + template: CollectiveTemplate + isTransparent: boolean + pactDocId: Id // TextDoc role='pact' — settings AS a voted document + rootCircleId: Id + lineage?: { + // seeding trail: set automatically when importing a bundle from another collective + sourceSlug: string + exportedAt: ISODate + sha256: string + } // NO parentCollectiveId — federation is never a table + createdAt: ISODate + updatedAt: ISODate +} + +// ── The people ─────────────────────────────────────────────── +export interface Person extends Entity { + displayName: string + isMe: boolean // local profile (v2 single-seat; « mode atelier » for in-person capture) + attributes?: Record // SELF-declared attributes (free keys: 'heures/mois'…), + // edited in /donnees — feed ONLY the « Pour moi » card; + // never shown to others, never aggregated (invariant 3) + duniterAddress?: string // SS58 — reserved for Duniter auth (out of v2 scope) + wotStatus?: 'member' | 'smith' +} + +export type CircleKind = 'place' | 'theme' | 'team' + +export interface Circle extends Entity { + name: string + purpose: string + kind?: CircleKind // lieu / thème / équipe — iconography, grouping, suggestion order + // ONLY; never a right; non-blocking hint when a 'team' exceeds 12 + memberIds: Id[] // v2: nominative list = the ONLY membership rule (auditable) + parentCircleId?: Id // nesting (couches d'oignon) — root = rootCircle + domains: string[] // tags — SUGGESTION/discovery only, never a voting right +} +// Corpus of a decision = COMPUTABLE union: members of scoped circles +// ∪ named persons ∪ holders of mandates whose domain intersects. +// Every inclusion keeps its reason (Concern.reason) — auditable in one tap. + +// ── THE DECISION (pivot) ───────────────────────────────────── +export type Reversibility = 'easy' | 'costly' | 'irreversible' +export type Weight = 'light' | 'binding' | 'structural' + +export type DecisionRoute = + | 'solo' // « Je décide » (optional trace outside mandate) + | 'mandate' // « Je décide, sous mandat » (mandatory trace + objection window) + | 'transmit' // « Je transmets » — someone else's mandate covers + | 'advice' // « J'écoute, puis je décide » + | 'collective' // « Nous décidons » (protocol modality) + | 'record' // « Déjà tranché — je le consigne » : the observatory in one gesture, adopted immediately + +export type DecisionStatus = + | 'draft' + | 'advice' + | 'objection' + | 'framing' + | 'voting' + | 'adopted' + | 'rejected' + | 'revoked' + | 'transmitted' + | 'closed' + +export type TriageRule = 'R-U' | 'R0a' | 'R0b' | 'R0c' | 'R2' | 'R3' | 'R4' | 'R5' | 'R6' + +// The matter — « il n'est pas consulté, il est instruit » +export interface Effect { + label: string // sought effect, one line + target?: string // optional measurable target (« ≤ 400 € », « +5 membres ») + measured?: { note: string; at: ISODate; byId: Id } // observed — filled by measurerIds at review time +} +export interface Brief { + context?: string + sources?: { label: string; url: string }[] + symptomsVsCauses?: string // doctrinal qualification of the subject + effects: Effect[] // GUARD (state.ts, opening of a COLLECTIVE session only): + // required when weight ∈ Pact 'triage.requireEffects' ('binding' covers binding+structural); + // structural under guard ⇒ ≥1 effect with target. NEVER for weight 'light', + // NEVER in the tunnel, NEVER for solo/record. +} + +// The linked resource decision — « chaque décision implique une allocation » +export interface Resources { + note: string // « Ce que ça engage » in one sentence + amount?: number + unit?: 'heures' | '€' | 'DU' | string // oil/water: units NEVER converted nor summed together +} + +// The collective tuning — « raisonner curseur / table de mixage » +export type ParamKind = 'slider' | 'share' // 'curve' reserved for v3 (full SejeteralO) +export interface ParamDef { + key: string + label: string // MANDATORY business label — never raw a, b, c + kind: ParamKind + min: number + max: number + step: number + unit?: string + baseline?: number // status quo — hollow marker always displayed + derived?: boolean // resolved by the constraint, never voted — labelled « calculé » +} +export interface ParamSpec { + params: ParamDef[] // ≤ 7 (SejeteralO lesson: small manipulable space) + constraint: 'none' | 'sum100' // sum100: 'share' params total 100; EXACTLY ONE 'derived' + // share required (validated at spec creation) — the absorption variable, resolved + // linearly (100 − Σ others) + impactAttrKey?: string // key of Person.attributes → « Pour moi » card ('linear-share') +} + +export type ChainKind = 'ratification' | 'revision' | 'revocation' | 'element' + +export interface Decision extends Entity { + authorId: Id + title: string // THE capture sentence — only mandatory field + body?: string // progressive disclosure + brief?: Brief // matter — see opening guard above + baselineNote?: string // « Aujourd'hui : … » — suggested on advice/collective routes, never solo + resources?: Resources // SUGGESTED-unfolded in the tunnel (never blocking); REQUIRED by the + // window/session opening guard (state.ts) when weight != 'light' outside solo + tags: string[] // discovery + recurrence — NEVER a corpus + + // — triage verdict (pre-filled by the engine, adjustable) — + reversibility: Reversibility + weight: Weight + urgent: boolean + scope: { + selfOnly: boolean + circleIds: Id[] // floor: widening free, NEVER narrowing below the computation + personIds: Id[] + } + route: DecisionRoute + triageRule: TriageRule // relegated to the « pourquoi ? » disclosure, PV, journal — never on the card + routeOverridden: boolean + overrideNote?: string // MANDATORY when lightening (derogation asymmetry) + scopeKeptNote?: string // influx reached + perimeter kept ⇒ mandatory PUBLIC motivated note + + // — chained specializations (the generative folding) — + underMandateId?: Id // decided UNDER this mandate → the mandate's « feux de la rampe » + createsMandate?: MandateDraft + amendsClauseId?: Id + protocolId?: Id // resolved by the Pact, adjustable by weighting up + paramSpec?: ParamSpec // when parametric modality (« Réglage collectif ») + parentDecisionId?: Id + chainKind?: ChainKind + // 'element': micro-decision of a split dossier (« décision incrémentielle »). + // Parent of elements: framing→closed transition guarded by + // « all element children terminal » — steward GESTURE (Fil card), never automatic; + // the closing cartography becomes the dossier's PV. + decidedHow?: string // route 'record': « comment ça s'est décidé », one sentence + + // — time & windows — + status: DecisionStatus + windowEndsAt?: ISODate + windowSuspendedAt?: ISODate // boundary objection → countdown SUSPENDED (visible) + decidedAt?: ISODate + review?: Review // mandatory when irreversible || structural + sunsetAt?: ISODate + stewardIds: Id[] // stewards — REALIZE (and crystallize parametric, close dossiers) + measurerIds: Id[] // measurers — OBSERVE (fill Effect.measured): the doctrinal pair + + // — proof & visibility — + visibility: 'private' | 'scope' | 'collective' // anti-surveillance: trace owned outside mandate + engraving?: Engraving +} + +export interface Review { + dueAt: ISODate + verdict?: 'confirmed' | 'revise' | 'revoke' // UI: « Ça tient / À revoir / À révoquer » + note?: string + decidedAt?: ISODate +} + +export interface Engraving { + sha256: string + engravedAt: ISODate + proofLevel: 'local' // v2: ONLY 'local' — « empreinte locale — démo » + ipfsCid?: string // reserved v3 — never displayed until real + chainRef?: string +} + +// ── Inclusion & windows ────────────────────────────────────── +export interface Concern extends Entity { + decisionId: Id + personId: Id + origin: 'computed' | 'declared' // UI: « concerné·e en premier lieu » / « en second lieu » + reason: string // « membre du cercle Forgerons » — shown on tap + declaredNote?: string + beforeSnapshot: boolean // before the list is arrested ⇒ vote; after ⇒ consultative voice + priority?: 0 | 1 | 2 | 3 // « pondère tes enjeux » — per element of a dossier; + // feeds the closing cartography, never the voting right +} + +export interface Assent extends Entity { + // « Ça me va » STORED — the explicit agreement of objection windows + decisionId: Id + personId: Id + recordedById?: Id // mode atelier +} +// OBJECTION WINDOW RULE (state.ts): at deadline with no open objection — +// easy ⇒ adopted (silence counts as agreement); costly/irreversible ⇒ adopted ONLY if +// ≥1 Assent from a concerned person ≠ author, otherwise EXTENSION by one notch +// (+objectionWindowHours, Fil reminder) — never adoption by pure silence outside easy. + +export interface Objection extends Entity { + decisionId: Id + personId: Id + kind: 'content' | 'boundary' // boundary SUSPENDS the window — the frontier precedes substance + argument: string + status: 'open' | 'withdrawn' | 'integrated' | 'escalated' + resolutionNote?: string + recordedById?: Id // mode atelier: « saisi par X pour Y — atelier du » +} + +export interface Advice extends Entity { + decisionId: Id + personId: Id + position: 'favorable' | 'reserved' | 'unfavorable' + note?: string + recordedById?: Id // mode atelier +} + +// ── The power ──────────────────────────────────────────────── +export type NominationMethod = + | 'ratified-self' + | 'election-no-candidate' + | 'nuanced-vote' + | 'consent' + | 'draw' + | 'rotation' + +export interface Mandate extends Entity { + title: string + holderId: Id + originDecisionId: Id // the mandate IS born of a decision — clickable + domain: { circleIds: Id[]; tags: string[] } + startsAt: ISODate + endsAt: ISODate // always bounded + electorCircleId: Id // election corpus = revocation corpus + nominationMethod: NominationMethod + reports: MandateReport[] + status: 'proposed' | 'active' | 'expired' | 'revoked' + // NO aggregate/gauge: the sheet shows counted facts (« Exercice du mandat »), + // never synthesized; the public stream = « Feux de la rampe » +} +export interface MandateReport { + dueAt: ISODate + deliveredAt?: ISODate + content?: string +} +export interface MandateDraft { + title: string + domainCircleIds: Id[] + domainTags: string[] + durationDays: number + reportEveryDays?: number +} + +// ── The texts ──────────────────────────────────────────────── +export interface TextDoc extends Entity { + slug: string + title: string + role: 'pact' | 'reference' + description: string // seeded Pact: « Notre contrat social — sacralisé, jamais immuable » + provenance?: Provenance +} +export interface Provenance { + sources: { title: string; url: string; date?: string; version?: string }[] + voteRecord?: { + url: string + modeParams: string + period: string + result: { + for: number + against: number + invalid?: number + wotSize: number + thresholdRequired: number + status: string + } + } + contributors?: string[] + notes?: string // typos of voted texts: FLAGGED, never fixed without a vote +} + +export type InertiaPreset = 'low' | 'standard' | 'high' | 'max' +// REAL wiring: low={M:50,G:0.1} standard={M:50,G:0.2} high={M:60,G:0.4} max={M:66,G:0.6} (B=0.1, C=0) + +export interface Clause extends Entity { + docId: Id + section: string + position: number + code: string + title: string + inertia: InertiaPreset + currentVersionId?: Id + settingKey?: string // Pact clauses: e.g. 'triage.smallGroupMax' +} + +export interface ClauseVersion extends Entity { + clauseId: Id + decisionId: Id // EVERY version is born of a decision (founding seeded ones included) + versionLabel: string + content: string + settingValue?: Json + status: 'current' | 'proposed' | 'superseded' | 'rejected' + adoptedAt?: ISODate +} + +// ── Modalities & vote ──────────────────────────────────────── +export type VoteMethod = 'consent' | 'binary' | 'nuanced' | 'parametric' | 'election' +// UI: consentement · pour/contre (last-resort tool) · nuancé · +// « Réglage collectif » (au curseur) · élection + +export interface Protocol extends Entity { + name: string + method: VoteMethod + description: string + durationDays: number + ballot: 'open' | 'secret' // decided by the Pact — default 'open' (feux de la rampe). + // v2 single-seat: « secret d'affichage — démo » (aggregate tallies and anonymized + // comments only); real crypto arrives with sync (v3) + formula: FormulaParams + modeParams: string // DERIVED display DSL: 'D30M50B.1G.2S.1' — never hand-edited + pactClauseId?: Id // referenced by the Pact ⇒ changing it = amending the Pact +} +export interface FormulaParams { + majorityPct: number // M + baseExponent: number // B (default 0.1) + gradientExponent: number // G — the inertia + constantBase: number // C (default 0) + smithExponent?: number + techcommExponent?: number + nuancedMinParticipants?: number + nuancedThresholdPct?: number + parametricMinParticipants?: number // collective-tuning quorum — not reached ⇒ rejected at the gesture + electionMinParticipants?: number // election quorum — not reached ⇒ rejected + tieBreak?: 'runoff' | 'draw' // election tie-break DECIDED IN ADVANCE by the Pact + // (default 'runoff': chained runoff session among tied; 'draw': draw among tied — + // never chosen by the engine) +} + +export interface VoteSession extends Entity { + decisionId: Id + protocolId: Id + corpusPersonIds: Id[] // SNAPSHOT at opening — UI: « liste arrêtée le … » + corpusSize: number // W — never 0 again (letter reserved to the Atelier) + opensAt: ISODate + closesAt: ISODate + status: 'open' | 'frozen' | 'closed' + // 'frozen': PARAMETRIC only — at closesAt votes are frozen, the frozen median is shown, + // and the session WAITS for the steward's human gesture (Fil reminder if late). + crystallizedById?: Id // the « Cristalliser » gesture: who, when — dated and signed + crystallizedAt?: ISODate + outcome?: 'adopted' | 'rejected' | 'tie' + // 'tie': tied election — the tool NEVER breaks a tie: chained runoff session + // (restricted to tied) or draw IF the protocol planned it (tieBreak). + // Consent/nuanced/binary: open→closed automatic at closesAt (the engine only observes a + // threshold over human-written content). Parametric: open→frozen→closed (gesture). + // INVARIANT: tallies, histograms AND medians ALWAYS recomputed from Vote[] — never denormalized. +} + +export type NuancedValue = 0 | 1 | 2 | 3 | 4 | 5 + +export interface Vote extends Entity { + sessionId: Id + voterId: Id + value?: 'for' | 'against' | NuancedValue + // nuancé UI: 0 Pas du tout · 1 Plutôt non · 2 Réservé · 3 Plutôt oui · 4 Oui · 5 Tout à fait + values?: number[] // parametric — order of paramSpec.params EXCLUDING derived + choicePersonId?: Id // election — designation; a vote without choicePersonId = BLANK + // (counts for participation, not designation) + // INVARIANT: EXACTLY ONE of value / values / choicePersonId per method (validated by the + // store) — except an election blank (none of the three, method 'election' only) + comment?: string // MANDATORY when negative (against, 0, 1) + supersedesVoteId?: Id // audited re-vote — PRIVACY: the chain is NEVER displayed publicly + // under a voter's name; visible to its AUTHOR only; even with ballot 'open', only the + // LAST active vote appears under a name, aggregate tallies recomputed + // (« pardonner les positions ») + recordedById?: Id // mode atelier +} + +// ── Resolved Pact settings (pure function — NOT a settings table) ── +export interface CollectiveSettings { + // resolveSettings(pactClauses, versions) + triage: { + smallGroupMax: number // default 5 + collectiveMin: number // default 50 + consentMax: number // default 7 + objectionWindowHours: number // default 48 + adviceWindowHours: number // default 72 + framingDays: number // default 14 — « s'instruire et formuler des contre-propositions » + concernEscalateRatio: number // default 0.5 — reached ⇒ MANDATORY handling (state.ts guard) + recurrenceThreshold: number // default 3 + reviewDelayDays: number // default 90 + requireEffects: 'none' | 'structural' | 'binding' + // key 'triage.requireEffects' — governs the matter guard of COLLECTIVE sessions: + // 'binding' = binding AND structural (default of structured templates); + // 'structural' = structural only; 'none' seeded in blank and informal. + // Under guard, structural ⇒ ≥1 effect with measurable target. + } + protocolByRange: { + consent: Id // MANDATORY — invariant: every collective has a Consent protocol + // (every template seeds it, import validates it) + nuanced?: Id + large?: Id // ex-inertialBinary: the name no longer presumes the method. + // Standard templates → nuanced; free-currency → inertial binary + parametric?: Id // « Réglage collectif » + election?: Id + clauseByInertia?: Record // the Ğ1 heritage lives here, intact + } + // SPECIFIED FALLBACK (resolveSettings): any unresolved optional key falls back to consent; + // consent unfindable (corrupted bundle) ⇒ triage routes 'advice' with the banner + // « Aucun protocole — crée-le ou décide sur avis » — NEVER a crash. +} + +// ── Triage engine contracts ────────────────────────────────── +export interface TriageInput { + title: string + tags: string[] + scope: Decision['scope'] + reversibility: Reversibility + weight: Weight + urgent: boolean + amendsClauseId?: Id +} +export interface TriageContext { + // assembled by the store, consumed pure — testable without I/O + myActiveMandates: Mandate[] + otherActiveMandates: Mandate[] + matchingClauses: Clause[] // Q0 index « déjà décidé ? » — the SAME index as Cmd+K + similarRecentCount: number // adopted decisions sharing ≥2 tags over 90 d + similarRecordedCount: number // 'record' entries sharing ≥2 tags — maturation + computedConcernedIds: Id[] +} +export interface Verdict { + route: DecisionRoute + rule: TriageRule + explanation: string // ONE French sentence — displayed ALONE, without rule code + protocolId?: Id + windowHours?: number + framingDays?: number + reviewRequired: boolean + engravingSuggested: boolean + conservatoryChain?: boolean + parametricHint?: boolean // number/%/amount detected ⇒ « C'est un réglage » put forward + alternatives: { route: DecisionRoute; label: string; cost: string }[] + // always present: « C'est un réglage — décidez au curseur » (parametric); + // « Déjà tranché — je le consigne » (record); binary with its cost (« deux camps, un perdant ») + suggestion?: { kind: 'claim-mandate' | 'create-rule' | 'protocolize'; prefill: Json } +} + +// ── Local persistence & future sync ────────────────────────── +export interface Bundle { + // export/import AND seeds — the SAME code path + schemaVersion: 2 + exportedAt: ISODate + collective: Collective + people: Person[] + circles: Circle[] + decisions: Decision[] + concerns: Concern[] + objections: Objection[] + advices: Advice[] + assents: Assent[] + mandates: Mandate[] + docs: TextDoc[] + clauses: Clause[] + versions: ClauseVersion[] + protocols: Protocol[] + sessions: VoteSession[] + votes: Vote[] +} +// data/persistence.ts: IndexedDB (idb-keyval), key `ld2:`, 500 ms debounce. +// importBundle() validates protocolByRange.consent + sets Collective.lineage for foreign bundles. +// Sync-ready: UUIDv7, updatedAt (LWW), archivedAt, Vote/Advice/Objection/Assent append-only. +// Pinia store contract unchanged — the future FastAPI adapter changes NO screen. +// Contract notes: ballot 'secret' must become cryptographic server-side (v3); +// the privacy of supersedesVoteId chains (author-visible only) is a contract invariant. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e54213e..ee8a83e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "@unocss/nuxt": "^66.6.0", "@vueuse/nuxt": "^14.2.1", "@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0", + "idb-keyval": "^6.3.0", "nuxt": "^4.3.1", "pinia": "^3.0.2", "vue": "^3.5.28", @@ -25,7 +26,8 @@ "devDependencies": { "@iconify-json/lucide": "^1.2.91", "typescript": "^5.9.3", - "unocss": "^66.6.0" + "unocss": "^66.6.0", + "vitest": "^4.1.10" } }, "node_modules/@alloc/quick-lru": { @@ -7767,6 +7769,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -7776,6 +7789,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -8378,6 +8398,129 @@ "vue": "^3.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@volar/language-core": { "version": "2.4.28", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", @@ -9355,6 +9498,16 @@ "node": ">=10" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-kit": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", @@ -9914,6 +10067,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -11254,6 +11417,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -12271,6 +12444,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb-keyval": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz", + "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", + "license": "Apache-2.0" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -17300,6 +17479,13 @@ "node": ">=20" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -17541,6 +17727,13 @@ "node": ">=20.16.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -17974,6 +18167,13 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -17999,6 +18199,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -19838,6 +20048,103 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -20107,6 +20414,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index faddad3..3a4034d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,8 @@ "dev": "nuxt dev", "generate": "nuxt generate", "preview": "nuxt preview", - "postinstall": "nuxt prepare" + "postinstall": "nuxt prepare", + "test": "vitest run" }, "dependencies": { "@nuxt/content": "^3.11.2", @@ -19,6 +20,7 @@ "@unocss/nuxt": "^66.6.0", "@vueuse/nuxt": "^14.2.1", "@yvv/nuxt-base": "git+ssh://gitea@git.open.us.org/yvv/yvv-nuxt-base.git#v0.1.0", + "idb-keyval": "^6.3.0", "nuxt": "^4.3.1", "pinia": "^3.0.2", "vue": "^3.5.28", @@ -27,6 +29,7 @@ "devDependencies": { "@iconify-json/lucide": "^1.2.91", "typescript": "^5.9.3", - "unocss": "^66.6.0" + "unocss": "^66.6.0", + "vitest": "^4.1.10" } } diff --git a/frontend/tests/engine/modeParams.spec.ts b/frontend/tests/engine/modeParams.spec.ts new file mode 100644 index 0000000..051e1e9 --- /dev/null +++ b/frontend/tests/engine/modeParams.spec.ts @@ -0,0 +1,127 @@ +/** + * Ported from backend/app/tests/test_mode_params.py — every pytest case + * exists here with the same expected values, plus tests for + * formatModeParams() (no Python counterpart: defined as the canonical + * inverse of parseModeParams, matching the seed DSL strings). + */ +import { describe, expect, it } from 'vitest' +import { formatModeParams, parseModeParams } from '../../app/engine/modeParams' + +describe('parseModeParams', () => { + it('D30M50B.1G.2 => standard Licence G1 params', () => { + const result = parseModeParams('D30M50B.1G.2') + expect(result.duration_days).toBe(30) + expect(result.majority_pct).toBe(50) + expect(result.base_exponent).toBe(0.1) + expect(result.gradient_exponent).toBe(0.2) + // Optional criteria absent + expect(result.smith_exponent).toBeNull() + expect(result.techcomm_exponent).toBeNull() + }) + + it('D30M50B.1G.2S.1 => standard + smith_exponent=0.1', () => { + const result = parseModeParams('D30M50B.1G.2S.1') + expect(result.duration_days).toBe(30) + expect(result.majority_pct).toBe(50) + expect(result.base_exponent).toBe(0.1) + expect(result.gradient_exponent).toBe(0.2) + expect(result.smith_exponent).toBe(0.1) + expect(result.techcomm_exponent).toBeNull() + }) + + it('D30M50B.1G.2T.1 => standard + techcomm_exponent=0.1', () => { + const result = parseModeParams('D30M50B.1G.2T.1') + expect(result.duration_days).toBe(30) + expect(result.majority_pct).toBe(50) + expect(result.base_exponent).toBe(0.1) + expect(result.gradient_exponent).toBe(0.2) + expect(result.smith_exponent).toBeNull() + expect(result.techcomm_exponent).toBe(0.1) + }) + + it('D30M50B1G.5C10 => integer base, gradient=0.5, constant=10', () => { + const result = parseModeParams('D30M50B1G.5C10') + expect(result.duration_days).toBe(30) + expect(result.majority_pct).toBe(50) + expect(result.base_exponent).toBe(1.0) + expect(result.gradient_exponent).toBe(0.5) + expect(result.constant_base).toBe(10.0) + }) + + it('empty string returns all defaults', () => { + const result = parseModeParams('') + expect(result.duration_days).toBe(30) + expect(result.majority_pct).toBe(50) + expect(result.base_exponent).toBe(0.1) + expect(result.gradient_exponent).toBe(0.2) + expect(result.constant_base).toBe(0.0) + expect(result.smith_exponent).toBeNull() + expect(result.techcomm_exponent).toBeNull() + expect(result.ratio_multiplier).toBeNull() + expect(result.is_ratio_mode).toBe(false) + }) + + it('whitespace-only string treated as empty', () => { + const result = parseModeParams(' ') + expect(result.duration_days).toBe(30) + }) + + it('result exposes exactly the expected keys', () => { + const result = parseModeParams('D30M50B.1G.2') + const expectedKeys = [ + 'duration_days', + 'majority_pct', + 'base_exponent', + 'gradient_exponent', + 'constant_base', + 'smith_exponent', + 'techcomm_exponent', + 'ratio_multiplier', + 'is_ratio_mode', + ].sort() + expect(Object.keys(result).sort()).toEqual(expectedKeys) + }) + + it('unknown code letter throws (Python parity)', () => { + expect(() => parseModeParams('D30X5')).toThrow(/inconnu/) + }) + + it('D7R1N2.5 => ratio mode with multiplier (Python cross-check)', () => { + const result = parseModeParams('D7R1N2.5') + expect(result.duration_days).toBe(7) + expect(result.is_ratio_mode).toBe(true) + expect(result.ratio_multiplier).toBe(2.5) + // Untouched defaults + expect(result.majority_pct).toBe(50) + expect(result.base_exponent).toBe(0.1) + expect(result.gradient_exponent).toBe(0.2) + }) +}) + +describe('formatModeParams', () => { + it('defaults format to the canonical standard string', () => { + expect(formatModeParams()).toBe('D30M50B.1G.2') + expect(formatModeParams({})).toBe('D30M50B.1G.2') + }) + + it('round-trips the seed DSL strings', () => { + for (const s of ['D30M50B.1G.2', 'D30M50B.1G.2S.1', 'D30M50B.1G.2T.1']) { + expect(formatModeParams(parseModeParams(s))).toBe(s) + } + }) + + it('formats integer base and constant', () => { + expect(formatModeParams(parseModeParams('D30M50B1G.5C10'))).toBe('D30M50B1G.5C10') + }) + + it('formats ratio mode and multiplier', () => { + expect(formatModeParams(parseModeParams('D7R1N2.5'))).toBe('D7M50B.1G.2N2.5R1') + }) + + it('parse(format(p)) is identical to p', () => { + for (const s of ['D30M50B.1G.2S.1', 'D30M50B1G.5C10', 'D7R1N2.5', 'D90M66B.2G.3S.2T.1']) { + const p = parseModeParams(s) + expect(parseModeParams(formatModeParams(p))).toEqual(p) + } + }) +}) diff --git a/frontend/tests/engine/nuanced.spec.ts b/frontend/tests/engine/nuanced.spec.ts new file mode 100644 index 0000000..6f5052a --- /dev/null +++ b/frontend/tests/engine/nuanced.spec.ts @@ -0,0 +1,118 @@ +/** + * Ported from backend/app/tests/test_nuanced.py — every pytest case + * exists here with the same expected values. + * + * Levels: 0-CONTRE, 1-PAS DU TOUT, 2-PAS D'ACCORD, 3-NEUTRE, 4-D'ACCORD, 5-TOUT A FAIT + * Positive = levels 3 + 4 + 5 + * Adoption requires: positive_pct >= threshold (80%) AND total >= min_participants (59). + */ +import { describe, expect, it } from 'vitest' +import { nuancedResult } from '../../app/engine/nuanced' + +/** Build a votes array from level repetitions, e.g. fill([5, 20], [4, 20]). */ +function fill(...groups: Array<[level: number, count: number]>): number[] { + const votes: number[] = [] + for (const [level, count] of groups) { + for (let i = 0; i < count; i++) votes.push(level) + } + return votes +} + +describe('nuancedResult — adoption', () => { + it('59 positive + 10 negative = 69 total => adopted (85.51% >= 80%)', () => { + const votes = fill([5, 20], [4, 20], [3, 19], [2, 5], [1, 3], [0, 2]) + const result = nuancedResult(votes, 80, 59) + + expect(result.total).toBe(69) + expect(result.positive_count).toBe(59) + expect(result.positive_pct).toBeCloseTo(85.51, 1) + // Python cross-check: round(59/69*100, 2) == 85.51 + expect(result.positive_pct).toBe(85.51) + expect(result.threshold_met).toBe(true) + expect(result.min_participants_met).toBe(true) + expect(result.adopted).toBe(true) + }) + + it('all 59 voters at level 5 => 100% positive, adopted', () => { + const votes = fill([5, 59]) + const result = nuancedResult(votes, 80, 59) + + expect(result.total).toBe(59) + expect(result.positive_count).toBe(59) + expect(result.positive_pct).toBe(100.0) + expect(result.adopted).toBe(true) + }) +}) + +describe('nuancedResult — rejection', () => { + it('40 positive + 30 negative = 70 total => threshold not met (57.14% < 80%)', () => { + const votes = fill([5, 15], [4, 15], [3, 10], [2, 10], [1, 10], [0, 10]) + const result = nuancedResult(votes, 80, 59) + + expect(result.total).toBe(70) + expect(result.positive_count).toBe(40) + expect(result.positive_pct).toBeCloseTo(57.14, 1) + expect(result.threshold_met).toBe(false) + expect(result.min_participants_met).toBe(true) // 70 >= 59 + expect(result.adopted).toBe(false) + }) + + it('55 total < 59 min participants => rejected despite 90.9% positive', () => { + const votes = fill([5, 30], [4, 10], [3, 10], [1, 3], [0, 2]) + const result = nuancedResult(votes, 80, 59) + + expect(result.total).toBe(55) + expect(result.positive_count).toBe(50) + expect(result.positive_pct).toBeGreaterThan(80) + expect(result.threshold_met).toBe(true) + expect(result.min_participants_met).toBe(false) + expect(result.adopted).toBe(false) + }) +}) + +describe('nuancedResult — edge cases', () => { + it('exactly 80% positive votes passes the threshold', () => { + // 80 positive out of 100 = exactly 80% + const votes = fill([5, 40], [4, 20], [3, 20], [2, 10], [1, 5], [0, 5]) + const result = nuancedResult(votes, 80, 59) + + expect(result.total).toBe(100) + expect(result.positive_count).toBe(80) + expect(result.positive_pct).toBe(80.0) + expect(result.threshold_met).toBe(true) + expect(result.min_participants_met).toBe(true) + expect(result.adopted).toBe(true) + }) + + it('79 positive out of 100 = 79% < 80% => rejected', () => { + const votes = fill([5, 39], [4, 20], [3, 20], [2, 11], [1, 5], [0, 5]) + const result = nuancedResult(votes, 80, 59) + + expect(result.total).toBe(100) + expect(result.positive_count).toBe(79) + expect(result.positive_pct).toBe(79.0) + expect(result.threshold_met).toBe(false) + expect(result.adopted).toBe(false) + }) + + it('zero votes => not adopted', () => { + const result = nuancedResult([], 80, 59) + expect(result.total).toBe(0) + expect(result.positive_count).toBe(0) + expect(result.positive_pct).toBe(0.0) + expect(result.adopted).toBe(false) + }) + + it('vote level outside 0-5 throws', () => { + expect(() => nuancedResult([5, 3, 6])).toThrow(/invalide/) + }) + + it('per-level breakdown is correct', () => { + const votes = [0, 1, 2, 3, 4, 5, 5, 4, 3] + const result = nuancedResult(votes, 50, 1) + + expect(result.per_level_counts).toEqual({ 0: 1, 1: 1, 2: 1, 3: 2, 4: 2, 5: 2 }) + expect(result.positive_count).toBe(6) // 2+2+2 + expect(result.total).toBe(9) + }) +}) diff --git a/frontend/tests/engine/threshold.spec.ts b/frontend/tests/engine/threshold.spec.ts new file mode 100644 index 0000000..d89dd6a --- /dev/null +++ b/frontend/tests/engine/threshold.spec.ts @@ -0,0 +1,174 @@ +/** + * Ported from backend/app/tests/test_threshold.py — every pytest case + * exists here with the same expected values. Exact integer expectations + * were cross-checked by executing the Python engine (backend/.venv). + * + * Real-world reference case: + * Vote Engagement Forgeron v2.0.0 (Feb 2026) + * wotSize=7224, votesFor=97, votesAgainst=23, total=120 + * params M=50, B=0.1, G=0.2 => threshold=94 => adopted (97 >= 94) + */ +import { describe, expect, it } from 'vitest' +import { smithThreshold, techcommThreshold, wotThreshold } from '../../app/engine/threshold' + +// --------------------------------------------------------------------------- +// WoT threshold: real-world vote Forgeron +// --------------------------------------------------------------------------- + +describe('wotThreshold — Forgeron reference case', () => { + it('forgeron vote passes (97 for out of 120 total, wot=7224)', () => { + const threshold = wotThreshold(7224, 120, 50, 0.1, 0.2) + // With low participation (120/7224 ~ 1.66%), near-unanimity is required. + // The historical threshold was 94, and 97 >= 94. + expect(97).toBeGreaterThanOrEqual(threshold) + // The threshold should be high relative to total votes (inertia effect) + expect(threshold).toBeGreaterThan(60) + }) + + it('forgeron threshold value is in a reasonable range', () => { + const threshold = wotThreshold(7224, 120, 50, 0.1, 0.2) + // At ~1.66% participation, inertia should push threshold close to 78-95% + // of total votes. + expect(threshold).toBeGreaterThanOrEqual(80) + expect(threshold).toBeLessThanOrEqual(120) + }) + + it('forgeron threshold is exactly 94 (Python cross-check)', () => { + expect(wotThreshold(7224, 120, 50, 0.1, 0.2)).toBe(94) + }) +}) + +// --------------------------------------------------------------------------- +// WoT threshold: low participation +// --------------------------------------------------------------------------- + +describe('wotThreshold — low participation', () => { + it('10 votes out of 7224 requires near-unanimity', () => { + const threshold = wotThreshold(7224, 10, 50, 0.1, 0.2) + // With participation ratio 10/7224 ~ 0.14%, threshold should be + // very close to totalVotes (near-unanimity). + expect(threshold).toBeGreaterThanOrEqual(9) + expect(threshold).toBeLessThanOrEqual(10) + // Python cross-check: exact value + expect(threshold).toBe(9) + }) +}) + +// --------------------------------------------------------------------------- +// WoT threshold: high participation +// --------------------------------------------------------------------------- + +describe('wotThreshold — high participation', () => { + it('3000 votes out of 7224 approaches simple majority', () => { + const threshold = wotThreshold(7224, 3000, 50, 0.1, 0.2) + // With ~42% participation, the inertia factor diminishes. + const simpleMajority = Math.ceil(3000 * 0.5) + expect(threshold).toBeGreaterThanOrEqual(simpleMajority) + // Should be noticeably less than near-unanimity + expect(threshold).toBeLessThan(2700) + // Python cross-check: exact value + expect(threshold).toBe(1742) + }) +}) + +// --------------------------------------------------------------------------- +// WoT threshold: edge cases +// --------------------------------------------------------------------------- + +describe('wotThreshold — edge cases', () => { + it('zero total votes => ceil(C + B^W)', () => { + const threshold = wotThreshold(7224, 0, 50, 0.1, 0.2) + // B^W = 0.1^7224 is effectively 0 + const expected = Math.ceil(0.0 + 0.1 ** 7224) + expect(threshold).toBe(expected) + expect(threshold).toBe(0) + }) + + it('throws on wotSize = 0', () => { + expect(() => wotThreshold(0, 10)).toThrow(/wotSize/) + }) + + it('throws on negative totalVotes', () => { + expect(() => wotThreshold(100, -1)).toThrow(/totalVotes/) + }) + + it('throws on majorityPct out of range', () => { + expect(() => wotThreshold(100, 10, 150)).toThrow(/majorityPct/) + }) +}) + +// --------------------------------------------------------------------------- +// WoT threshold: Python↔TS parity (values executed from backend/.venv) +// --------------------------------------------------------------------------- + +describe('wotThreshold — Python parity cases', () => { + it('T > W (100 wot, 150 votes) => 69', () => { + expect(wotThreshold(100, 150, 50, 0.1, 0.2)).toBe(69) + }) + + it('T = W (full participation) => simple majority 50', () => { + expect(wotThreshold(100, 100, 50, 0.1, 0.2)).toBe(50) + }) + + it('constantBase C=10 on Forgeron numbers => 96', () => { + expect(wotThreshold(7224, 120, 50, 0.1, 0.2, 10.0)).toBe(96) + }) + + it('majorityPct 80 on Forgeron numbers => 110', () => { + expect(wotThreshold(7224, 120, 80, 0.1, 0.2)).toBe(110) + }) + + it('minimal corpus W=1, T=1 => 1', () => { + expect(wotThreshold(1, 1, 50, 0.1, 0.2)).toBe(1) + }) + + it('W=10, T=5 => 3', () => { + expect(wotThreshold(10, 5, 50, 0.1, 0.2)).toBe(3) + }) +}) + +// --------------------------------------------------------------------------- +// Smith threshold +// --------------------------------------------------------------------------- + +describe('smithThreshold — ceil(smithSize ^ S)', () => { + it('smithSize=20, exponent=0.1 => ceil(20^0.1) = 2', () => { + const result = smithThreshold(20, 0.1) + expect(result).toBe(Math.ceil(20 ** 0.1)) + // 20^0.1 ~ 1.35, ceil => 2 + expect(result).toBe(2) + }) + + it('smithSize=1 => ceil(1^0.1) = 1', () => { + expect(smithThreshold(1, 0.1)).toBe(1) + }) + + it('throws on smithSize = 0', () => { + expect(() => smithThreshold(0)).toThrow() + }) + + it('smithSize=1000, exponent=0.5 => 32 (Python cross-check)', () => { + expect(smithThreshold(1000, 0.5)).toBe(32) + }) +}) + +// --------------------------------------------------------------------------- +// TechComm threshold +// --------------------------------------------------------------------------- + +describe('techcommThreshold — ceil(cotecSize ^ T)', () => { + it('cotecSize=5, exponent=0.1 => ceil(5^0.1) = 2', () => { + const result = techcommThreshold(5, 0.1) + expect(result).toBe(Math.ceil(5 ** 0.1)) + // 5^0.1 ~ 1.175, ceil => 2 + expect(result).toBe(2) + }) + + it('cotecSize=1 => 1', () => { + expect(techcommThreshold(1, 0.1)).toBe(1) + }) + + it('throws on cotecSize = 0', () => { + expect(() => techcommThreshold(0)).toThrow() + }) +}) diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..be5c169 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,15 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '~': fileURLToPath(new URL('./app', import.meta.url)), + '@': fileURLToPath(new URL('./app', import.meta.url)), + }, + }, + test: { + environment: 'node', + include: ['tests/**/*.spec.ts'], + }, +}) From f707b5f15d91c8a296df21e9084f03917f2b33d4 Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 11 Aug 2026 09:22:19 +0200 Subject: [PATCH 2/8] v2 : moteurs purs complets + ambiances + persistance + seed Atelier du Canal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/scripts/export_seed_bundle.py | 1617 +++++++ frontend/app/assets/css/moods.css | 465 +- frontend/app/data/persistence.ts | 179 + .../data/seeds/atelier-du-canal.bundle.json | 3841 +++++++++++++++++ frontend/app/engine/impact.ts | 101 + frontend/app/engine/parametric.ts | 416 ++ frontend/app/engine/settings.ts | 167 + frontend/app/engine/state.ts | 248 ++ frontend/app/engine/threshold.ts | 103 + frontend/app/engine/triage.ts | 341 ++ frontend/tests/engine/election.spec.ts | 168 + frontend/tests/engine/impact.spec.ts | 158 + frontend/tests/engine/parametric.spec.ts | 480 ++ frontend/tests/engine/settings.spec.ts | 226 + frontend/tests/engine/state.spec.ts | 623 +++ frontend/tests/engine/triage.spec.ts | 608 +++ frontend/tests/lexicon.spec.ts | 142 + frontend/tests/seeds/atelier-du-canal.spec.ts | 622 +++ 18 files changed, 10324 insertions(+), 181 deletions(-) create mode 100644 backend/scripts/export_seed_bundle.py create mode 100644 frontend/app/data/persistence.ts create mode 100644 frontend/app/data/seeds/atelier-du-canal.bundle.json create mode 100644 frontend/app/engine/impact.ts create mode 100644 frontend/app/engine/parametric.ts create mode 100644 frontend/app/engine/settings.ts create mode 100644 frontend/app/engine/state.ts create mode 100644 frontend/app/engine/triage.ts create mode 100644 frontend/tests/engine/election.spec.ts create mode 100644 frontend/tests/engine/impact.spec.ts create mode 100644 frontend/tests/engine/parametric.spec.ts create mode 100644 frontend/tests/engine/settings.spec.ts create mode 100644 frontend/tests/engine/state.spec.ts create mode 100644 frontend/tests/engine/triage.spec.ts create mode 100644 frontend/tests/lexicon.spec.ts create mode 100644 frontend/tests/seeds/atelier-du-canal.spec.ts diff --git a/backend/scripts/export_seed_bundle.py b/backend/scripts/export_seed_bundle.py new file mode 100644 index 0000000..ccb5cd4 --- /dev/null +++ b/backend/scripts/export_seed_bundle.py @@ -0,0 +1,1617 @@ +#!/usr/bin/env python3 +"""Export du seed backend v1 (backend/seed.py) vers un Bundle JSON v2. + +Émet frontend/app/data/seeds/duniter-g1.bundle.json au schéma Bundle v2 +(frontend/app/types/domain.ts), sans perte de contenu Ğ1. + +ISOLATION DES DONNÉES (aucune dépendance DB) : + Importer seed.py déclencherait `from app.database import …` (SQLAlchemy, + driver async, config). Ce script n'importe donc PAS seed.py : il le PARSE + avec `ast` et extrait les structures par évaluation littérale tolérante : + - littéraux au niveau module : GENESIS_CERTIFICATION, + ENGAGEMENT_CERTIFICATION_ITEMS (33), GENESIS_FORGERON, + ENGAGEMENT_FORGERON_ITEMS (59), VOTER_NAMES (11), RUNTIME_UPGRADE_STEPS ; + - littéraux internes aux fonctions : configs (FormulaConfigs ×7), + protocols (VotingProtocols ×3), mandates_data (×3), steps (licence), + orgs_data, kwargs des appels get_or_create / QualificationProtocol. + Les valeurs non littérales (ids SQLAlchemy, datetime.now()…) sont ignorées + par l'extracteur et recalculées côté v2 (dates historiques fixes, + uuid5 déterministes). + +REPRODUCTIBILITÉ : uuid5(NAMESPACE fixe, slug) partout + dates constantes + (aucun datetime.now) → deux exécutions produisent un JSON identique + octet à octet. + +ÉCARTS ASSUMÉS (voir rapport imprimé en fin d'exécution) : + - les 3 VoteSessions de démo v1 (10 pour / 1 contre simulés sur les 3 + premiers items forgeron) ne sont PAS portées : votes factices sans + contenu Ğ1 réel — le vrai vote (97/23/19) vit dans provenance.voteRecord ; + - l'organisation v1 'axiom-team' n'est pas exportée (un bundle = UN + collectif) ; + - icon 'i-lucide-network' (spec BLUEPRINT-V2 « Seeds ») remplace + 'i-lucide-globe' (v1) ; la description de l'organisation v1 est conservée + dans la provenance du Pacte ; + - v1 mappait TOUS les items cert sur « Vote WoT standard » (son + inertia_protocol_map pointait le même protocole pour les 4 presets) ; + v2 câble clauseByInertia par preset (règle R3 du blueprint) — les + FormulaConfigs v1 basse/haute/très haute deviennent des protocoles ; + - item_type v1 (preamble/section/clause/rule/verification) conservé dans + les tags de la décision fondatrice de chaque clause (`v1:`). + +Usage : backend/.venv/bin/python backend/scripts/export_seed_bundle.py +""" + +from __future__ import annotations + +import ast +import hashlib +import json +import uuid +from pathlib import Path + +HERE = Path(__file__).resolve() +BACKEND = HERE.parents[1] +REPO = HERE.parents[2] +SEED_PATH = BACKEND / "seed.py" +OUT_PATH = REPO / "frontend" / "app" / "data" / "seeds" / "duniter-g1.bundle.json" + +# ───────────────────────────────────────────────────────────── +# Extraction AST — aucun import de seed.py +# ───────────────────────────────────────────────────────────── + +_SKIP = object() + + +def _tolerant(node: ast.AST): + """Évaluation littérale tolérante : tout nœud non littéral est ignoré.""" + if isinstance(node, ast.Constant): + return node.value + if isinstance(node, ast.Dict): + out = {} + # (clé None = **unpacking — absent des littéraux extraits de seed.py) + for key_node, value_node in zip(node.keys, node.values): + if key_node is None: + continue + key = _tolerant(key_node) + value = _tolerant(value_node) + if key is _SKIP or value is _SKIP: + continue + out[key] = value + return out + if isinstance(node, (ast.List, ast.Tuple)): + return [v for v in (_tolerant(e) for e in node.elts) if v is not _SKIP] + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): + operand = _tolerant(node.operand) + return -operand if operand is not _SKIP else _SKIP + return _SKIP + + +def _assign_value(scope: ast.AST, name: str): + """Valeur littérale de la première assignation `name = …` dans le scope.""" + for node in ast.walk(scope): + target = None + if ( + isinstance(node, ast.Assign) + and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) + ): + target = node.targets[0].id + elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): + target = node.target.id + if target == name and getattr(node, "value", None) is not None: + value = _tolerant(node.value) + if value is not _SKIP: + return value + raise KeyError(f"assignation littérale introuvable : {name}") + + +def _function(tree: ast.Module, name: str) -> ast.AST: + for node in tree.body: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name: + return node + raise KeyError(f"fonction introuvable : {name}") + + +def _call_kwargs(scope: ast.AST, callee: str) -> dict: + """Kwargs littéraux du premier appel `callee(...)` du scope.""" + for node in ast.walk(scope): + if not isinstance(node, ast.Call): + continue + func = node.func + fname = func.id if isinstance(func, ast.Name) else getattr(func, "attr", None) + if fname != callee: + continue + kwargs = {} + for kw in node.keywords: + if kw.arg is None: + continue + value = _tolerant(kw.value) + if value is not _SKIP: + kwargs[kw.arg] = value + return kwargs + raise KeyError(f"appel introuvable : {callee}") + + +def _json_dumps_arg(scope: ast.AST): + """Premier argument littéral d'un appel json.dumps(...) dans le scope.""" + for node in ast.walk(scope): + if ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "dumps" + and node.args + ): + value = _tolerant(node.args[0]) + if value is not _SKIP: + return value + return None + + +_tree = ast.parse(SEED_PATH.read_text(encoding="utf-8")) + +GEN_CERT = _assign_value(_tree, "GENESIS_CERTIFICATION") +CERT_ITEMS = _assign_value(_tree, "ENGAGEMENT_CERTIFICATION_ITEMS") +GEN_FORGE = _assign_value(_tree, "GENESIS_FORGERON") +FORGE_ITEMS = _assign_value(_tree, "ENGAGEMENT_FORGERON_ITEMS") +VOTER_NAMES = _assign_value(_tree, "VOTER_NAMES") +RUNTIME_STEPS = _assign_value(_tree, "RUNTIME_UPGRADE_STEPS") + +FORMULAS_V1 = _assign_value(_function(_tree, "seed_formula_configs"), "configs") +PROTOCOLS_V1 = _assign_value(_function(_tree, "seed_voting_protocols"), "protocols") +MANDATES_V1 = _assign_value(_function(_tree, "seed_mandates"), "mandates_data") +ORGS_V1 = _assign_value(_function(_tree, "seed_organizations"), "orgs_data") +LICENCE_STEPS = _assign_value(_function(_tree, "seed_decision_licence_evolution"), "steps") + +_fn_qual = _function(_tree, "seed_qualification_protocol") +QUAL_V1 = _call_kwargs(_fn_qual, "QualificationProtocol") +QUAL_MODALITIES_V1 = _json_dumps_arg(_fn_qual) or [] + +CERT_DOC_META = _call_kwargs( + _function(_tree, "seed_document_engagement_certification"), "get_or_create" +) +FORGE_DOC_META = _call_kwargs( + _function(_tree, "seed_document_engagement_forgeron"), "get_or_create" +) +RUNTIME_META = _call_kwargs(_function(_tree, "seed_decision_runtime_upgrade"), "get_or_create") +LICENCE_META = _call_kwargs(_function(_tree, "seed_decision_licence_evolution"), "get_or_create") + +# ── Garde-fous de non-perte à l'extraction ─────────────────── +assert len(CERT_ITEMS) == 33, f"attendu 33 items Certification, extrait {len(CERT_ITEMS)}" +assert len(FORGE_ITEMS) == 59, f"attendu 59 items Forgeron, extrait {len(FORGE_ITEMS)}" +assert len(VOTER_NAMES) == 11, f"attendu 11 votants, extrait {len(VOTER_NAMES)}" +assert len(MANDATES_V1) == 3, f"attendu 3 mandats v1, extrait {len(MANDATES_V1)}" +assert len(FORMULAS_V1) == 7, f"attendu 7 FormulaConfigs, extrait {len(FORMULAS_V1)}" +assert len(PROTOCOLS_V1) == 3, f"attendu 3 VotingProtocols, extrait {len(PROTOCOLS_V1)}" +_vr = GEN_FORGE["vote_record"]["result"] +assert (_vr["pour"], _vr["contre"], _vr["nuls_invalides"]) == (97, 23, 19) +assert (_vr["wot_size"], _vr["threshold_required"]) == (7224, 97) + +# ───────────────────────────────────────────────────────────── +# Constantes v2 — ids déterministes et dates historiques fixes +# ───────────────────────────────────────────────────────────── + +NAMESPACE = uuid.uuid5(uuid.NAMESPACE_URL, "https://libredecision.org/seeds/duniter-g1") + + +def uid(slug: str) -> str: + return str(uuid.uuid5(NAMESPACE, slug)) + + +COLLECTIVE_ID = uid("collective:duniter-g1") +EXPORTED_AT = "2026-08-11T12:00:00Z" + +T_GENESIS = "2025-12-01T10:00:00Z" # création du collectif, adoption cert + Pacte +T_CERT_DRAFT = "2025-11-15T10:00:00Z" +T_FORGE_OPEN = "2026-01-07T09:00:00Z" # ouverture du vote forgeron (seed v1) +T_FORGE_ADOPT = "2026-02-06T18:00:00Z" # clôture/adoption (97/23, WoT 7224) +T_COMTECH_OPEN = "2026-02-04T09:00:00Z" +T_COMTECH_ADOPT = "2026-03-06T18:00:00Z" + +PID = {name: uid(f"person:{name.lower()}") for name in VOTER_NAMES} +SMITH_NAMES = VOTER_NAMES[:5] # v1 : is_smith=(i < 5) + +C_ROOT = uid("circle:toile-de-confiance") +C_FORGE = uid("circle:forgerons") +C_TECH = uid("circle:comite-technique") + +DOC_CERT = uid("doc:engagement-certification") +DOC_FORGE = uid("doc:engagement-forgeron") +DOC_TECH = uid("doc:engagement-comite-tech") +DOC_PACT = uid("doc:pacte-duniter-g1") + +P_CONSENT = uid("protocol:consentement") +P_WOT = uid("protocol:vote-wot-standard") +P_NUANCED = uid("protocol:vote-nuance") +P_SMITH = uid("protocol:vote-forgeron-smith") +P_TECHCOMM = uid("protocol:vote-comite-technique") +P_PARAMETRIC = uid("protocol:reglage-collectif") +P_ELECTION = uid("protocol:election-sans-candidat") +P_INERTIA_LOW = uid("protocol:inertie-basse") +P_INERTIA_HIGH = uid("protocol:inertie-haute") +P_INERTIA_MAX = uid("protocol:inertie-tres-haute") + +# Presets v1 → InertiaPreset v2 (spec : very_high → 'max') +PRESET_MAP = {"low": "low", "standard": "standard", "high": "high", "very_high": "max"} + +# R3 : protocole d'amendement d'une clause = clauseByInertia[preset] +PROTO_BY_INERTIA = { + "low": P_INERTIA_LOW, + "standard": P_WOT, + "high": P_INERTIA_HIGH, + "max": P_INERTIA_MAX, +} + + +def _fmt(value: float) -> str: + """0.1 → '.1', 50 → '50' (DSL modeParams, forme canonique).""" + if value == int(value): + return str(int(value)) + s = str(value) + return s[1:] if s.startswith("0.") else s + + +def entity(eid: str, created: str, updated: str | None = None, **fields) -> dict: + return { + "id": eid, + "collectiveId": COLLECTIVE_ID, + "createdAt": created, + "updatedAt": updated or created, + **fields, + } + + +# ───────────────────────────────────────────────────────────── +# Personnes (11, dont 5 smiths — v1 VOTER_NAMES) +# ───────────────────────────────────────────────────────────── + +people = [] +for i, name in enumerate(VOTER_NAMES): + # Même dérivation d'adresse que seed.py (sha256(name)[:32] préfixé '5') + address = "5" + hashlib.sha256(name.encode()).hexdigest()[:32] + people.append( + entity( + PID[name], + T_GENESIS, + displayName=name, + isMe=False, + duniterAddress=address, + wotStatus="smith" if i < 5 else "member", + ) + ) + +# ───────────────────────────────────────────────────────────── +# Cercles (3, typés kind) +# ───────────────────────────────────────────────────────────── + +COMTECH_MEMBERS = ["Elois", "Cgeek", "Maaltir", "Hugo", "Tuxmain"] # v1 : outcome de l'élection + +circles = [ + entity( + C_ROOT, + T_GENESIS, + name="Toile de confiance", + purpose="Tous les membres de la toile de confiance Ğ1 — le corps souverain du collectif.", + memberIds=[PID[n] for n in VOTER_NAMES], + domains=["toile", "certification", "licence"], + ), + entity( + C_FORGE, + T_GENESIS, + name="Forgerons", + purpose="Les forgerons (validateurs de blocs) de la blockchain Duniter V2 — la sous-toile Smith.", + kind="team", + memberIds=[PID[n] for n in SMITH_NAMES], + parentCircleId=C_ROOT, + domains=["forge", "noeuds", "runtime"], + ), + entity( + C_TECH, + T_GENESIS, + name="Comité Technique", + purpose="Le Comité Technique protège la souveraineté de la communauté sur les mises à jour de la blockchain.", + kind="team", + memberIds=[PID[n] for n in COMTECH_MEMBERS], + parentCircleId=C_ROOT, + domains=["runtime", "code", "on-chain"], + ), +] + +# ───────────────────────────────────────────────────────────── +# Protocoles (10) — FormulaParams complets depuis les FormulaConfigs v1 +# ───────────────────────────────────────────────────────────── + +_f_std = FORMULAS_V1["Standard Licence G1"] +_f_low = FORMULAS_V1["Inertie basse (Annexes)"] +_f_high = FORMULAS_V1["Inertie haute (Formule)"] +_f_vhigh = FORMULAS_V1["Inertie très haute (Méta-réglage)"] +_f_smith = FORMULAS_V1["Forgeron avec Smith"] +_f_tech = FORMULAS_V1["Comité Tech"] +_f_nuanced = FORMULAS_V1["Vote Nuance"] + + +def _formula(cfg: dict, **extra) -> dict: + return { + "majorityPct": cfg["majority_pct"], + "baseExponent": cfg["base_exponent"], + "gradientExponent": cfg["gradient_exponent"], + "constantBase": cfg["constant_base"], + **extra, + } + + +def _binary_mode(cfg: dict, suffix: str = "") -> str: + return ( + f"D{cfg['duration_days']}M{_fmt(cfg['majority_pct'])}" + f"B{_fmt(cfg['base_exponent'])}G{_fmt(cfg['gradient_exponent'])}{suffix}" + ) + + +def protocol(pid, name, method, description, days, formula, mode, pact_clause=None): + p = entity( + pid, + T_GENESIS, + name=name, + method=method, + description=description, + durationDays=days, + ballot="open", + formula=formula, + modeParams=mode, + ) + if pact_clause: + p["pactClauseId"] = uid(f"clause:pacte-duniter-g1:{pact_clause}") + return p + + +protocols = [ + protocol( + P_CONSENT, + "Consentement", + "consent", + "Ça me va / J'objecte — zéro objection argumentée maintenue vaut adoption. " + "La modalité par défaut des petits corps, et le repli universel de toute " + "clé de protocole absente. Hors du réversible, au moins un accord " + "explicite est requis à l'échéance.", + 7, + {"majorityPct": 100, "baseExponent": 0.1, "gradientExponent": 0, "constantBase": 0}, + "D7M100B.1G0", + pact_clause="M1", + ), + protocol( + P_WOT, + "Vote WoT standard", + "binary", + "L'outil de dernier recours des très grands corps — héritage Toile de " + "Confiance. " + PROTOCOLS_V1["Vote WoT standard"]["description"], + _f_std["duration_days"], + _formula(_f_std), + PROTOCOLS_V1["Vote WoT standard"]["mode_params"], + pact_clause="M2", + ), + protocol( + P_NUANCED, + "Vote nuancé", + "nuanced", + "Vote nuancé à 6 niveaux — 0 « Pas du tout » à 5 « Tout à fait » : chacun " + "se prononce en nuances, pas en camps. Histogramme des 6 niveaux visible " + "pendant et après le vote ; commentaire obligatoire sur 0-1. " + "(v1 : « " + _f_nuanced["description"] + " »)", + _f_nuanced["duration_days"], + _formula( + _f_nuanced, + nuancedMinParticipants=_f_nuanced["nuanced_min_participants"], + nuancedThresholdPct=_f_nuanced["nuanced_threshold_pct"], + ), + _binary_mode(_f_nuanced), + pact_clause="M3", + ), + protocol( + P_SMITH, + "Vote forgeron (Smith)", + "binary", + PROTOCOLS_V1["Vote forgeron (Smith)"]["description"], + _f_smith["duration_days"], + _formula(_f_smith, smithExponent=_f_smith["smith_exponent"]), + PROTOCOLS_V1["Vote forgeron (Smith)"]["mode_params"], + ), + protocol( + P_TECHCOMM, + "Vote Comité Technique", + "binary", + PROTOCOLS_V1["Vote Comité Technique"]["description"], + _f_tech["duration_days"], + _formula(_f_tech, techcommExponent=_f_tech["techcomm_exponent"]), + PROTOCOLS_V1["Vote Comité Technique"]["mode_params"], + ), + protocol( + P_PARAMETRIC, + "Réglage collectif", + "parametric", + "Décider au curseur — chacun pose sa valeur, la médiane basse éclaire, " + "le geste du garant cristallise. Quorum : 5 participants.", + 14, + { + "majorityPct": 50, + "baseExponent": 0.1, + "gradientExponent": 0, + "constantBase": 0, + "parametricMinParticipants": 5, + }, + "D14M50B.1G0", + pact_clause="M4", + ), + protocol( + P_ELECTION, + "Élection sans candidat", + "election", + "Désigner sans candidature — chacun nomme la personne qu'il estime la " + "mieux placée. Vote blanc possible (compte pour la participation, pas " + "pour la désignation). En cas d'égalité : session de départage entre " + "ex æquo — jamais l'outil.", + 14, + { + "majorityPct": 50, + "baseExponent": 0.1, + "gradientExponent": 0, + "constantBase": 0, + "electionMinParticipants": 5, + "tieBreak": "runoff", + }, + "D14M50B.1G0", + pact_clause="M5", + ), + protocol( + P_INERTIA_LOW, + "Inertie basse (Annexes)", + "binary", + _f_low["description"], + _f_low["duration_days"], + _formula(_f_low), + _binary_mode(_f_low), + pact_clause="N1", + ), + protocol( + P_INERTIA_HIGH, + "Inertie haute (Formule)", + "binary", + _f_high["description"], + _f_high["duration_days"], + _formula(_f_high), + _binary_mode(_f_high), + pact_clause="N3", + ), + protocol( + P_INERTIA_MAX, + "Inertie très haute (Méta-réglage)", + "binary", + _f_vhigh["description"], + _f_vhigh["duration_days"], + _formula(_f_vhigh), + _binary_mode(_f_vhigh), + pact_clause="N4", + ), +] + +# ───────────────────────────────────────────────────────────── +# TextDocs, Clauses, ClauseVersions, Décisions fondatrices +# ───────────────────────────────────────────────────────────── + +docs: list[dict] = [] +clauses: list[dict] = [] +versions: list[dict] = [] +decisions: list[dict] = [] + + +def _steps_md(steps: list[dict]) -> str: + lines = [] + for s in steps: + line = f"{s.get('step_order')}. **{s.get('title')}** — {s.get('description', '').strip()}" + if s.get("status"): + line += f" *(v1 : {s['status']}" + if s.get("outcome"): + line += f" — {s['outcome']}" + line += ")*" + lines.append(line) + return "\n".join(lines) + + +def add_doc(doc_id, slug, title, role, description, provenance, created, updated=None): + docs.append( + entity( + doc_id, + created, + updated, + slug=slug, + title=title, + role=role, + description=description, + provenance=provenance, + ) + ) + + +def add_clause( + *, + doc_slug: str, + doc_id: str, + code: str, + section: str, + position: int, + title: str, + inertia: str, + content: str, + version_label: str, + created: str, + adopted: str, + author: str, + protocol_id: str, + scope_circles: list[str], + tags: list[str], + setting_key: str | None = None, + setting_value=None, + body: str | None = None, +): + cid = uid(f"clause:{doc_slug}:{code}") + vid = uid(f"version:{doc_slug}:{code}:current") + did = uid(f"decision:fondatrice:{doc_slug}:{code}") + + clause = entity( + cid, + adopted, + docId=doc_id, + section=section, + position=position, + code=code, + title=title, + inertia=inertia, + currentVersionId=vid, + ) + if setting_key is not None: + clause["settingKey"] = setting_key + clauses.append(clause) + + version = entity( + vid, + created, + adopted, + clauseId=cid, + decisionId=did, + versionLabel=version_label, + content=content, + status="current", + adoptedAt=adopted, + ) + if setting_value is not None: + version["settingValue"] = setting_value + versions.append(version) + + decisions.append( + entity( + did, + created, + adopted, + authorId=PID[author], + title=f"Adoption fondatrice — {code}", + body=body + or ( + f"Adoption fondatrice de la clause {code} « {title} » " + f"(document {doc_slug}). Import du seed v1 — contenu en vigueur " + "dans la communauté Ğ1." + ), + tags=tags, + reversibility="costly", + weight="binding", + urgent=False, + scope={"selfOnly": False, "circleIds": scope_circles, "personIds": []}, + route="collective", + triageRule="R3", + routeOverridden=False, + amendsClauseId=cid, + protocolId=protocol_id, + status="adopted", + decidedAt=adopted, + stewardIds=[], + measurerIds=[], + visibility="collective", + ) + ) + return cid + + +def _sources_from_genesis(genesis: dict) -> list[dict]: + sources = [] + src = genesis.get("source_document", {}) + if src: + item = {"title": src["title"], "url": src["url"]} + if src.get("date"): + item["date"] = src["date"] + if src.get("version"): + item["version"] = src["version"] + sources.append(item) + if src.get("repo"): + sources.append({"title": "Dépôt git officiel (licence Ğ1)", "url": src["repo"]}) + for topic in genesis.get("forum_synthesis", []): + sources.append({"title": topic["title"], "url": topic["url"]}) + tools = genesis.get("référence_tools", {}) + tool_labels = { + "g1vote_repo": "g1vote-view (dépôt)", + "g1vote_live": "g1vote-view (live)", + "cesium": "Cesium", + "gecko": "Gecko", + "pad_source": "Pad source (charte forgeron)", + } + for key, label in tool_labels.items(): + if tools.get(key): + sources.append({"title": label, "url": tools[key]}) + return sources + + +def _forum_status_notes(genesis: dict) -> str: + lines = [] + for topic in genesis.get("forum_synthesis", []): + line = f"- {topic['title']} : {topic.get('status', '?')}" + if topic.get("posts"): + line += f" ({topic['posts']} messages)" + lines.append(line) + return "\n".join(lines) + + +# ── Doc 1 : Acte d'engagement Certification (33 clauses) ───── + +add_doc( + DOC_CERT, + "engagement-certification", + CERT_DOC_META["title"], + "reference", + CERT_DOC_META["description"], + { + "sources": _sources_from_genesis(GEN_CERT), + "contributors": [f"{c['name']} — {c['role']}" for c in GEN_CERT["contributors"]], + "notes": ( + "Déclencheur formule (v1) : " + + GEN_CERT["formula_trigger"] + + "\n\nÉtat des discussions forum au moment du seed v1 :\n" + + _forum_status_notes(GEN_CERT) + + f"\n\nDocument v1 : version {CERT_DOC_META['version']}, " + f"type {CERT_DOC_META['doc_type']}, statut {CERT_DOC_META['status']}." + ), + }, + T_GENESIS, +) + +for item in CERT_ITEMS: + preset = PRESET_MAP[item["inertia_preset"]] + add_clause( + doc_slug="engagement-certification", + doc_id=DOC_CERT, + code=item["position"], + section=item["section_tag"], + position=item["sort_order"], + title=item["title"], + inertia=preset, + content=item["current_text"], + version_label=CERT_DOC_META["version"], + created=T_CERT_DRAFT, + adopted=T_GENESIS, + author="Galuel", + protocol_id=PROTO_BY_INERTIA[preset], + scope_circles=[C_ROOT], + tags=["engagement-certification", item["section_tag"], f"v1:{item['item_type']}"], + ) + +# ── Doc 2 : Acte d'engagement forgeron (59 clauses v2.0.0-fr) ─ + +_vote_record_v1 = GEN_FORGE["vote_record"] +add_doc( + DOC_FORGE, + "engagement-forgeron", + FORGE_DOC_META["title"], + "reference", + FORGE_DOC_META["description"], + { + "sources": _sources_from_genesis(GEN_FORGE) + + [{"title": "g1vote — suivi du vote 33165", "url": _vote_record_v1["g1vote_url"]}], + "voteRecord": { + "url": _vote_record_v1["topic_url"], + "modeParams": _vote_record_v1["mode_params"], + "period": _vote_record_v1["period"], + "result": { + "for": _vr["pour"], + "against": _vr["contre"], + "invalid": _vr["nuls_invalides"], + "wotSize": _vr["wot_size"], + "thresholdRequired": _vr["threshold_required"], + "status": _vr["status"], + }, + }, + "contributors": [f"{c['name']} — {c['role']}" for c in GEN_FORGE["contributors"]], + "notes": ( + f"Critère Smith du vote v2.0.0-fr : {_vr['smith_pour']} forgerons pour / " + f"{_vr['smith_contre']} contre. Adresses de vote — pour : " + f"{_vote_record_v1['addresses']['pour']} ; contre : " + f"{_vote_record_v1['addresses']['contre']}.\n\n" + "Actions d'implémentation votées :\n- " + + "\n- ".join(GEN_FORGE["implementation_actions"]) + + "\n\nDéclencheur formule (v1) : " + + GEN_FORGE["formula_trigger"] + + "\n\nÉtat des discussions forum au moment du seed v1 :\n" + + _forum_status_notes(GEN_FORGE) + ), + }, + T_FORGE_OPEN, + T_FORGE_ADOPT, +) + +for item in FORGE_ITEMS: + preset = PRESET_MAP[item["inertia_preset"]] + add_clause( + doc_slug="engagement-forgeron", + doc_id=DOC_FORGE, + code=item["position"], + section=item["section_tag"], + position=item["sort_order"], + title=item["title"], + inertia=preset, + content=item["current_text"], + version_label=GEN_FORGE["source_document"]["version"], # '2.0.0-fr' + created=T_FORGE_OPEN, + adopted=T_FORGE_ADOPT, + author="Elois", + # v1 : tous les items forgeron votés au protocole Smith (double critère) + protocol_id=P_SMITH, + scope_circles=[C_ROOT, C_FORGE], + tags=["engagement-forgeron", item["section_tag"], f"v1:{item['item_type']}"], + ) + +# ── Doc 3 : Acte d'engagement du Comité Technique (6 clauses) ─ +# Contenu ABSENT de seed.py — reconstruit depuis research_duniter_forum.md §3 +# (Engagement Comité Tech v2.0.0-fr, vote 2026-02-04 → 2026-03-06, en cours +# au moment de la recherche). Documenté dans provenance.notes. + +COMTECH_CLAUSES = [ + { + "code": "CT1", + "section": "mission", + "title": "Mission du Comité Technique", + "inertia": "standard", + "content": ( + "Le Comité Technique protège la souveraineté de la communauté Ğ1 " + "sur les mises à jour de la blockchain. Ses missions :\n\n" + "- Auditer le code\n" + "- Détecter le code malveillant\n" + "- Vérifier les fonctionnalités annoncées\n" + "- Déployer uniquement les mises à jour servant la communauté" + ), + }, + { + "code": "CT2", + "section": "engagements", + "title": "Respect des règles en vigueur", + "inertia": "standard", + "content": ( + "Chaque membre du Comité Technique s'engage à respecter les règles " + "décrites dans la version en vigueur du présent document." + ), + }, + { + "code": "CT3", + "section": "engagements", + "title": "Démission en cas de désaccord", + "inertia": "standard", + "content": ( + "Chaque membre s'engage à démissionner s'il est en désaccord " + "avec les règles en vigueur." + ), + }, + { + "code": "CT4", + "section": "engagements", + "title": "Révocation des membres en violation", + "inertia": "standard", + "content": ( + "Chaque membre s'engage à voter pour retirer le mandat des " + "collègues qui violent visiblement les directives du présent document." + ), + }, + { + "code": "CT5", + "section": "formule", + "title": "Conditions d'adoption", + "inertia": "high", + "content": ( + "L'adoption d'une modification exige le seuil unani-majoritaire " + "de la toile ET le support minimum des membres du comité :\n\n" + "```\nvotesPour >= ceil(WotSize^0.1 + (0.5 + (1 - 0.5) " + "× (1 - (TotalVotes/WotSize)^0.2)) × TotalVotes)\n```\n" + "ET\n" + "```\nvotesCoTecPour >= ceil(CoTecSize^0.1)\n```\n\n" + "Mode compact : D30M50B.1G.2T.1." + ), + }, + { + "code": "CT6", + "section": "composition", + "title": "Composition du comité", + "inertia": "high", + "content": ( + "L'entrée et la sortie d'un membre nécessitent l'approbation des " + "2/3 des membres existants (modifiable uniquement par mise à jour " + "runtime avec validation des 2/3)." + ), + }, +] + +add_doc( + DOC_TECH, + "engagement-comite-tech", + "Acte d'engagement du Comité Technique", + "reference", + "Acte d'engagement des membres du Comité Technique Duniter V2 " + "(v2.0.0-fr). Protège la souveraineté de la communauté Ğ1 sur les mises " + "à jour de la blockchain : audit du code, détection de code malveillant, " + "déploiement des seules mises à jour servant la communauté.", + { + "sources": [ + { + "title": "Vote : Engagement Comité Tech 2.0.0-fr", + "url": "https://forum.monnaie-libre.fr/t/vote-engagement-comite-tech-2-0-0-fr/33293", + "date": "2026-02-04", + "version": "2.0.0-fr", + }, + { + "title": "g1vote — mode D30M50B.1G.2T.1 (talk 32960)", + "url": "https://g1vote-view-237903.pages.duniter.org/", + }, + ], + "notes": ( + "Contenu ABSENT du seed v1 (seed.py) — reconstruit depuis la " + "synthèse interne research_duniter_forum.md (§3). Le vote " + "(2026-02-04 → 2026-03-06) était en cours au moment de la " + "recherche : texte intégral non disponible, clauses condensées " + "depuis la synthèse. Méthode de vote : 0,01 Ğ1 envoyés aux " + "adresses désignées (POUR ou CONTRE)." + ), + }, + T_COMTECH_OPEN, + T_COMTECH_ADOPT, +) + +for i, c in enumerate(COMTECH_CLAUSES): + add_clause( + doc_slug="engagement-comite-tech", + doc_id=DOC_TECH, + code=c["code"], + section=c["section"], + position=i + 1, + title=c["title"], + inertia=c["inertia"], + content=c["content"], + version_label="2.0.0-fr", + created=T_COMTECH_OPEN, + adopted=T_COMTECH_ADOPT, + author="Cgeek", + protocol_id=P_TECHCOMM, + scope_circles=[C_ROOT, C_TECH], + tags=["engagement-comite-tech", c["section"]], + ) + +# ── Doc 4 : Pacte duniter-g1 (20 clauses, settingKeys) ─────── + +_org_v1 = next(o for o in ORGS_V1 if o["slug"] == "duniter-g1") + +A1_TEXT = ( + "Nous, membres de la toile de confiance Ğ1, nous donnons cet outil pour " + "décider ensemble sans remettre notre souveraineté à quiconque.\n\n" + "**Autonomie** — chacun décide de ce qui ne concerne que lui. La monnaie " + "libre garantit à chacun sa part de création monétaire ; notre gouvernance " + "garantit à chacun sa part de décision.\n\n" + "**Équilibre** — aucun pouvoir sans contrepoids : tout mandat est borné, " + "révocable et rend compte ; toute règle porte son inertie et reste " + "amendable par ceux qu'elle gouverne.\n\n" + "**Liaison** — la confiance circule de personne à personne, comme les " + "certifications : nos décisions relient ceux qu'elles concernent, du petit " + "cercle à la toile entière.\n\n" + "Notre finalité : une toile de confiance vivante où la monnaie et la " + "décision appartiennent également à chacun — sacralisée dans ce Pacte, " + "jamais immuable." +) + +PACT_CLAUSES = [ + # (code, section, title, inertia, settingKey, settingValue, content) + ("A1", "preambule", "Notre finalité", "high", None, None, A1_TEXT), + ( + "P1", "triage", "Petit cercle", "standard", + "triage.smallGroupMax", QUAL_V1["small_group_max"], + "Jusqu'à 5 personnes concernées, la voie légère suffit : demander " + "l'avis de chacun, écouter, puis décider.", + ), + ( + "P2", "triage", "Grand corps", "standard", + "triage.collectiveMin", QUAL_V1["collective_wot_min"], + "Au-delà de 50 personnes concernées, la décision passe par le " + "protocole des grands corps — pour la toile Ğ1 : le vote binaire " + "inertiel, héritage Toile de Confiance.", + ), + ( + "P3", "triage", "Tour de consentement", "standard", + "triage.consentMax", 7, + "Jusqu'à 7 personnes, un tour d'accord suffit : sans objection " + "argumentée maintenue, c'est adopté.", + ), + ( + "P4", "triage", "Fenêtre d'objection", "standard", + "triage.objectionWindowHours", 48, + "Toute décision sous mandat ou en voie légère reste ouverte à " + "l'objection pendant 48 heures avant d'entrer en vigueur.", + ), + ( + "P5", "triage", "Fenêtre d'avis", "standard", + "triage.adviceWindowHours", 72, + "Quand la voie est l'avis, chacun dispose de 72 heures pour le donner " + "avant que l'auteur décide.", + ), + ( + "P6", "triage", "Temps de formulation", "standard", + "triage.framingDays", 14, + "Quatorze jours pour s'instruire et formuler des contre-propositions " + "avant l'ouverture du vote des décisions qui engagent.", + ), + ( + "P7", "triage", "Affluence", "standard", + "triage.concernEscalateRatio", 0.5, + "Quand la moitié des personnes concernées se déclare, le traitement " + "collectif devient obligatoire : élargir le périmètre, ou motiver " + "publiquement son maintien.", + ), + ( + "P8", "triage", "Récurrence", "standard", + "triage.recurrenceThreshold", 3, + "À la troisième décision semblable, l'outil propose de réclamer un " + "mandat ou de créer une règle.", + ), + ( + "P9", "triage", "Épreuve du réel", "standard", + "triage.reviewDelayDays", 90, + "Toute décision structurante ou irréversible est revue au plus tard " + "90 jours après son adoption : ça tient, à revoir, ou à révoquer.", + ), + ( + "P10", "triage", "Matière exigée", "standard", + "triage.requireEffects", "binding", + "Ouvrir un vote collectif sur une décision qui engage exige d'énoncer " + "au moins un effet recherché — et une cible mesurable quand elle est " + "structurante.", + ), + ( + "M1", "protocoles", "Protocole de consentement", "high", + "protocols.consent", P_CONSENT, + "Le consentement est notre modalité de base et le repli universel : " + "toute clé de protocole non résolue y retombe.", + ), + ( + "M2", "protocoles", "Protocole des grands corps", "high", + "protocols.large", P_WOT, + "Au-delà du grand corps, la toile Ğ1 vote pour/contre sous la formule " + "d'inertie héritée de la Toile de Confiance — l'outil de dernier " + "recours des très grands corps.", + ), + ( + "M3", "protocoles", "Protocole nuancé", "high", + "protocols.nuanced", P_NUANCED, + "Entre le tour de consentement et le grand corps : le vote nuancé à " + "6 niveaux — chacun se prononce en nuances, pas en camps.", + ), + ( + "M4", "protocoles", "Réglage collectif", "high", + "protocols.parametric", P_PARAMETRIC, + "Quand la décision est un nombre, un taux ou un montant : décider au " + "curseur — la médiane basse éclaire, le geste du garant cristallise.", + ), + ( + "M5", "protocoles", "Élection sans candidat", "high", + "protocols.election", P_ELECTION, + "Pour désigner une personne : élection sans candidat, vote blanc " + "possible, départage entre ex æquo décidé d'avance.", + ), + ( + "N1", "inertie", "Inertie basse", "max", + "protocols.clauseByInertia.low", P_INERTIA_LOW, + "Amender une clause d'inertie basse (annexes, recommandations) : " + "G = 0,1 — M = 50 %.", + ), + ( + "N2", "inertie", "Inertie standard", "max", + "protocols.clauseByInertia.standard", P_WOT, + "Amender une clause d'inertie standard (engagements fondamentaux et " + "techniques) : G = 0,2 — M = 50 %.", + ), + ( + "N3", "inertie", "Inertie haute", "max", + "protocols.clauseByInertia.high", P_INERTIA_HIGH, + "Amender une clause d'inertie haute (formule de vote) : " + "G = 0,4 — M = 60 %.", + ), + ( + "N4", "inertie", "Inertie très haute — méta-réglage", "max", + "protocols.clauseByInertia.max", P_INERTIA_MAX, + "Amender le réglage de l'inertie lui-même exige la quasi-unanimité : " + "G = 0,6 — M = 66 %. La protection des règles de modification — " + "l'héritage N1 des actes Ğ1.", + ), +] + +add_doc( + DOC_PACT, + "pacte-duniter-g1", + "Pacte de la toile Ğ1", + "pact", + "Notre contrat social — sacralisé, jamais immuable", + { + "sources": [], + "notes": ( + f"Import v1 : organisation « {_org_v1['name']} » " + f"({_org_v1['org_type']}, transparente) — " + f"« {_org_v1['description']} » Seuils importés du protocole de " + f"qualification v1 « {QUAL_V1['name']} » " + f"(small_group_max={QUAL_V1['small_group_max']}, " + f"collective_wot_min={QUAL_V1['collective_wot_min']}, " + f"modalités par défaut : {', '.join(QUAL_MODALITIES_V1)})." + ), + }, + T_GENESIS, +) + +for i, (code, section, title, inertia, key, value, content) in enumerate(PACT_CLAUSES): + add_clause( + doc_slug="pacte-duniter-g1", + doc_id=DOC_PACT, + code=code, + section=section, + position=i + 1, + title=title, + inertia=inertia, + content=content, + version_label="1.0.0", + created=T_CERT_DRAFT, + adopted=T_GENESIS, + author="Cgeek", + protocol_id=P_CONSENT, + scope_circles=[C_ROOT], + tags=["pacte-duniter-g1", section], + setting_key=key, + setting_value=value, + ) + +# ───────────────────────────────────────────────────────────── +# Décisions historiques +# ───────────────────────────────────────────────────────────── + +# ── Runtime Upgrade 1100 — adoptée, structurelle, gravée ───── + +D_RUNTIME = uid("decision:runtime-upgrade-1100") +_runtime_decided = "2026-05-20T17:00:00Z" +decisions.append( + entity( + D_RUNTIME, + "2026-05-02T09:00:00Z", + _runtime_decided, + authorId=PID["Cgeek"], + title="Runtime Upgrade 1100", + body=( + RUNTIME_META["description"] + + "\n\n" + + RUNTIME_META["context"] + + "\n\n**Processus (import v1 — titre v1 : « Runtime Upgrade », " + "type runtime_upgrade) :**\n" + + _steps_md(RUNTIME_STEPS) + ), + brief={ + "context": RUNTIME_META["context"], + "sources": [ + {"label": "Dépôt Duniter V2", "url": "https://git.duniter.org/nodes/rust/duniter-v2s"}, + ], + "effects": [ + { + "label": "Réseau stable après la mise à niveau", + "target": "zéro anomalie critique sous 7 jours", + "measured": { + "note": "Aucune anomalie critique observée sur 7 jours — métriques réseau nominales.", + "at": "2026-05-27T17:00:00Z", + "byId": PID["Moul"], + }, + }, + { + "label": "Nœuds validateurs synchronisés", + "target": "100 % des validateurs à jour sous 48 h", + "measured": { + "note": "Ensemble des validateurs en ligne à jour en 36 h.", + "at": "2026-05-22T09:00:00Z", + "byId": PID["Tuxmain"], + }, + }, + ], + }, + resources={ + "note": "Coordination des forgerons pour la synchronisation des nœuds — fenêtre de maintenance.", + "unit": "heures", + }, + tags=["runtime", "upgrade", "on-chain"], + reversibility="costly", + weight="structural", + urgent=False, + scope={"selfOnly": False, "circleIds": [C_ROOT, C_FORGE, C_TECH], "personIds": []}, + route="collective", + triageRule="R5", + routeOverridden=False, + protocolId=P_TECHCOMM, + status="adopted", + decidedAt=_runtime_decided, + review={"dueAt": "2026-08-18T17:00:00Z"}, + stewardIds=[PID["Cgeek"]], + measurerIds=[PID["Moul"], PID["Tuxmain"]], + visibility="collective", + engraving={ + "sha256": hashlib.sha256( + f"runtime-upgrade-1100|{_runtime_decided}".encode() + ).hexdigest(), + "engravedAt": _runtime_decided, + "proofLevel": "local", + }, + ) +) + +# ── Évolution Licence Ğ1 v0.4.0 — en formulation, versions proposées ── + +D_LICENCE = uid("decision:evolution-licence-0.4.0") +decisions.append( + entity( + D_LICENCE, + "2026-07-28T09:00:00Z", + "2026-08-01T09:00:00Z", + authorId=PID["Poka"], + title="Évolution Licence G1 v0.4.0", + body=( + LICENCE_META["description"] + + "\n\n**Processus (import v1) :**\n" + + _steps_md(LICENCE_STEPS) + ), + baselineNote="Aujourd'hui : Licence Ğ1 v0.3.0 en vigueur.", + brief={ + "context": LICENCE_META["context"], + "sources": [ + { + "label": "Préparation licence v0.4.0 (topic 32375)", + "url": "https://forum.monnaie-libre.fr/t/preparation-dune-proposition-devolution-de-la-licence-1/32375", + }, + { + "label": "Proposition Charte 1.0, rejetée (topic 31066)", + "url": "https://forum.monnaie-libre.fr/t/proposition-charte-1-0/31066", + }, + ], + "effects": [ + { + "label": "Une licence clarifiée, comprise par les nouveaux certificateurs", + "target": "adoption de la v0.4.0 au seuil WoT", + } + ], + }, + resources={ + "note": "Co-rédaction et animation du vote — temps bénévole des contributeurs.", + "unit": "heures", + }, + tags=["licence", "certification"], + reversibility="costly", + weight="structural", + urgent=False, + scope={"selfOnly": False, "circleIds": [C_ROOT], "personIds": []}, + route="collective", + triageRule="R5", + routeOverridden=False, + protocolId=P_NUANCED, + status="framing", + windowEndsAt="2026-08-15T09:00:00Z", + stewardIds=[PID["Poka"]], + measurerIds=[], + visibility="collective", + ) +) + +# Versions proposées (liées aux clauses cert E3 et E7, contre-propositions v0.4.0) +versions.append( + entity( + uid("version:engagement-certification:E3:0.4.0-draft"), + "2026-08-05T09:00:00Z", + clauseId=uid("clause:engagement-certification:E3"), + decisionId=D_LICENCE, + versionLabel="0.4.0-draft", + content=( + "Je me suis assuré de connaître suffisamment la personne qui gère " + "cette clé publique : je sais la recontacter par au moins deux " + "canaux indépendants, je suis en mesure de repérer un double-compte, " + "et je m'engage à signaler tout doute aux experts de la communauté. " + "« Avoir vu » quelqu'un ne suffit pas." + ), + status="proposed", + ) +) +versions.append( + entity( + uid("version:engagement-certification:E7:0.4.0-draft"), + "2026-08-05T09:00:00Z", + clauseId=uid("clause:engagement-certification:E7"), + decisionId=D_LICENCE, + versionLabel="0.4.0-draft", + content=( + "J'ai rencontré la personne physiquement (préférable), **OU** j'ai " + "vérifié à distance le lien personne / clé publique par au moins " + "trois moyens de communication différents et indépendants, dont un " + "échange en visio en direct." + ), + status="proposed", + ) +) + +# ───────────────────────────────────────────────────────────── +# Mandats (3) + décisions d'origine, session de vote, votes +# ───────────────────────────────────────────────────────────── + +_m_comtech = next(m for m in MANDATES_V1 if m["mandate_type"] == "techcomm") +_m_smith = next(m for m in MANDATES_V1 if m["mandate_type"] == "smith") +_m_forum = next(m for m in MANDATES_V1 if m["mandate_type"] == "custom") + +# ── Mandat 1 : Comité Technique — actif, holder Cgeek, 1 rapport dû ── + +D_M_COMTECH = uid("decision:mandat-comite-technique") +M_COMTECH = uid("mandate:comite-technique") +decisions.append( + entity( + D_M_COMTECH, + "2025-08-20T09:00:00Z", + "2025-09-10T18:00:00Z", + authorId=PID["Elois"], + title=_m_comtech["title"], + body=( + _m_comtech["description"] + + "\n\n**Parcours (import v1 — type techcomm) :**\n" + + _steps_md(_m_comtech["steps"]) + ), + tags=["mandat", "comite-technique", "v1:techcomm"], + reversibility="costly", + weight="binding", + urgent=False, + scope={"selfOnly": False, "circleIds": [C_ROOT], "personIds": []}, + route="collective", + triageRule="R5", + routeOverridden=False, + protocolId=P_NUANCED, + createsMandate={ + "title": _m_comtech["title"], + "domainCircleIds": [C_TECH], + "domainTags": ["runtime", "code", "on-chain"], + "durationDays": 365, + "reportEveryDays": 180, + }, + status="adopted", + decidedAt="2025-09-10T18:00:00Z", + stewardIds=[PID["Cgeek"]], + measurerIds=[], + visibility="collective", + ) +) +mandates = [ + entity( + M_COMTECH, + "2025-09-15T09:00:00Z", + title=_m_comtech["title"], + holderId=PID["Cgeek"], + originDecisionId=D_M_COMTECH, + domain={"circleIds": [C_TECH], "tags": ["runtime", "code", "on-chain"]}, + startsAt="2025-09-15T09:00:00Z", + endsAt="2026-09-15T09:00:00Z", + electorCircleId=C_ROOT, + nominationMethod="nuanced-vote", + reports=[{"dueAt": "2026-03-15T09:00:00Z"}], # rapport de mi-mandat dû, non remis + status="active", + ) +] + +# ── Mandat 2 : Administrateur des Forgerons — proposé, EN vote nuancé ── +# v1 : status 'voting' (rotation 2026-Q1) ; la session v2 est positionnée +# OUVERTE à la date du seed (2026-07-25 → 2026-08-24) — écart documenté. + +D_M_SMITH = uid("decision:mandat-admin-forgerons") +M_SMITH = uid("mandate:admin-forgerons") +S_SMITH = uid("session:mandat-admin-forgerons") +decisions.append( + entity( + D_M_SMITH, + "2026-07-10T09:00:00Z", + "2026-07-25T09:00:00Z", + authorId=PID["Inso"], + title=_m_smith["title"], + body=( + _m_smith["description"] + + "\n\n**Parcours (import v1 — type smith, statut v1 : voting) :**\n" + + _steps_md(_m_smith["steps"]) + ), + tags=["mandat", "forgerons", "v1:smith"], + reversibility="costly", + weight="binding", + urgent=False, + scope={"selfOnly": False, "circleIds": [C_ROOT, C_FORGE], "personIds": []}, + route="collective", + triageRule="R5", + routeOverridden=False, + protocolId=P_NUANCED, + createsMandate={ + "title": "Administrateur des Forgerons", + "domainCircleIds": [C_FORGE], + "domainTags": ["forge", "noeuds"], + "durationDays": 180, + }, + status="voting", + windowEndsAt="2026-08-24T09:00:00Z", + stewardIds=[PID["Inso"]], + measurerIds=[], + visibility="collective", + ) +) +mandates.append( + entity( + M_SMITH, + "2026-07-10T09:00:00Z", + title=_m_smith["title"], + holderId=PID["Moul"], # v1 : mandatee = Moul (Forgeron senior) + originDecisionId=D_M_SMITH, + domain={"circleIds": [C_FORGE], "tags": ["forge", "noeuds"]}, + startsAt="2026-09-01T09:00:00Z", + endsAt="2027-03-01T09:00:00Z", + electorCircleId=C_FORGE, + nominationMethod="nuanced-vote", + reports=[], + status="proposed", + ) +) +sessions = [ + entity( + S_SMITH, + "2026-07-25T09:00:00Z", + decisionId=D_M_SMITH, + protocolId=P_NUANCED, + corpusPersonIds=[PID[n] for n in VOTER_NAMES], + corpusSize=len(VOTER_NAMES), + opensAt="2026-07-25T09:00:00Z", + closesAt="2026-08-24T09:00:00Z", + status="open", + ) +] +votes = [ + entity( + uid("vote:mandat-admin-forgerons:tuxmain"), + "2026-07-26T14:00:00Z", + sessionId=S_SMITH, + voterId=PID["Tuxmain"], + value=5, + comment="Rôle nécessaire — l'embarquement des nouveaux forgerons traîne depuis des mois.", + ), + entity( + uid("vote:mandat-admin-forgerons:hugo"), + "2026-07-27T10:30:00Z", + sessionId=S_SMITH, + voterId=PID["Hugo"], + value=4, + ), + entity( + uid("vote:mandat-admin-forgerons:matograine"), + "2026-07-29T19:45:00Z", + sessionId=S_SMITH, + voterId=PID["Matograine"], + value=2, + comment="Réservé : je préfère une rotation plus courte, renouvelée à chaque trimestre.", + ), +] + +# ── Mandat 3 : Modération Forum — actif, tags forum/moderation ── +# v1 : status 'draft' — la spec Seeds v2 le pose actif (écart documenté). + +D_M_FORUM = uid("decision:mandat-moderation-forum") +M_FORUM = uid("mandate:moderation-forum") +decisions.append( + entity( + D_M_FORUM, + "2026-04-10T09:00:00Z", + "2026-04-25T18:00:00Z", + authorId=PID["Hugo"], + title=_m_forum["title"], + body=( + _m_forum["description"] + + "\n\n**Parcours (import v1 — type custom, statut v1 : draft) :**\n" + + _steps_md(_m_forum["steps"]) + ), + tags=["mandat", "forum", "moderation", "v1:custom"], + reversibility="easy", + weight="binding", + urgent=False, + scope={"selfOnly": False, "circleIds": [C_ROOT], "personIds": []}, + route="collective", + triageRule="R5", + routeOverridden=False, + protocolId=P_ELECTION, + createsMandate={ + "title": _m_forum["title"], + "domainCircleIds": [C_ROOT], + "domainTags": ["forum", "moderation"], + "durationDays": 365, + "reportEveryDays": 180, + }, + status="adopted", + decidedAt="2026-04-25T18:00:00Z", + stewardIds=[PID["Tortue"]], + measurerIds=[], + visibility="collective", + ) +) +mandates.append( + entity( + M_FORUM, + "2026-05-01T09:00:00Z", + title=_m_forum["title"], + holderId=PID["Tortue"], + originDecisionId=D_M_FORUM, + domain={"circleIds": [C_ROOT], "tags": ["forum", "moderation"]}, + startsAt="2026-05-01T09:00:00Z", + endsAt="2027-05-01T09:00:00Z", + electorCircleId=C_ROOT, + nominationMethod="election-no-candidate", + reports=[{"dueAt": "2026-11-01T09:00:00Z"}], + status="active", + ) +) + +# ───────────────────────────────────────────────────────────── +# Collectif + Bundle +# ───────────────────────────────────────────────────────────── + +collective = { + "id": COLLECTIVE_ID, + "slug": "duniter-g1", + "name": _org_v1["name"], + "color": _org_v1["color"], # '#22c55e' (v1 == spec) + "icon": "i-lucide-network", # spec Seeds v2 (v1 : i-lucide-globe, noté) + "template": "free-currency", + "isTransparent": _org_v1["is_transparent"], + "pactDocId": DOC_PACT, + "rootCircleId": C_ROOT, + "createdAt": T_GENESIS, + "updatedAt": EXPORTED_AT, +} + +bundle = { + "schemaVersion": 2, + "exportedAt": EXPORTED_AT, + "collective": collective, + "people": people, + "circles": circles, + "decisions": decisions, + "concerns": [], + "objections": [], + "advices": [], + "assents": [], + "mandates": mandates, + "docs": docs, + "clauses": clauses, + "versions": versions, + "protocols": protocols, + "sessions": sessions, + "votes": votes, +} + +# ───────────────────────────────────────────────────────────── +# Validation interne (invariants) puis écriture +# ───────────────────────────────────────────────────────────── + + +def _validate() -> None: + ids: set[str] = set() + for key in ( + "people", "circles", "decisions", "mandates", "docs", + "clauses", "versions", "protocols", "sessions", "votes", + ): + for item in bundle[key]: + assert item["id"] not in ids, f"id dupliqué : {item['id']} ({key})" + ids.add(item["id"]) + assert item["collectiveId"] == COLLECTIVE_ID, f"collectiveId manquant ({key})" + assert item["createdAt"] and item["updatedAt"], f"dates manquantes ({key})" + + decision_ids = {d["id"] for d in decisions} + version_ids = {v["id"] for v in versions} + protocol_ids = {p["id"] for p in protocols} + person_ids = {p["id"] for p in people} + circle_ids = {c["id"] for c in circles} + doc_ids = {d["id"] for d in docs} + + for clause in clauses: + assert clause["docId"] in doc_ids + assert clause["currentVersionId"] in version_ids, f"version manquante : {clause['code']}" + for version in versions: + assert version["decisionId"] in decision_ids, f"décision manquante : {version['id']}" + for decision in decisions: + assert decision["authorId"] in person_ids + if decision.get("protocolId"): + assert decision["protocolId"] in protocol_ids + for cid in decision["scope"]["circleIds"]: + assert cid in circle_ids + for mandate in mandates: + assert mandate["holderId"] in person_ids + assert mandate["originDecisionId"] in decision_ids + assert mandate["electorCircleId"] in circle_ids + for session_ in sessions: + assert session_["decisionId"] in decision_ids + assert session_["protocolId"] in protocol_ids + for vote in votes: + assert vote["voterId"] in person_ids + assert vote["sessionId"] in {s["id"] for s in sessions} + + # Décision fondatrice pour CHAQUE clause courante + by_id_version = {v["id"]: v for v in versions} + by_id_decision = {d["id"]: d for d in decisions} + for clause in clauses: + version = by_id_version[clause["currentVersionId"]] + founding = by_id_decision[version["decisionId"]] + assert founding["status"] == "adopted", f"fondatrice non adoptée : {clause['code']}" + assert founding["title"].startswith("Adoption fondatrice — ") + + # Invariant consent (importBundle le valide aussi) + consent = [p for p in protocols if p["method"] == "consent"] + assert len(consent) == 1 + + # Comptages de non-perte + cert = [c for c in clauses if c["docId"] == DOC_CERT] + forge = [c for c in clauses if c["docId"] == DOC_FORGE] + assert len(cert) == 33 and len(forge) == 59 + assert len(people) == 11 + assert sum(1 for p in people if p["wotStatus"] == "smith") == 5 + assert len(mandates) == 3 + assert len(protocols) >= 7 + + +def _dist(doc_id: str) -> dict: + out: dict[str, int] = {} + for clause in clauses: + if clause["docId"] == doc_id: + out[clause["inertia"]] = out.get(clause["inertia"], 0) + 1 + return dict(sorted(out.items())) + + +def main() -> None: + _validate() + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) + payload = json.dumps(bundle, ensure_ascii=False, indent=2) + "\n" + OUT_PATH.write_text(payload, encoding="utf-8") + + print("=" * 64) + print("Export seed v1 → Bundle v2 : duniter-g1.bundle.json") + print("=" * 64) + print(f"Sortie : {OUT_PATH}") + print(f"Taille : {len(payload.encode('utf-8'))} octets") + print() + print("Diff de comptage seed.py (extrait) → bundle (émis) :") + print(f" items Certification : {len(CERT_ITEMS):>3} → {len([c for c in clauses if c['docId'] == DOC_CERT]):>3} clauses {_dist(DOC_CERT)}") + print(f" items Forgeron : {len(FORGE_ITEMS):>3} → {len([c for c in clauses if c['docId'] == DOC_FORGE]):>3} clauses {_dist(DOC_FORGE)}") + print(f" items Comité Tech : 0 → {len([c for c in clauses if c['docId'] == DOC_TECH]):>3} clauses (research_duniter_forum.md §3)") + print(f" clauses Pacte : 0 → {len([c for c in clauses if c['docId'] == DOC_PACT]):>3} (settingKeys v2 ; seuils v1 qualification)") + print(f" VOTER_NAMES : {len(VOTER_NAMES):>3} → {len(people):>3} personnes ({sum(1 for p in people if p['wotStatus'] == 'smith')} smiths)") + print(f" mandates_data : {len(MANDATES_V1):>3} → {len(mandates):>3} mandats") + print(f" VotingProtocols : {len(PROTOCOLS_V1):>3} → {len(protocols):>3} protocoles (7 FormulaConfigs portées)") + print(f" décisions : 2 → {len(decisions):>3} (dont {len([d for d in decisions if d['title'].startswith('Adoption fondatrice')])} fondatrices)") + print(f" sessions / votes : 3 → {len(sessions):>3} / {len(votes):>3} (3 sessions de démo v1 NON portées — votes factices)") + print(f" versions : — → {len(versions):>3} ({len([v for v in versions if v['status'] == 'proposed'])} proposées, licence v0.4.0)") + print() + print("voteRecord forgeron :", json.dumps( + next(d for d in docs if d["id"] == DOC_FORGE)["provenance"]["voteRecord"]["result"], + ensure_ascii=False, + )) + + +if __name__ == "__main__": + main() diff --git a/frontend/app/assets/css/moods.css b/frontend/app/assets/css/moods.css index ebb7df8..2504dec 100644 --- a/frontend/app/assets/css/moods.css +++ b/frontend/app/assets/css/moods.css @@ -1,158 +1,196 @@ /* ========================================================================== - libreDecision — Mood / Ambiance System - Palettes harmoniques variees, colores en lite, lumineux en dark. + libreDecision v2 — Ambiances (champ lexical du puits 井) + Source (light, eau claire) · Margelle (light, pierre chaude) + Nappe (dark, eau profonde) · Minuit (dark, encre & lanterne) + Borderless : profondeur par background + box-shadow, jamais de bordures. ========================================================================== */ /* -------------------------------------------------------------------------- - Peps — Chaud, colore, tonique (Light) - Palette: corail vif / ocre / indigo / vert franc + Source — l'eau vive au grand jour (Light) -------------------------------------------------------------------------- */ -.mood-peps { - --mood-bg: #faf8f5; - --mood-surface: #ffffff; - --mood-surface-hover: #fdf2ec; - --mood-text: #1e1410; - --mood-text-muted: #7a5e48; - --mood-accent: #d44a10; - --mood-accent-soft: rgba(212, 74, 16, 0.08); +.mood-source { + --mood-bg: #f0f5f6; + --mood-surface: #fcfefe; + --mood-surface-hover: #e6eff2; + --mood-text: #102126; + --mood-text-muted: #52707a; + --mood-accent: #0f7fa8; + --mood-accent-soft: rgba(15, 127, 168, 0.09); --mood-accent-text: #ffffff; - --mood-border: #e8d8c8; - --mood-secondary: #4a5ec0; - --mood-tertiary: #c07820; - --mood-success: #1a8c3e; - --mood-warning: #c47d0a; - --mood-error: #c23028; - --mood-gradient: linear-gradient(145deg, #fdf8f0 0%, #ffffff 40%, #f0f4fb 100%); - --mood-shadow: rgba(180, 80, 20, 0.07); - --mood-input-bg: #ffffff; - --mood-input-border: #d4c0aa; - --mood-input-focus: #d44a10; + --mood-secondary: #a8742c; /* laiton — le mandat, le pouvoir confié */ + --mood-tertiary: #2e8a72; /* eau végétale — l'avis, le dialogue */ + --mood-success: #1d8a4a; + --mood-warning: #b07c14; + --mood-error: #bc3d33; + --mood-gradient: linear-gradient(150deg, #eaf3f5 0%, #fcfefe 45%, #eef2ee 100%); + --mood-shadow: rgba(20, 90, 120, 0.08); + --mood-input-bg: #fcfefe; + --mood-input-border: #b8ccd2; + --mood-input-focus: #0f7fa8; - --mood-status-prepa: #b8600e; - --mood-status-prepa-bg: rgba(184, 96, 14, 0.12); - --mood-status-vote: #4a5ec0; - --mood-status-vote-bg: rgba(74, 94, 192, 0.10); - --mood-status-vigueur: #1a8c3e; - --mood-status-vigueur-bg: rgba(26, 140, 62, 0.10); - --mood-status-clos: #6e5844; - --mood-status-clos-bg: rgba(110, 88, 68, 0.08); + --mood-status-prepa: #8a7040; + --mood-status-prepa-bg: rgba(138, 112, 64, 0.10); + --mood-status-fenetre: #b07c14; + --mood-status-fenetre-bg: rgba(176, 124, 20, 0.11); + --mood-status-vote: #2661b8; + --mood-status-vote-bg: rgba(38, 97, 184, 0.10); + --mood-status-fige: #58708c; + --mood-status-fige-bg: rgba(88, 112, 140, 0.12); + --mood-status-vigueur: #1d8a4a; + --mood-status-vigueur-bg: rgba(29, 138, 74, 0.10); + --mood-status-revoque: #a4553a; + --mood-status-revoque-bg: rgba(164, 85, 58, 0.10); + --mood-status-clos: #5f6f74; + --mood-status-clos-bg: rgba(95, 111, 116, 0.09); } /* -------------------------------------------------------------------------- - Zen — Vegetal lumineux, nature vivante (Light) - Palette: vert franc / terre sienne / bleu canard / bois chaud + Margelle — la pierre du puits, calcaire et bronze (Light) -------------------------------------------------------------------------- */ -.mood-zen { - --mood-bg: #f4f6f0; - --mood-surface: #fbfcf8; - --mood-surface-hover: #eaf0e4; - --mood-text: #1a2418; - --mood-text-muted: #5a7050; - --mood-accent: #2e8b48; - --mood-accent-soft: rgba(46, 139, 72, 0.09); +.mood-margelle { + --mood-bg: #f6f3ec; + --mood-surface: #fdfbf6; + --mood-surface-hover: #efe9dc; + --mood-text: #221c10; + --mood-text-muted: #77684c; + --mood-accent: #96682a; + --mood-accent-soft: rgba(150, 104, 42, 0.10); --mood-accent-text: #ffffff; - --mood-border: #b8d0a8; - --mood-secondary: #8a6030; - --mood-tertiary: #2a7888; - --mood-success: #268040; - --mood-warning: #a87818; - --mood-error: #b83838; - --mood-gradient: linear-gradient(145deg, #f0f5ea 0%, #fbfcf8 40%, #f4efe6 100%); - --mood-shadow: rgba(40, 100, 50, 0.07); - --mood-input-bg: #fbfcf8; - --mood-input-border: #a8c898; - --mood-input-focus: #2e8b48; + --mood-secondary: #285e88; /* le ciel dans l'eau du seau */ + --mood-tertiary: #4f7a3c; /* mousse entre les pierres */ + --mood-success: #3d7e34; + --mood-warning: #a8720e; + --mood-error: #b23f30; + --mood-gradient: linear-gradient(150deg, #f3eee2 0%, #fdfbf6 45%, #f0eee6 100%); + --mood-shadow: rgba(120, 90, 40, 0.09); + --mood-input-bg: #fdfbf6; + --mood-input-border: #cfc2a6; + --mood-input-focus: #96682a; - --mood-status-prepa: #a87818; - --mood-status-prepa-bg: rgba(168, 120, 24, 0.11); - --mood-status-vote: #2a7888; - --mood-status-vote-bg: rgba(42, 120, 136, 0.10); - --mood-status-vigueur: #268040; - --mood-status-vigueur-bg: rgba(38, 128, 64, 0.10); - --mood-status-clos: #607858; - --mood-status-clos-bg: rgba(96, 120, 88, 0.08); + --mood-status-prepa: #7c6a48; + --mood-status-prepa-bg: rgba(124, 106, 72, 0.10); + --mood-status-fenetre: #a8720e; + --mood-status-fenetre-bg: rgba(168, 114, 14, 0.11); + --mood-status-vote: #285e88; + --mood-status-vote-bg: rgba(40, 94, 136, 0.10); + --mood-status-fige: #6e6a58; + --mood-status-fige-bg: rgba(110, 106, 88, 0.12); + --mood-status-vigueur: #3d7e34; + --mood-status-vigueur-bg: rgba(61, 126, 52, 0.10); + --mood-status-revoque: #9c5236; + --mood-status-revoque-bg: rgba(156, 82, 54, 0.10); + --mood-status-clos: #6f6a5c; + --mood-status-clos-bg: rgba(111, 106, 92, 0.09); } /* -------------------------------------------------------------------------- - Chagrine — Nuit profonde, reflets varies (Dark) - Palette: bleu lavande / ambre chaud / rose ancien / vert d'eau + Nappe — l'eau profonde sous la ville (Dark) -------------------------------------------------------------------------- */ -.mood-chagrine { - --mood-bg: #0e1018; - --mood-surface: #151a28; - --mood-surface-hover: #1c2640; - --mood-text: #dce0ec; - --mood-text-muted: #8898b8; - --mood-accent: #6488d8; - --mood-accent-soft: rgba(100, 136, 216, 0.12); - --mood-accent-text: #ffffff; - --mood-border: #222e48; - --mood-secondary: #d0a040; - --mood-tertiary: #c87090; - --mood-success: #48c87a; - --mood-warning: #d8a838; - --mood-error: #d86060; - --mood-gradient: linear-gradient(145deg, #0e1018 0%, #151a28 40%, #101420 100%); - --mood-shadow: rgba(60, 100, 180, 0.12); - --mood-input-bg: #151a28; - --mood-input-border: #2a3858; - --mood-input-focus: #6488d8; +.mood-nappe { + --mood-bg: #0a141b; + --mood-surface: #101f2a; + --mood-surface-hover: #16293a; + --mood-text: #d6e3ea; + --mood-text-muted: #7e98a8; + --mood-accent: #3fa9cc; + --mood-accent-soft: rgba(63, 169, 204, 0.13); + --mood-accent-text: #06141c; + --mood-secondary: #cfa14a; /* laiton à la lueur de la lampe */ + --mood-tertiary: #52b894; + --mood-success: #46bf7c; + --mood-warning: #d2a13c; + --mood-error: #d86458; + --mood-gradient: linear-gradient(150deg, #0a141b 0%, #101f2a 45%, #0d1a24 100%); + --mood-shadow: rgba(30, 120, 160, 0.14); + --mood-input-bg: #101f2a; + --mood-input-border: #24425a; + --mood-input-focus: #3fa9cc; - --mood-status-prepa: #d8a838; - --mood-status-prepa-bg: rgba(216, 168, 56, 0.14); - --mood-status-vote: #6488d8; - --mood-status-vote-bg: rgba(100, 136, 216, 0.14); - --mood-status-vigueur: #48c87a; - --mood-status-vigueur-bg: rgba(72, 200, 122, 0.14); - --mood-status-clos: #7888a8; - --mood-status-clos-bg: rgba(120, 136, 168, 0.10); + --mood-status-prepa: #a5987c; + --mood-status-prepa-bg: rgba(165, 152, 124, 0.12); + --mood-status-fenetre: #d2a13c; + --mood-status-fenetre-bg: rgba(210, 161, 60, 0.14); + --mood-status-vote: #5f97e0; + --mood-status-vote-bg: rgba(95, 151, 224, 0.14); + --mood-status-fige: #7d97b5; + --mood-status-fige-bg: rgba(125, 151, 181, 0.14); + --mood-status-vigueur: #46bf7c; + --mood-status-vigueur-bg: rgba(70, 191, 124, 0.13); + --mood-status-revoque: #cd7a56; + --mood-status-revoque-bg: rgba(205, 122, 86, 0.13); + --mood-status-clos: #7d8fa0; + --mood-status-clos-bg: rgba(125, 143, 160, 0.11); } /* -------------------------------------------------------------------------- - Grave — Ambre mineral, ocre lumineux (Dark) - Palette: ambre dore / ocre rouge / bleu ardoise / vert mousse + Minuit — l'encre, et la lanterne posée sur la margelle (Dark) -------------------------------------------------------------------------- */ -.mood-grave { - --mood-bg: #131210; - --mood-surface: #1c1a16; - --mood-surface-hover: #28241c; - --mood-text: #ece4d4; - --mood-text-muted: #a89870; - --mood-accent: #d8a030; - --mood-accent-soft: rgba(216, 160, 48, 0.11); - --mood-accent-text: #131210; - --mood-border: #342e24; - --mood-secondary: #6898b8; - --mood-tertiary: #c07040; - --mood-success: #58c870; - --mood-warning: #d0a030; - --mood-error: #d05050; - --mood-gradient: linear-gradient(145deg, #131210 0%, #1c1a16 40%, #16181c 100%); - --mood-shadow: rgba(180, 130, 40, 0.10); - --mood-input-bg: #1c1a16; - --mood-input-border: #3c3428; - --mood-input-focus: #d8a030; +.mood-minuit { + --mood-bg: #101014; + --mood-surface: #17171e; + --mood-surface-hover: #202029; + --mood-text: #e2e2e8; + --mood-text-muted: #8e8e9e; + --mood-accent: #cf9c3e; + --mood-accent-soft: rgba(207, 156, 62, 0.12); + --mood-accent-text: #131309; + --mood-secondary: #6c94d8; + --mood-tertiary: #58ae8e; + --mood-success: #52bd7e; + --mood-warning: #cfa03a; + --mood-error: #d25f55; + --mood-gradient: linear-gradient(150deg, #101014 0%, #17171e 45%, #131318 100%); + --mood-shadow: rgba(190, 140, 50, 0.11); + --mood-input-bg: #17171e; + --mood-input-border: #33333f; + --mood-input-focus: #cf9c3e; - --mood-status-prepa: #c07040; - --mood-status-prepa-bg: rgba(192, 112, 64, 0.15); - --mood-status-vote: #6898b8; - --mood-status-vote-bg: rgba(104, 152, 184, 0.14); - --mood-status-vigueur: #58c870; - --mood-status-vigueur-bg: rgba(88, 200, 112, 0.14); - --mood-status-clos: #807860; - --mood-status-clos-bg: rgba(128, 120, 96, 0.12); + --mood-status-prepa: #9e9584; + --mood-status-prepa-bg: rgba(158, 149, 132, 0.12); + --mood-status-fenetre: #cfa03a; + --mood-status-fenetre-bg: rgba(207, 160, 58, 0.14); + --mood-status-vote: #6c94d8; + --mood-status-vote-bg: rgba(108, 148, 216, 0.14); + --mood-status-fige: #8b8ba0; + --mood-status-fige-bg: rgba(139, 139, 160, 0.14); + --mood-status-vigueur: #52bd7e; + --mood-status-vigueur-bg: rgba(82, 189, 126, 0.13); + --mood-status-revoque: #c97a5c; + --mood-status-revoque-bg: rgba(201, 122, 92, 0.13); + --mood-status-clos: #85859a; + --mood-status-clos-bg: rgba(133, 133, 154, 0.11); } /* ========================================================================== - Global design tokens — Plus Jakarta Sans, rounded, borderless + Tokens dérivés — routes, tailles, ombres (identiques dans toutes les ambiances) ========================================================================== */ +.mood-source, .mood-margelle, .mood-nappe, .mood-minuit { + /* Routes du chemin — vocabulaire visuel fixe */ + --route-solo: var(--mood-accent); + --route-mandate: var(--mood-secondary); + --route-transmit: var(--mood-text-muted); + --route-advice: var(--mood-tertiary); + --route-collective: var(--mood-status-vote); + --route-record: var(--mood-status-prepa); + --route-urgent: var(--mood-error); -*, -*::before, -*::after { - border-color: transparent; + /* Rayons (consignes) */ + --r-card: 16px; + --r-input: 12px; + --r-pill: 20px; + --r-icon: 14px; + + /* Ombres borderless — deux couches, jamais de bordure */ + --shadow-card: 0 1px 2px var(--mood-shadow), 0 5px 18px var(--mood-shadow); + --shadow-card-hover: 0 2px 4px var(--mood-shadow), 0 10px 28px var(--mood-shadow); + --shadow-raised: 0 1px 3px var(--mood-shadow), 0 8px 24px var(--mood-shadow); } +/* ========================================================================== + Socle global — Plus Jakarta Sans, borderless, gestes + ========================================================================== */ +*, *::before, *::after { border-color: transparent; } + html { font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif; font-size: 16px; @@ -160,89 +198,154 @@ html { body { font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif; + background: var(--mood-bg); + color: var(--mood-text); transition: background-color 0.3s ease, color 0.3s ease; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -/* --- No borders anywhere --- */ button, input, select, textarea { border: none !important; outline: none; font-family: inherit; + color: inherit; } -input:focus, select:focus, textarea:focus { +input, select, textarea { + background: var(--mood-input-bg); + border-radius: var(--r-input); +} + +input:focus-visible, select:focus-visible, textarea:focus-visible, +button:focus-visible, a:focus-visible, [tabindex]:focus-visible { outline: none; - box-shadow: 0 0 0 2.5px var(--mood-accent-soft); + box-shadow: 0 0 0 2.5px var(--mood-accent-soft), 0 0 0 1.5px var(--mood-accent); } -/* --- Status pills --- */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} + +/* --- Cartes --- */ +.ld-card { + background: var(--mood-surface); + border-radius: var(--r-card); + box-shadow: var(--shadow-card); + transition: transform 0.12s ease, box-shadow 0.12s ease; +} +.ld-card--hover:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-card-hover); +} +.ld-card--hover:active { transform: translateY(0); } + +/* --- Boutons pill --- */ +.ld-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + min-height: 2.25rem; + padding: 0.5rem 1.25rem; + border-radius: var(--r-pill); + font-size: 0.9375rem; + font-weight: 700; + cursor: pointer; + background: var(--mood-accent); + color: var(--mood-accent-text); + transition: transform 0.1s ease, box-shadow 0.1s ease, filter 0.1s ease; + user-select: none; +} +.ld-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px var(--mood-shadow); } +.ld-btn:active { transform: translateY(0); } +.ld-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; } +.ld-btn--ghost { + background: var(--mood-accent-soft); + color: var(--mood-accent); +} +.ld-btn--quiet { + background: none; + color: var(--mood-text-muted); +} +.ld-btn--quiet:hover { background: var(--mood-accent-soft); color: var(--mood-text); box-shadow: none; } + +/* --- Pills d'état (mapping unique état→couleur) --- */ .status-pill { display: inline-flex; align-items: center; - padding: 5px 14px; - border-radius: 20px; + gap: 0.3rem; + padding: 4px 13px; + border-radius: var(--r-pill); font-size: 0.8125rem; font-weight: 700; letter-spacing: 0.01em; - cursor: pointer; - transition: all 0.15s ease; user-select: none; - border: none !important; + transition: all 0.15s ease; } -.status-pill:hover { - filter: brightness(1.08); - transform: translateY(-1px); -} -.status-pill.active { - box-shadow: 0 0 0 2.5px var(--mood-accent); +.status-pill.is-clickable { cursor: pointer; } +.status-pill.is-clickable:hover { filter: brightness(1.08); transform: translateY(-1px); } +.status-pill.active { box-shadow: 0 0 0 2.5px var(--mood-accent); } + +.status-draft { background: var(--mood-status-prepa-bg); color: var(--mood-status-prepa); } +.status-advice, +.status-objection { background: var(--mood-status-fenetre-bg); color: var(--mood-status-fenetre); } +.status-framing { background: var(--mood-status-prepa-bg); color: var(--mood-status-prepa); } +.status-voting { background: var(--mood-status-vote-bg); color: var(--mood-status-vote); } +.status-frozen { background: var(--mood-status-fige-bg); color: var(--mood-status-fige); } +.status-adopted { background: var(--mood-status-vigueur-bg); color: var(--mood-status-vigueur); } +.status-rejected { background: var(--mood-status-clos-bg); color: var(--mood-status-clos); text-decoration: line-through; } +.status-revoked { background: var(--mood-status-revoque-bg); color: var(--mood-status-revoque); } +.status-transmitted { background: var(--mood-status-fige-bg); color: var(--mood-status-fige); } +.status-closed { background: var(--mood-status-clos-bg); color: var(--mood-status-clos); } + +/* Suspension (frontière contestée) : fond strié */ +.status-suspended { + background: repeating-linear-gradient( + -45deg, + var(--mood-status-fenetre-bg), + var(--mood-status-fenetre-bg) 6px, + transparent 6px, + transparent 10px + ); + color: var(--mood-status-fenetre); } -.status-prepa { - background: var(--mood-status-prepa-bg); - color: var(--mood-status-prepa); -} -.status-vote { - background: var(--mood-status-vote-bg); - color: var(--mood-status-vote); -} -.status-vigueur { - background: var(--mood-status-vigueur-bg); +/* --- Le tampon 井 (signature) --- */ +.ld-stamp { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.85rem; + border-radius: 10px; + transform: rotate(-10deg); + font-weight: 800; + font-size: 0.8125rem; + letter-spacing: 0.04em; color: var(--mood-status-vigueur); -} -.status-clos { - background: var(--mood-status-clos-bg); - color: var(--mood-status-clos); + box-shadow: inset 0 0 0 2px var(--mood-status-vigueur); + background: var(--mood-status-vigueur-bg); + user-select: none; } -/* ========================================================================== - Nuxt UI overrides — borderless everything - ========================================================================== */ - -:root .mood-peps, -:root .mood-zen, -:root .mood-chagrine, -:root .mood-grave { - --ui-border: transparent; - --ui-bg: var(--mood-input-bg); - --ui-text-highlighted: var(--mood-accent); +/* --- Sceau hexagramme #48 Tsing --- */ +.app-seal { + display: block; + width: 44px; + color: var(--mood-accent); + opacity: 0.28; + filter: drop-shadow(1px 1px 0.5px rgba(0, 0, 0, 0.25)) + drop-shadow(-0.5px -0.5px 0.5px rgba(255, 255, 255, 0.15)); } -:root button, -:root [class*="UButton"], -:root [class*="u-button"], -:root [data-variant] { - border: none !important; - font-family: 'Plus Jakarta Sans', system-ui, sans-serif !important; -} - -:root input, -:root textarea, -:root select, -:root [class*="UInput"], -:root [class*="USelect"], -:root [class*="UTextarea"] { - border: none !important; - font-family: 'Plus Jakarta Sans', system-ui, sans-serif !important; +/* --- Impression A4 (PV, affiches de session) --- */ +@media print { + body { background: #ffffff !important; color: #111111 !important; } + .app-header, .app-sidebar, .app-fab, .app-seal, .no-print { display: none !important; } + .ld-card { box-shadow: none !important; } + .print-only { display: block !important; } } +.print-only { display: none; } diff --git a/frontend/app/data/persistence.ts b/frontend/app/data/persistence.ts new file mode 100644 index 0000000..cdeb81c --- /dev/null +++ b/frontend/app/data/persistence.ts @@ -0,0 +1,179 @@ +// ───────────────────────────────────────────────────────────── +// libreDecision v2 — local-first persistence (IndexedDB via idb-keyval). +// One key per collective: `ld2:` holds a Bundle-shaped state. +// Seeds and user imports go through the SAME importBundle() path. +// Sync-ready invariants live in types/domain.ts — this layer stays dumb. +// ───────────────────────────────────────────────────────────── + +import { get, set, del, keys } from 'idb-keyval' +import type { Bundle, Collective, Id, ISODate } from '~/types/domain' + +const KEY_PREFIX = 'ld2:' +const ACTIVE_KEY = 'ld2-active' + +/** In-memory state of one collective — exactly a Bundle minus export metadata. */ +export type CollectiveState = Omit + +export function emptyState(collective: Collective): CollectiveState { + return { + collective, + people: [], + circles: [], + decisions: [], + concerns: [], + objections: [], + advices: [], + assents: [], + mandates: [], + docs: [], + clauses: [], + versions: [], + protocols: [], + sessions: [], + votes: [], + } +} + +// ── Load / save ────────────────────────────────────────────── + +export async function listCollectiveIds(): Promise { + const all = await keys() + return all + .filter((k): k is string => typeof k === 'string' && k.startsWith(KEY_PREFIX)) + .map(k => k.slice(KEY_PREFIX.length)) +} + +export async function loadState(collectiveId: Id): Promise { + return await get(KEY_PREFIX + collectiveId) +} + +const pendingSaves = new Map>() + +/** Debounced write (500 ms) — every mutation calls this; last write wins. */ +export function saveStateDebounced(state: CollectiveState): void { + const id = state.collective.id + const existing = pendingSaves.get(id) + if (existing) clearTimeout(existing) + pendingSaves.set( + id, + setTimeout(() => { + pendingSaves.delete(id) + void set(KEY_PREFIX + id, toRaw(state)) + }, 500), + ) +} + +export async function saveStateNow(state: CollectiveState): Promise { + const id = state.collective.id + const existing = pendingSaves.get(id) + if (existing) { + clearTimeout(existing) + pendingSaves.delete(id) + } + await set(KEY_PREFIX + id, toRaw(state)) +} + +export async function deleteCollective(collectiveId: Id): Promise { + await del(KEY_PREFIX + collectiveId) + const active = await getActiveCollectiveId() + if (active === collectiveId) await set(ACTIVE_KEY, null) +} + +export async function getActiveCollectiveId(): Promise { + return (await get(ACTIVE_KEY)) ?? null +} + +export async function setActiveCollectiveId(id: Id | null): Promise { + await set(ACTIVE_KEY, id) +} + +// ── Export / import — the single bundle path ───────────────── + +export function toBundle(state: CollectiveState, exportedAt: ISODate): Bundle { + return { schemaVersion: 2, exportedAt, ...toRaw(state) } +} + +export async function sha256Hex(text: string): Promise { + const data = new TextEncoder().encode(text) + const digest = await crypto.subtle.digest('SHA-256', data) + return Array.from(new Uint8Array(digest)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') +} + +export interface ImportIssue { + level: 'error' | 'warning' + message: string +} + +/** Structural validation — never throws, returns readable French issues. */ +export function validateBundle(raw: unknown): { bundle?: Bundle; issues: ImportIssue[] } { + const issues: ImportIssue[] = [] + if (typeof raw !== 'object' || raw === null) { + return { issues: [{ level: 'error', message: 'Ce fichier ne contient pas un collectif lisible.' }] } + } + const b = raw as Partial + if (b.schemaVersion !== 2) + issues.push({ level: 'error', message: 'Version de fichier inconnue (schemaVersion ≠ 2).' }) + if (!b.collective?.id || !b.collective?.slug || !b.collective?.name) + issues.push({ level: 'error', message: 'Le collectif du fichier est incomplet (id, slug ou nom manquant).' }) + for (const key of [ + 'people', 'circles', 'decisions', 'concerns', 'objections', 'advices', 'assents', + 'mandates', 'docs', 'clauses', 'versions', 'protocols', 'sessions', 'votes', + ] as const) { + if (!Array.isArray(b[key])) + issues.push({ level: 'error', message: `Collection manquante ou invalide : ${key}.` }) + } + // Invariant: every collective carries a Consent protocol (import validates it). + if (Array.isArray(b.protocols) && !b.protocols.some(p => p.method === 'consent')) + issues.push({ + level: 'warning', + message: 'Aucun protocole de consentement — les chemins collectifs retomberont sur « décider sur avis » jusqu\'à sa création.', + }) + if (issues.some(i => i.level === 'error')) return { issues } + return { bundle: b as Bundle, issues } +} + +export interface ImportResult { + state?: CollectiveState + issues: ImportIssue[] + collided: boolean +} + +/** + * Import a bundle as a new local collective. + * Foreign bundles (exportedAt present, not one of ours) get a lineage stamp — « essaimé de … ». + */ +export async function importBundle(json: string, opts?: { asSeed?: boolean }): Promise { + let raw: unknown + try { + raw = JSON.parse(json) + } catch { + return { issues: [{ level: 'error', message: 'Fichier illisible : ce n\'est pas du JSON valide.' }], collided: false } + } + const { bundle, issues } = validateBundle(raw) + if (!bundle) return { issues, collided: false } + + const existingIds = await listCollectiveIds() + const collided = existingIds.includes(bundle.collective.id) + + const { schemaVersion: _v, exportedAt, ...stateRest } = bundle + const state: CollectiveState = stateRest + + if (!opts?.asSeed) { + state.collective = { + ...state.collective, + lineage: { + sourceSlug: bundle.collective.slug, + exportedAt, + sha256: await sha256Hex(json), + }, + } + } + return { state, issues, collided } +} + +/** Deep-clone to plain JSON — strips Pinia/Vue reactivity proxies before IndexedDB. */ +function toRaw(value: T): T { + return JSON.parse(JSON.stringify(value)) as T +} diff --git a/frontend/app/data/seeds/atelier-du-canal.bundle.json b/frontend/app/data/seeds/atelier-du-canal.bundle.json new file mode 100644 index 0000000..c6de4cd --- /dev/null +++ b/frontend/app/data/seeds/atelier-du-canal.bundle.json @@ -0,0 +1,3841 @@ +{ + "schemaVersion": 2, + "exportedAt": "2026-08-11T12:00:00.000Z", + "collective": { + "id": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "slug": "atelier-du-canal", + "name": "L’Atelier du Canal", + "color": "#0e7490", + "icon": "i-lucide-hammer", + "template": "association", + "isTransparent": true, + "pactDocId": "931d78d6-6931-4570-a7ae-058a344dc105", + "rootCircleId": "d35d01d4-4570-4e75-9c64-f00ba5118624", + "createdAt": "2026-01-10T09:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z" + }, + "people": [ + { + "id": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Nadia Benkacem", + "isMe": false, + "attributes": { + "heures/mois": 12 + } + }, + { + "id": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Marcel Aubry", + "isMe": false, + "attributes": { + "heures/mois": 6 + } + }, + { + "id": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Chloé Ferrand", + "isMe": false + }, + { + "id": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Ibrahim Diallo", + "isMe": false + }, + { + "id": "62789d12-de5a-43bf-b7f4-111c642ec156", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Suzanne Kowalski", + "isMe": false + }, + { + "id": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Théo Lemoine", + "isMe": false, + "attributes": { + "heures/mois": 8 + } + }, + { + "id": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Awa N'Diaye", + "isMe": false, + "attributes": { + "heures/mois": 20 + } + }, + { + "id": "318cf576-aa00-4cba-be63-e62551b4693e", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Pierre-Yves Guivarch", + "isMe": false + }, + { + "id": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Lucia Moreira", + "isMe": false + }, + { + "id": "4e1455b5-9697-48f9-b559-6edfe889b339", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Josette Renard", + "isMe": false + }, + { + "id": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Karim Haddad", + "isMe": false + }, + { + "id": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-10T10:00:00.000Z", + "updatedAt": "2026-01-10T10:00:00.000Z", + "displayName": "Élise Vandenberghe", + "isMe": false + } + ], + "circles": [ + { + "id": "d35d01d4-4570-4e75-9c64-f00ba5118624", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-12T10:00:00.000Z", + "updatedAt": "2026-01-12T10:00:00.000Z", + "name": "Tous", + "purpose": "Toutes les membres de l'Atelier du Canal réunies — le cercle racine.", + "memberIds": [ + "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "318cf576-aa00-4cba-be63-e62551b4693e", + "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "4e1455b5-9697-48f9-b559-6edfe889b339", + "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "cd7b8066-88ab-47af-9d53-acb467a5ea44" + ], + "domains": [] + }, + { + "id": "9f399d53-6cca-4375-bee7-374ae9cdd53f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-12T10:05:00.000Z", + "updatedAt": "2026-01-12T10:05:00.000Z", + "name": "Bureau", + "purpose": "Faire tourner l'association au quotidien : finances, papiers, liens avec la mairie.", + "kind": "team", + "memberIds": [ + "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "d0a27b00-af87-4715-9bde-20cdbe2f29c5" + ], + "parentCircleId": "d35d01d4-4570-4e75-9c64-f00ba5118624", + "domains": [ + "gestion", + "finances" + ] + }, + { + "id": "db3def42-7c4c-4163-907e-3a818045db67", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-12T10:10:00.000Z", + "updatedAt": "2026-01-12T10:10:00.000Z", + "name": "Ateliers", + "purpose": "Celles et ceux qui animent les ateliers partagés : bois, textile, vélo.", + "kind": "theme", + "memberIds": [ + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "318cf576-aa00-4cba-be63-e62551b4693e", + "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "cd7b8066-88ab-47af-9d53-acb467a5ea44" + ], + "parentCircleId": "d35d01d4-4570-4e75-9c64-f00ba5118624", + "domains": [ + "ateliers", + "bois", + "textile", + "velo" + ] + }, + { + "id": "18a487ab-c0ec-43a4-bc15-e4861b949878", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-12T10:15:00.000Z", + "updatedAt": "2026-01-12T10:15:00.000Z", + "name": "Le local du quai", + "purpose": "Les habitué·es du local au 12 quai des Chalands — clés, rangement, voisinage.", + "kind": "place", + "memberIds": [ + "4e1455b5-9697-48f9-b559-6edfe889b339", + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "dfe07fca-dc13-4ebc-9e11-0dced155e13f" + ], + "parentCircleId": "d35d01d4-4570-4e75-9c64-f00ba5118624", + "domains": [ + "local", + "quai" + ] + } + ], + "decisions": [ + { + "id": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-12T19:00:00.000Z", + "updatedAt": "2026-04-16T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "3c5e8e6c-c66d-4796-81bd-937ce602ba1a" + ], + "measurerIds": [ + "343a60a7-add5-46b1-b6d6-9093ed2ad559" + ], + "visibility": "collective", + "authorId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "title": "Adopter notre Pacte", + "body": "Deux soirées d’assemblée au local pour poser notre contrat social : trois boussoles, une finalité, des seuils courts et des protocoles sans camps.", + "brief": { + "context": "L’association se crée : il nous faut un cadre commun avant la première saison d’ateliers.", + "effects": [ + { + "label": "Se doter d’un cadre commun pour décider sans chef et sans vote-couperet", + "target": "12 membres signataires au 1er février" + } + ] + }, + "resources": { + "note": "Deux soirées d’assemblée et l’énergie de commencer" + }, + "tags": [ + "pacte", + "fondation" + ], + "reversibility": "costly", + "weight": "structural", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R5", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "status": "adopted", + "decidedAt": "2026-01-15T18:00:00.000Z", + "review": { + "dueAt": "2026-04-15T18:00:00.000Z", + "verdict": "confirmed", + "note": "Le Pacte a tenu ses trois premiers mois — personne n’a demandé à le rouvrir.", + "decidedAt": "2026-04-16T09:00:00.000Z" + }, + "engraving": { + "sha256": "5f9c6ea9fb7eea6651b99ac38fe1f955478ee52e0768ee67b1135e3f2ee2ab7a", + "engravedAt": "2026-01-15T18:00:00.000Z", + "proofLevel": "local" + } + }, + { + "id": "2822b935-48db-483d-851b-eb38b7d8659d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-19T19:00:00.000Z", + "updatedAt": "2026-04-22T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "343a60a7-add5-46b1-b6d6-9093ed2ad559" + ], + "measurerIds": [ + "62789d12-de5a-43bf-b7f4-111c642ec156" + ], + "visibility": "collective", + "authorId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "title": "Adopter le Règlement intérieur", + "body": "Huit clauses de vie commune : le local, le matériel, la sécurité, l’adhésion.", + "brief": { + "context": "Le Pacte pose le cadre ; il reste à régler la vie quotidienne du local.", + "effects": [ + { + "label": "Des règles de vie claires pour le local et les ateliers", + "target": "8 clauses adoptées avant l’ouverture de février" + } + ] + }, + "resources": { + "note": "Une soirée de rédaction collective au local" + }, + "tags": [ + "reglement", + "fondation" + ], + "reversibility": "easy", + "weight": "structural", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R5", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "status": "adopted", + "decidedAt": "2026-01-22T18:30:00.000Z", + "review": { + "dueAt": "2026-04-22T18:00:00.000Z", + "verdict": "confirmed", + "note": "Le cahier des prêts et le rangement tiennent — seule la clause d’accueil demande révision.", + "decidedAt": "2026-04-22T09:00:00.000Z" + } + }, + { + "id": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-14T19:00:00.000Z", + "updatedAt": "2026-01-29T19:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "title": "Confier la trésorerie pour un an", + "body": "Un compte à ouvrir, des cotisations à suivre, des rapports trimestriels : la trésorerie mérite un mandat borné et rendu.", + "brief": { + "effects": [ + { + "label": "Des comptes tenus et rendus tous les trimestres", + "target": "4 rapports sur l’année" + } + ] + }, + "resources": { + "note": "Quelques heures par mois et la signature du compte associatif" + }, + "tags": [ + "finances", + "mandat" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R6", + "protocolId": "d9af660a-5fea-4984-9ada-a3df18220783", + "createsMandate": { + "title": "Trésorerie", + "domainCircleIds": [ + "9f399d53-6cca-4375-bee7-374ae9cdd53f" + ], + "domainTags": [ + "finances", + "tresorerie" + ], + "durationDays": 365, + "reportEveryDays": 90 + }, + "status": "adopted", + "decidedAt": "2026-01-29T19:00:00.000Z" + }, + { + "id": "74875264-2a7e-4bdc-a543-902d56921cb1", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-20T09:00:00.000Z", + "updatedAt": "2026-07-21T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "title": "Renouveler l’assurance du local", + "body": "Même contrat que l’an dernier, prime quasi inchangée — je renouvelle avant l’échéance du 31 juillet.", + "resources": { + "note": "Prime annuelle de l’assurance du local", + "amount": 312, + "unit": "€" + }, + "tags": [ + "finances", + "local", + "contrats" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "9f399d53-6cca-4375-bee7-374ae9cdd53f" + ], + "personIds": [] + }, + "route": "mandate", + "triageRule": "R0a", + "underMandateId": "a76dfe11-db3e-4356-b540-87109ee02199", + "status": "adopted", + "windowEndsAt": "2026-07-21T09:00:00.000Z", + "decidedAt": "2026-07-21T09:00:00.000Z" + }, + { + "id": "1e46a8f1-89f6-4018-b35c-b9c9e3f28551", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "title": "Ouvrir un livret d’épargne associatif", + "body": "La caisse dort sur le compte courant ; un livret associatif rapporterait de quoi payer l’assurance.", + "resources": { + "note": "Déplacer 1 500 € de la caisse vers un livret rémunéré", + "amount": 1500, + "unit": "€" + }, + "tags": [ + "finances", + "epargne" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "9f399d53-6cca-4375-bee7-374ae9cdd53f" + ], + "personIds": [] + }, + "route": "mandate", + "triageRule": "R0a", + "underMandateId": "a76dfe11-db3e-4356-b540-87109ee02199", + "status": "objection", + "windowEndsAt": "2026-08-12T10:00:00.000Z" + }, + { + "id": "3061eba5-2c2b-4117-bedd-84a3be46d00d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T17:45:00.000Z", + "updatedAt": "2026-08-10T17:45:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "title": "Organiser la fête des voisins de septembre", + "tags": [ + "fete", + "quartier" + ], + "reversibility": "easy", + "weight": "light", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R4", + "status": "draft" + }, + { + "id": "06c7e749-0fe2-41bf-ab59-3c3fadc5928d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T09:00:00.000Z", + "updatedAt": "2026-08-11T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "title": "Repeindre la façade du local", + "body": "Un week-end de chantier participatif, peinture microporeuse, couleur à choisir avec les habitué·es.", + "baselineNote": "Aujourd’hui : la façade s’écaille côté quai, dernière peinture en 2019.", + "resources": { + "note": "Peinture et petit matériel, un week-end de bénévolat", + "amount": 140, + "unit": "€" + }, + "tags": [ + "local", + "travaux" + ], + "reversibility": "easy", + "weight": "light", + "scope": { + "selfOnly": false, + "circleIds": [ + "18a487ab-c0ec-43a4-bc15-e4861b949878" + ], + "personIds": [] + }, + "route": "advice", + "triageRule": "R3", + "status": "advice", + "windowEndsAt": "2026-08-13T09:00:00.000Z" + }, + { + "id": "3c9829f6-196e-4029-ac60-06039ceb121f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T10:00:00.000Z", + "updatedAt": "2026-08-10T14:00:00.000Z", + "urgent": false, + "routeOverridden": true, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "title": "Poser une serrure connectée au local", + "body": "Fini les doubles de clés qui circulent : un digicode avec codes nominatifs, réversible si ça ne convient pas.", + "resources": { + "note": "Serrure connectée et pose", + "amount": 180, + "unit": "€" + }, + "tags": [ + "local", + "cles" + ], + "reversibility": "costly", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "18a487ab-c0ec-43a4-bc15-e4861b949878" + ], + "personIds": [] + }, + "route": "solo", + "triageRule": "R2", + "overrideNote": "J’allège : je finance la pose et c’est démontable — fenêtre d’objection ouverte en contrepartie.", + "status": "objection", + "windowEndsAt": "2026-08-12T10:00:00.000Z", + "windowSuspendedAt": "2026-08-10T14:00:00.000Z" + }, + { + "id": "88cced14-15b9-4291-9534-59f6fa141b92", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T11:00:00.000Z", + "urgent": false, + "routeOverridden": true, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "title": "Remplacer le lave-linge partagé par un modèle d’occasion", + "body": "Celui du local fuit depuis juin ; le dépôt-vente du quai en propose un garanti six mois.", + "resources": { + "note": "Achat d’un lave-linge d’occasion garanti", + "amount": 220, + "unit": "€" + }, + "tags": [ + "local", + "materiel" + ], + "reversibility": "costly", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "18a487ab-c0ec-43a4-bc15-e4861b949878" + ], + "personIds": [] + }, + "route": "solo", + "triageRule": "R2", + "overrideNote": "J’allège : l’achat est modeste et le vendeur reprend l’ancien — fenêtre d’objection ouverte en contrepartie.", + "status": "objection", + "windowEndsAt": "2026-08-12T10:00:00.000Z" + }, + { + "id": "6bae3695-3fc5-4f70-8fbe-3b8a8076b416", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-06T09:00:00.000Z", + "updatedAt": "2026-08-08T14:30:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "title": "Réviser l’accueil des nouveaux", + "body": "Trop de monde pousse la porte le samedi sans savoir par où commencer — deux pistes sont sur la table.", + "baselineNote": "Aujourd’hui : un accueil au fil de l’eau, sans référent·e, et un petit guide souvent épuisé.", + "brief": { + "context": "Depuis le printemps, chaque samedi amène trois ou quatre nouvelles têtes.", + "effects": [ + { + "label": "Chaque nouvelle personne repart accompagnée", + "target": "plus aucun samedi sans accueillant·e d’ici octobre" + } + ] + }, + "resources": { + "note": "Un binôme d’accueil par samedi d’ouverture" + }, + "tags": [ + "accueil", + "reglement" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R5", + "amendsClauseId": "8606f567-ebf8-4e61-a015-287c14a673ea", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "status": "framing", + "windowEndsAt": "2026-08-20T09:00:00.000Z" + }, + { + "id": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T18:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "title": "Adhérer au réseau des ateliers partagés d’Île-de-France", + "body": "Quatre ateliers voisins mutualisent outillage, formations et bons plans fournisseurs.", + "baselineNote": "Aujourd’hui : nous fonctionnons seuls, sans échange avec les ateliers voisins.", + "brief": { + "effects": [ + { + "label": "Mutualiser l’outillage et les formations avec les ateliers voisins", + "target": "2 formations partagées avant décembre" + } + ] + }, + "resources": { + "note": "Cotisation annuelle au réseau", + "amount": 120, + "unit": "€" + }, + "tags": [ + "reseau", + "ateliers" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R5", + "protocolId": "ab3c947a-76ff-438e-87dd-24c6d053afac", + "status": "voting" + }, + { + "id": "c172cc05-571a-4d75-84b3-5bb602582534", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-20T18:00:00.000Z", + "updatedAt": "2026-05-07T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a" + ], + "measurerIds": [ + "62789d12-de5a-43bf-b7f4-111c642ec156" + ], + "visibility": "collective", + "authorId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "title": "Chauffer le local au poêle à granulés", + "body": "Les convecteurs datent du précédent locataire et chauffent mal ; un poêle à granulés couvrirait la grande salle.", + "baselineNote": "Aujourd’hui : trois convecteurs électriques, 620 € de facture l’hiver dernier.", + "brief": { + "context": "Le local est glacial les soirs d’atelier de novembre à mars.", + "symptomsVsCauses": "Le froid est le symptôme ; la cause est un chauffage inadapté au volume de la grande salle.", + "effects": [ + { + "label": "Réduire la facture de chauffage du local", + "target": "≤ 400 € pour l’hiver 2026-2027" + }, + { + "label": "Un local chauffé les soirs d’atelier" + } + ] + }, + "resources": { + "note": "Achat et pose du poêle, granulés pour l’hiver", + "amount": 2400, + "unit": "€" + }, + "tags": [ + "local", + "chauffage", + "travaux" + ], + "reversibility": "costly", + "weight": "structural", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R5", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "status": "adopted", + "decidedAt": "2026-05-07T09:00:00.000Z", + "review": { + "dueAt": "2026-08-05T09:00:00.000Z" + } + }, + { + "id": "04ee2ece-2e75-4394-a650-3ad7bea59123", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-03-02T10:00:00.000Z", + "updatedAt": "2026-07-01T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "343a60a7-add5-46b1-b6d6-9093ed2ad559" + ], + "measurerIds": [ + "cd7b8066-88ab-47af-9d53-acb467a5ea44" + ], + "visibility": "collective", + "authorId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "title": "Confier la communication à un prestataire", + "body": "Une lettre d’information mensuelle sous-traitée à une petite agence du quartier.", + "brief": { + "effects": [ + { + "label": "Une lettre d’information mensuelle qui sort à l’heure", + "target": "10 parutions par an", + "measured": { + "note": "Trois lettres parues en quatre mois, sans lien avec la vie du quartier.", + "at": "2026-06-20T09:00:00.000Z", + "byId": "cd7b8066-88ab-47af-9d53-acb467a5ea44" + } + } + ] + }, + "resources": { + "note": "Forfait mensuel du prestataire", + "amount": 90, + "unit": "€" + }, + "tags": [ + "communication" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R4", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "status": "revoked", + "decidedAt": "2026-03-10T09:00:00.000Z", + "review": { + "dueAt": "2026-06-10T09:00:00.000Z", + "verdict": "revoke", + "note": "La lettre sortait, mais elle ne parlait plus de nous.", + "decidedAt": "2026-07-01T09:00:00.000Z" + } + }, + { + "id": "69bc4dc5-b866-4a47-b2f3-46fa2d484fac", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-25T10:00:00.000Z", + "updatedAt": "2026-07-01T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "781e0bb0-8006-4fe9-9501-f9c4533b216e" + ], + "measurerIds": [], + "visibility": "collective", + "authorId": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "title": "Reprendre la communication en interne", + "body": "Ce qu’on en a appris : la parole du quartier ne se sous-traite pas. Élise et Karim reprennent la lettre, plus courte et plus vivante.", + "resources": { + "note": "Deux heures par mois d’Élise et de Karim" + }, + "tags": [ + "communication" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R4", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "parentDecisionId": "04ee2ece-2e75-4394-a650-3ad7bea59123", + "chainKind": "revocation", + "status": "adopted", + "decidedAt": "2026-07-01T09:00:00.000Z" + }, + { + "id": "64aea56e-93a3-43b6-8003-fe7ad9c7c86b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-02T07:40:00.000Z", + "updatedAt": "2026-08-02T07:45:00.000Z", + "urgent": true, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "title": "Couper l’eau de l’atelier bois — fuite sur le circuit", + "body": "Fuite au raccord de l’évier ce matin : vanne générale fermée en attendant le plombier.", + "tags": [ + "local", + "urgence" + ], + "reversibility": "easy", + "weight": "light", + "scope": { + "selfOnly": false, + "circleIds": [ + "18a487ab-c0ec-43a4-bc15-e4861b949878" + ], + "personIds": [] + }, + "route": "solo", + "triageRule": "R-U", + "status": "adopted", + "decidedAt": "2026-08-02T07:45:00.000Z" + }, + { + "id": "25edc8b0-36e6-444a-8982-fb426e1cf964", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-02T08:00:00.000Z", + "updatedAt": "2026-08-04T08:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "title": "Ratifier la coupure d’eau d’urgence", + "body": "Décidé en urgence le 2 août — le collectif ratifie : la vanne reste fermée jusqu’au passage du plombier.", + "tags": [ + "local", + "urgence" + ], + "reversibility": "easy", + "weight": "light", + "scope": { + "selfOnly": false, + "circleIds": [ + "18a487ab-c0ec-43a4-bc15-e4861b949878" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R-U", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "parentDecisionId": "64aea56e-93a3-43b6-8003-fe7ad9c7c86b", + "chainKind": "ratification", + "status": "adopted", + "windowEndsAt": "2026-08-04T08:00:00.000Z", + "decidedAt": "2026-08-04T08:00:00.000Z" + }, + { + "id": "c1b50996-bdf3-4c04-8060-fef8f80f9bfa", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-08T12:30:00.000Z", + "updatedAt": "2026-08-08T12:30:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "title": "Le marché du samedi est reconduit", + "tags": [ + "marche", + "quartier" + ], + "reversibility": "easy", + "weight": "light", + "scope": { + "selfOnly": false, + "circleIds": [ + "18a487ab-c0ec-43a4-bc15-e4861b949878" + ], + "personIds": [] + }, + "route": "record", + "triageRule": "R0c", + "decidedHow": "Décidé au café du quai après le marché, à main levée — comme d’habitude.", + "status": "adopted", + "decidedAt": "2026-08-08T12:30:00.000Z" + }, + { + "id": "fd343327-1d0c-48e4-a92f-a32eeac7845c", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-28T18:00:00.000Z", + "updatedAt": "2026-07-28T18:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "62789d12-de5a-43bf-b7f4-111c642ec156" + ], + "measurerIds": [], + "visibility": "collective", + "authorId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "title": "Réaménager le local du quai", + "body": "Le local déborde : électricité vieillissante, zones d’atelier qui se marchent dessus, espace commun encombré. On découpe en micro-décisions.", + "brief": { + "context": "Trois ans d’activité ont rempli le local plus vite que prévu.", + "effects": [ + { + "label": "Un local où chaque atelier a sa zone et où l’on circule", + "target": "zéro établi partagé entre deux ateliers d’ici novembre" + } + ] + }, + "resources": { + "note": "Budget travaux et week-ends de chantier", + "amount": 900, + "unit": "€" + }, + "tags": [ + "local", + "travaux", + "amenagement" + ], + "reversibility": "costly", + "weight": "structural", + "scope": { + "selfOnly": false, + "circleIds": [ + "18a487ab-c0ec-43a4-bc15-e4861b949878" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R5", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "status": "framing" + }, + { + "id": "00e9df0e-9975-4a4a-aa83-58fd3392c05c", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-29T09:00:00.000Z", + "updatedAt": "2026-08-06T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "7589bff0-c5bd-4052-9144-bed92b0e7a3a" + ], + "measurerIds": [], + "visibility": "collective", + "authorId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "title": "Refaire l’électricité de l’atelier bois", + "body": "Des prises aux normes près des machines, un tableau dédié — l’électricien de la rue Neuve est disponible fin août.", + "brief": { + "effects": [ + { + "label": "Des prises aux normes près des machines", + "target": "attestation de conformité obtenue" + } + ] + }, + "resources": { + "note": "Intervention de l’électricien et fournitures", + "amount": 450, + "unit": "€" + }, + "tags": [ + "local", + "travaux", + "electricite" + ], + "reversibility": "costly", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "18a487ab-c0ec-43a4-bc15-e4861b949878" + ], + "personIds": [] + }, + "route": "advice", + "triageRule": "R3", + "parentDecisionId": "fd343327-1d0c-48e4-a92f-a32eeac7845c", + "chainKind": "element", + "status": "adopted", + "windowEndsAt": "2026-07-31T09:00:00.000Z", + "decidedAt": "2026-08-06T09:00:00.000Z" + }, + { + "id": "0b07b85b-22c0-4fa1-8ce2-4b62dc518065", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-05T18:00:00.000Z", + "updatedAt": "2026-08-10T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "title": "Choisir l’agencement de l’espace commun", + "body": "Grande table centrale ou coin canapé près de la fenêtre : le plan est affiché au local.", + "tags": [ + "local", + "amenagement" + ], + "reversibility": "easy", + "weight": "light", + "scope": { + "selfOnly": false, + "circleIds": [ + "18a487ab-c0ec-43a4-bc15-e4861b949878" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R4", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "parentDecisionId": "fd343327-1d0c-48e4-a92f-a32eeac7845c", + "chainKind": "element", + "status": "voting" + }, + { + "id": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-03T18:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "f790d11d-a84f-4ed1-80c9-7077b870e099" + ], + "measurerIds": [], + "visibility": "collective", + "authorId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "title": "Répartir le budget d’ateliers 2026", + "body": "Le budget annuel des ateliers se répartit au curseur : bois, textile, vélo — et une réserve calculée pour les imprévus.", + "baselineNote": "Aujourd’hui : 40 % bois, 30 % textile, 20 % vélo, 10 % de réserve.", + "brief": { + "effects": [ + { + "label": "Un budget d’ateliers réparti en connaissance de cause", + "target": "100 % ventilés avant la rentrée" + } + ] + }, + "resources": { + "note": "budget annuel ateliers", + "amount": 1200, + "unit": "€" + }, + "tags": [ + "budget", + "ateliers" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "db3def42-7c4c-4163-907e-3a818045db67", + "9f399d53-6cca-4375-bee7-374ae9cdd53f" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R5", + "protocolId": "ddfabf00-4b0d-4ee0-98b2-bb51b6845c17", + "paramSpec": { + "constraint": "sum100", + "impactAttrKey": "heures/mois", + "params": [ + { + "key": "bois", + "label": "Atelier bois", + "kind": "share", + "min": 10, + "max": 60, + "step": 5, + "unit": "%", + "baseline": 40 + }, + { + "key": "textile", + "label": "Atelier textile", + "kind": "share", + "min": 10, + "max": 60, + "step": 5, + "unit": "%", + "baseline": 30 + }, + { + "key": "velo", + "label": "Atelier vélo", + "kind": "share", + "min": 0, + "max": 50, + "step": 5, + "unit": "%", + "baseline": 20 + }, + { + "key": "reserve", + "label": "Réserve", + "kind": "share", + "min": 0, + "max": 30, + "step": 1, + "unit": "%", + "baseline": 10, + "derived": true + } + ] + }, + "status": "voting" + }, + { + "id": "38e46122-8147-4503-9105-f90a07055456", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-24T18:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "343a60a7-add5-46b1-b6d6-9093ed2ad559" + ], + "measurerIds": [], + "visibility": "collective", + "authorId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "title": "Fixer les tarifs d’adhésion 2027", + "body": "Un tarif plein et un tarif solidaire, réglés au curseur — les votes sont figés, la médiane attend le geste de Chloé.", + "baselineNote": "Aujourd’hui : 30 € plein tarif, 10 € solidaire.", + "brief": { + "effects": [ + { + "label": "Une adhésion accessible qui couvre les charges", + "target": "≥ 480 € de cotisations en 2027" + } + ] + }, + "resources": { + "note": "Les cotisations financent la moitié du budget annuel" + }, + "tags": [ + "budget", + "adhesion" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R5", + "protocolId": "ddfabf00-4b0d-4ee0-98b2-bb51b6845c17", + "paramSpec": { + "constraint": "none", + "params": [ + { + "key": "plein", + "label": "Tarif plein", + "kind": "slider", + "min": 20, + "max": 60, + "step": 5, + "unit": "€", + "baseline": 30 + }, + { + "key": "solidaire", + "label": "Tarif solidaire", + "kind": "slider", + "min": 5, + "max": 25, + "step": 1, + "unit": "€", + "baseline": 10 + } + ] + }, + "status": "voting" + }, + { + "id": "1bd63c1e-8454-4d0d-81ec-f05d15fbb31f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T11:00:00.000Z", + "updatedAt": "2026-08-09T11:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "title": "Renouveler le contrat d’électricité du local", + "body": "Le mandat Trésorerie couvre les contrats du local — je transmets à Marcel.", + "tags": [ + "local", + "contrats" + ], + "reversibility": "easy", + "weight": "light", + "scope": { + "selfOnly": false, + "circleIds": [ + "9f399d53-6cca-4375-bee7-374ae9cdd53f" + ], + "personIds": [] + }, + "route": "transmit", + "triageRule": "R0b", + "status": "transmitted" + }, + { + "id": "ed6dc706-29f9-4520-9131-b8f6a88efaea", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-10T18:00:00.000Z", + "updatedAt": "2026-06-30T18:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [], + "measurerIds": [], + "visibility": "collective", + "authorId": "318cf576-aa00-4cba-be63-e62551b4693e", + "title": "Acheter une imprimante 3D", + "body": "Prototyper les pièces cassées plutôt que racheter — filament et formation compris.", + "brief": { + "effects": [ + { + "label": "Prototyper les pièces cassées plutôt que racheter" + } + ] + }, + "resources": { + "note": "Achat, filament et formation", + "amount": 650, + "unit": "€" + }, + "tags": [ + "materiel", + "ateliers" + ], + "reversibility": "costly", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "db3def42-7c4c-4163-907e-3a818045db67" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R5", + "protocolId": "ab3c947a-76ff-438e-87dd-24c6d053afac", + "status": "rejected", + "decidedAt": "2026-06-30T18:00:00.000Z" + }, + { + "id": "a57378de-cefe-4361-a083-32c4aeea9863", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-03-15T10:00:00.000Z", + "updatedAt": "2026-05-12T09:00:00.000Z", + "urgent": false, + "routeOverridden": false, + "stewardIds": [ + "4e1455b5-9697-48f9-b559-6edfe889b339" + ], + "measurerIds": [ + "dfe07fca-dc13-4ebc-9e11-0dced155e13f" + ], + "visibility": "collective", + "authorId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "title": "Organiser la braderie de printemps", + "body": "Un samedi de braderie devant le local : vide-ateliers, réparations minute, buvette à prix libre.", + "brief": { + "effects": [ + { + "label": "Financer les ateliers d’été", + "target": "≥ 300 € de recettes", + "measured": { + "note": "342 € de recettes et douze nouvelles adhésions.", + "at": "2026-05-02T09:00:00.000Z", + "byId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f" + } + } + ] + }, + "resources": { + "note": "Un samedi de bénévolat et les tables prêtées par la mairie" + }, + "tags": [ + "fete", + "budget", + "quartier" + ], + "reversibility": "easy", + "weight": "binding", + "scope": { + "selfOnly": false, + "circleIds": [ + "d35d01d4-4570-4e75-9c64-f00ba5118624" + ], + "personIds": [] + }, + "route": "collective", + "triageRule": "R4", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "status": "closed", + "decidedAt": "2026-03-25T09:00:00.000Z", + "review": { + "dueAt": "2026-05-10T09:00:00.000Z", + "verdict": "confirmed", + "note": "La braderie a rempli la caisse et le trottoir — à refaire l’an prochain.", + "decidedAt": "2026-05-12T09:00:00.000Z" + } + } + ], + "concerns": [ + { + "id": "e426a925-681a-43a8-9d96-e96ccd4966ca", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "41c5fd75-0445-462e-97f6-ae3b6d5c421d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "5bf8fee5-c72e-4830-9377-9bf7dd15054b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "b5b2d066-62af-4c70-88a7-a3d5377e687c", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "2a77789c-3fdc-4241-9a46-01e250f5d8b1", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "5b7e6df7-bf0c-4587-b351-03867ad2ea19", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "de8c5e42-ebf9-4fea-ab40-ffe5b295c83d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "cad4a375-3493-4ff4-b3c2-3cb37ba12ac1", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "318cf576-aa00-4cba-be63-e62551b4693e", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "af275341-aa5b-45d1-8341-da03d654af8a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "5e021df1-c0e8-46eb-b1b9-dd4e0769060d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "f8832052-e315-4a9b-af05-9223176e4ee5", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "d425fa70-1d08-4f6e-8137-d3adb247c6c8", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-15T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "personId": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "origin": "computed", + "reason": "Membre du cercle Tous — cercle électeur du mandat", + "beforeSnapshot": true + }, + { + "id": "f5c1371a-6e0f-446f-b948-2356511ee5e0", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "b7e16a51-e0f8-41b3-95f8-14bfc68749f8", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "a534a140-56b7-4236-8906-cb7dd1049bd0", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "0b1b5e4b-c575-4a6c-9f0d-66a181329cd7", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "00a87a19-fa11-4783-8658-ff2422acdd77", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "aeaf2d4b-ebef-40d3-9a90-f7eccca1f778", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "710908d9-4a85-48dd-ad1c-a7bdf1e33ab5", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "a3b0bdbc-a5ec-4a10-8ac5-e8cfbab08164", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "318cf576-aa00-4cba-be63-e62551b4693e", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "501a0623-baed-4b37-bc25-c7bd8cf155eb", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "8939ab28-3a21-4f2a-833e-3f5c583d6b91", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "88fbaa18-b01d-4b4f-a509-d5bdec35066d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "e97af53d-1ffc-4e36-bb60-00bf6d0feb57", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "personId": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "a4a21a2a-fe88-4d3b-9753-47b99ebe9191", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "e14fce56-1a73-43fb-be4d-c2df3c3b7ab4", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "3245c801-92ba-4e3f-965f-4aa123b33192", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "e5e385fc-aa43-4816-8717-60770b993751", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "a56cbce0-9168-487a-a8a2-bceffc394ec9", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "5daa18c1-c8f5-4964-95e2-bf90322a8bce", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "47cc8cba-9661-4709-95a8-af4011435e66", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "b49e3775-d318-49a0-928e-a47219951c6a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "318cf576-aa00-4cba-be63-e62551b4693e", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "cdf311e3-11b5-4db2-b635-26dff5e9996b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "e995d669-6bf2-4f17-a2f4-6150f1243a0d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "9945b15c-7e18-46f4-b991-e7ad9ea04f81", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "97642466-b734-45f4-a606-b9da09be8481", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-04-30T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "ecee9ec4-98fd-4bd1-9ae6-1b518a10757d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T09:00:00.000Z", + "updatedAt": "2026-08-10T09:00:00.000Z", + "decisionId": "0b07b85b-22c0-4fa1-8ce2-4b62dc518065", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true, + "priority": 3 + }, + { + "id": "cf837a65-dea1-4d9e-a6c3-2088bdc7a0a4", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T09:00:00.000Z", + "updatedAt": "2026-08-10T09:00:00.000Z", + "decisionId": "0b07b85b-22c0-4fa1-8ce2-4b62dc518065", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true, + "priority": 3 + }, + { + "id": "11f7e53a-fb5d-4c7f-9064-c3f154846395", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T09:00:00.000Z", + "updatedAt": "2026-08-10T09:00:00.000Z", + "decisionId": "0b07b85b-22c0-4fa1-8ce2-4b62dc518065", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true, + "priority": 1 + }, + { + "id": "aa57892d-a757-4c2d-a1d2-57636991ff9a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T09:00:00.000Z", + "updatedAt": "2026-08-10T09:00:00.000Z", + "decisionId": "0b07b85b-22c0-4fa1-8ce2-4b62dc518065", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true, + "priority": 2 + }, + { + "id": "6382fa52-7668-4816-800d-041dca8f1113", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T09:00:00.000Z", + "updatedAt": "2026-08-10T09:00:00.000Z", + "decisionId": "0b07b85b-22c0-4fa1-8ce2-4b62dc518065", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "620c8c68-bd29-4697-a25a-383f26887ba4", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "b0ab8b2e-b267-49a2-80ee-e35e0abf2cc5", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "44d4b6b3-de53-4c96-ab27-39435fb823bc", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "9b91fdf4-7687-4c66-b86a-d5edde5e5287", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "318cf576-aa00-4cba-be63-e62551b4693e", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "ad1b79b7-66e0-4d81-8aae-48f5aa071ad7", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "b56b0408-4f90-4895-84cd-238531a9c420", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "e88666ae-e697-4eae-b4a6-bbecda6fd288", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "3c13f70a-b391-4877-af34-590297c5fe1f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "origin": "computed", + "reason": "Membre du cercle Bureau", + "beforeSnapshot": true + }, + { + "id": "8979c4e5-faf0-4ac6-92ee-9aa31009d429", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Bureau", + "beforeSnapshot": true + }, + { + "id": "f7ebb8ba-da10-494e-a83b-856e60560908", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "origin": "computed", + "reason": "Membre du cercle Bureau", + "beforeSnapshot": true + }, + { + "id": "81df0889-01e4-41f9-bccc-165887634ee8", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "origin": "computed", + "reason": "Membre du cercle Bureau", + "beforeSnapshot": true + }, + { + "id": "6a48b4fc-6dd3-4435-9746-b3fab8963830", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-06T12:00:00.000Z", + "updatedAt": "2026-08-06T12:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "declared", + "reason": "S’est déclarée concernée après l’arrêt de la liste", + "beforeSnapshot": false, + "declaredNote": "Le budget vélo touche aussi les sorties du samedi que j’accompagne." + }, + { + "id": "b88c9d63-e209-40cd-8c95-5329700489d7", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "380cabb4-eced-4352-a224-0a4767649acb", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "da15ae05-1a69-4419-a6a8-73b1f2564cb6", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "090bf385-68fa-4237-bfb0-b9a93a7fb86d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "abc2e925-2844-4af7-ab0b-5f9e91d54e0d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "682466ae-03ff-4959-a90c-0235daf2e776", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "a419d006-623b-42ff-b7d4-795283fcb771", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "ff845fbb-43fa-43e5-bd9a-d80983077a9f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "318cf576-aa00-4cba-be63-e62551b4693e", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "3c804960-8741-42ac-aaa3-715c54c11977", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "1ae5fd3d-1f71-44fc-baca-d4ec2c7ca09f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "a4ae055f-94f4-4c76-8722-408b8a3d6ebf", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "700064ba-f9f7-4aca-a7bf-886e4dde18e9", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-07-26T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "personId": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "origin": "computed", + "reason": "Membre du cercle Tous", + "beforeSnapshot": true + }, + { + "id": "95334dd5-3b8c-475d-9424-7d6532243ac5", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-16T18:00:00.000Z", + "updatedAt": "2026-06-16T18:00:00.000Z", + "decisionId": "ed6dc706-29f9-4520-9131-b8f6a88efaea", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "14588892-33b9-45e0-96ae-8be9df872a44", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-16T18:00:00.000Z", + "updatedAt": "2026-06-16T18:00:00.000Z", + "decisionId": "ed6dc706-29f9-4520-9131-b8f6a88efaea", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "028e8fa0-1035-4d82-acdb-f7f622c349ae", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-16T18:00:00.000Z", + "updatedAt": "2026-06-16T18:00:00.000Z", + "decisionId": "ed6dc706-29f9-4520-9131-b8f6a88efaea", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "8f42f88d-5e8e-42d1-b1aa-80003e9150a3", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-16T18:00:00.000Z", + "updatedAt": "2026-06-16T18:00:00.000Z", + "decisionId": "ed6dc706-29f9-4520-9131-b8f6a88efaea", + "personId": "318cf576-aa00-4cba-be63-e62551b4693e", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "681b3c60-14ea-4844-b76b-cd0cb66b353e", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-16T18:00:00.000Z", + "updatedAt": "2026-06-16T18:00:00.000Z", + "decisionId": "ed6dc706-29f9-4520-9131-b8f6a88efaea", + "personId": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "c77c3c80-7065-4dcd-8dd2-506f017ca29f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-16T18:00:00.000Z", + "updatedAt": "2026-06-16T18:00:00.000Z", + "decisionId": "ed6dc706-29f9-4520-9131-b8f6a88efaea", + "personId": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "f1f3ad32-f95e-438c-bf2c-c3855f544097", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-16T18:00:00.000Z", + "updatedAt": "2026-06-16T18:00:00.000Z", + "decisionId": "ed6dc706-29f9-4520-9131-b8f6a88efaea", + "personId": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "origin": "computed", + "reason": "Membre du cercle Ateliers", + "beforeSnapshot": true + }, + { + "id": "06f121a7-dfa5-4562-a272-1cff787dcaeb", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T09:00:00.000Z", + "updatedAt": "2026-08-11T09:00:00.000Z", + "decisionId": "06c7e749-0fe2-41bf-ab59-3c3fadc5928d", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "56c16805-7cb4-49b9-8cf2-4bbc1f5689e5", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T09:00:00.000Z", + "updatedAt": "2026-08-11T09:00:00.000Z", + "decisionId": "06c7e749-0fe2-41bf-ab59-3c3fadc5928d", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "6d572839-6ce1-4e1d-b0e9-7d1f1f6d885b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T09:00:00.000Z", + "updatedAt": "2026-08-11T09:00:00.000Z", + "decisionId": "06c7e749-0fe2-41bf-ab59-3c3fadc5928d", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "1e18f870-3449-43e0-aaa5-5bcaa83e4b5f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T09:00:00.000Z", + "updatedAt": "2026-08-11T09:00:00.000Z", + "decisionId": "06c7e749-0fe2-41bf-ab59-3c3fadc5928d", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "5979d8e2-3698-4150-83cb-190278ebdf04", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T09:00:00.000Z", + "updatedAt": "2026-08-11T09:00:00.000Z", + "decisionId": "06c7e749-0fe2-41bf-ab59-3c3fadc5928d", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "653b45ac-a8eb-423b-94d5-a12f381db53a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:30:00.000Z", + "updatedAt": "2026-08-11T10:30:00.000Z", + "decisionId": "06c7e749-0fe2-41bf-ab59-3c3fadc5928d", + "personId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "origin": "declared", + "reason": "S’est déclarée concernée", + "beforeSnapshot": true, + "declaredNote": "Je passe devant la façade tous les matins — partante pour aider au chantier." + }, + { + "id": "19b995b5-b6c6-4fd3-84ca-437bee054390", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T10:00:00.000Z", + "updatedAt": "2026-08-09T10:00:00.000Z", + "decisionId": "3c9829f6-196e-4029-ac60-06039ceb121f", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "a62575e5-80ab-4d7d-9b60-c2c2100f0108", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T10:00:00.000Z", + "updatedAt": "2026-08-09T10:00:00.000Z", + "decisionId": "3c9829f6-196e-4029-ac60-06039ceb121f", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "e10ef725-af68-41f0-a4f8-b480edf1a7c5", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T10:00:00.000Z", + "updatedAt": "2026-08-09T10:00:00.000Z", + "decisionId": "3c9829f6-196e-4029-ac60-06039ceb121f", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "0608520e-f9ec-48cb-bed1-d5d7517ca4d9", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T10:00:00.000Z", + "updatedAt": "2026-08-09T10:00:00.000Z", + "decisionId": "3c9829f6-196e-4029-ac60-06039ceb121f", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "8307390f-087e-4fbe-8445-ec4f4602953d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T10:00:00.000Z", + "updatedAt": "2026-08-09T10:00:00.000Z", + "decisionId": "3c9829f6-196e-4029-ac60-06039ceb121f", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "cc7203e3-2b0c-472f-8eef-ce37fcd0cb01", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "88cced14-15b9-4291-9534-59f6fa141b92", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "19685591-c551-4bb0-b3fb-bafab3a3144e", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "88cced14-15b9-4291-9534-59f6fa141b92", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "85075aa2-a8fe-48aa-b057-64d6b45304da", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "88cced14-15b9-4291-9534-59f6fa141b92", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "aee175f9-7879-4704-bad7-f8ef8d340176", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "88cced14-15b9-4291-9534-59f6fa141b92", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "d9398f7e-efc3-40c1-a4ff-ddc881580421", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "88cced14-15b9-4291-9534-59f6fa141b92", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + }, + { + "id": "8efff575-dbfb-4bf6-a7b8-44e8531020a9", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "1e46a8f1-89f6-4018-b35c-b9c9e3f28551", + "personId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "origin": "computed", + "reason": "Membre du cercle Bureau", + "beforeSnapshot": true + }, + { + "id": "87c48c91-7888-4bd3-8d18-86cf8448a495", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "1e46a8f1-89f6-4018-b35c-b9c9e3f28551", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Bureau", + "beforeSnapshot": true + }, + { + "id": "7b61037b-7f8b-47ef-a9e0-3cf9db443fd2", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "1e46a8f1-89f6-4018-b35c-b9c9e3f28551", + "personId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "origin": "computed", + "reason": "Membre du cercle Bureau", + "beforeSnapshot": true + }, + { + "id": "aa64a7ad-123c-4478-839f-cd23cf9e87c0", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "1e46a8f1-89f6-4018-b35c-b9c9e3f28551", + "personId": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "origin": "computed", + "reason": "Membre du cercle Bureau", + "beforeSnapshot": true + }, + { + "id": "aa2f96f7-f039-401e-a75f-d287ec8e7fdf", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-29T09:00:00.000Z", + "updatedAt": "2026-07-29T09:00:00.000Z", + "decisionId": "00e9df0e-9975-4a4a-aa83-58fd3392c05c", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true, + "priority": 1 + }, + { + "id": "6ad661f8-4501-487b-ab20-89aeb6c3192b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-29T09:00:00.000Z", + "updatedAt": "2026-07-29T09:00:00.000Z", + "decisionId": "00e9df0e-9975-4a4a-aa83-58fd3392c05c", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true, + "priority": 2 + }, + { + "id": "913c5533-2e79-4279-ba28-f9c2cfee3acc", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-29T09:00:00.000Z", + "updatedAt": "2026-07-29T09:00:00.000Z", + "decisionId": "00e9df0e-9975-4a4a-aa83-58fd3392c05c", + "personId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true, + "priority": 3 + }, + { + "id": "c58acec1-9f9e-4a47-add7-9dff106262a1", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-29T09:00:00.000Z", + "updatedAt": "2026-07-29T09:00:00.000Z", + "decisionId": "00e9df0e-9975-4a4a-aa83-58fd3392c05c", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true, + "priority": 0 + }, + { + "id": "874cfea2-a603-4915-b2c8-4263a9489471", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-29T09:00:00.000Z", + "updatedAt": "2026-07-29T09:00:00.000Z", + "decisionId": "00e9df0e-9975-4a4a-aa83-58fd3392c05c", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "origin": "computed", + "reason": "Membre du cercle Le local du quai", + "beforeSnapshot": true + } + ], + "objections": [ + { + "id": "0d529a1e-1352-4036-a08d-eec479044f2c", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T14:00:00.000Z", + "updatedAt": "2026-08-10T14:00:00.000Z", + "decisionId": "3c9829f6-196e-4029-ac60-06039ceb121f", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "kind": "boundary", + "argument": "Les bénévoles du marché qui ouvrent le local le samedi ne sont pas dans le périmètre — il faut les compter avant de parler du fond.", + "status": "open" + }, + { + "id": "b5e3a095-0753-475d-a109-0784c9a8956b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-05-03T10:00:00.000Z", + "updatedAt": "2026-05-05T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "kind": "content", + "argument": "Le granulé nous lie à un seul fournisseur du coin — que fait-on s’il ferme ?", + "status": "withdrawn", + "resolutionNote": "Retirée après l’ajout d’un second fournisseur au dossier." + } + ], + "advices": [ + { + "id": "0282d102-4c3e-4f6b-b5ba-4129c54aa7ce", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T10:00:00.000Z", + "updatedAt": "2026-08-11T10:00:00.000Z", + "decisionId": "06c7e749-0fe2-41bf-ab59-3c3fadc5928d", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "position": "favorable", + "note": "Le jaune paille irait bien avec les volets — et j’ai un pistolet à peinture à prêter." + }, + { + "id": "8e920ded-3c91-4225-816b-d405d02e8285", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T11:15:00.000Z", + "updatedAt": "2026-08-11T11:15:00.000Z", + "decisionId": "06c7e749-0fe2-41bf-ab59-3c3fadc5928d", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "position": "reserved", + "note": "D’accord sur le principe, mais pas pendant les semaines de marché — l’échafaudage bloquerait le trottoir." + }, + { + "id": "159235f6-16e1-4735-be7f-9b2a1177371e", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-30T09:00:00.000Z", + "updatedAt": "2026-07-30T09:00:00.000Z", + "decisionId": "00e9df0e-9975-4a4a-aa83-58fd3392c05c", + "personId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "position": "favorable", + "note": "Prends l’électricien qui a refait la boutique de la rue Neuve — sérieux et pas cher." + } + ], + "assents": [ + { + "id": "16955505-5630-4cc4-a188-1af1b394eb70", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-11T11:00:00.000Z", + "updatedAt": "2026-08-11T11:00:00.000Z", + "decisionId": "88cced14-15b9-4291-9534-59f6fa141b92", + "personId": "62789d12-de5a-43bf-b7f4-111c642ec156" + }, + { + "id": "85483c5e-0ac0-4983-aede-d3c3195a29e5", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-05-02T10:00:00.000Z", + "updatedAt": "2026-05-02T10:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099" + }, + { + "id": "3e9c97c4-892e-4caf-8eb7-7e7ce2157bb0", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-05-04T16:00:00.000Z", + "updatedAt": "2026-05-04T16:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "personId": "4e1455b5-9697-48f9-b559-6edfe889b339" + }, + { + "id": "b20c904c-9c4a-4e3f-9c66-142925d95561", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T15:00:00.000Z", + "updatedAt": "2026-08-10T15:00:00.000Z", + "decisionId": "0b07b85b-22c0-4fa1-8ce2-4b62dc518065", + "personId": "f790d11d-a84f-4ed1-80c9-7077b870e099" + } + ], + "mandates": [ + { + "id": "a76dfe11-db3e-4356-b540-87109ee02199", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-29T19:00:00.000Z", + "updatedAt": "2026-05-03T09:00:00.000Z", + "title": "Trésorerie", + "holderId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "originDecisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "domain": { + "circleIds": [ + "9f399d53-6cca-4375-bee7-374ae9cdd53f" + ], + "tags": [ + "finances", + "tresorerie" + ] + }, + "startsAt": "2026-02-01T00:00:00.000Z", + "endsAt": "2027-01-31T23:59:00.000Z", + "electorCircleId": "d35d01d4-4570-4e75-9c64-f00ba5118624", + "nominationMethod": "election-no-candidate", + "reports": [ + { + "dueAt": "2026-05-01T12:00:00.000Z", + "deliveredAt": "2026-05-03T09:00:00.000Z", + "content": "Trésorerie au 30 avril : 3 180 € en caisse, cotisations rentrées aux deux tiers, assurance et loyer à jour. Le poêle à granulés est provisionné." + }, + { + "dueAt": "2026-08-01T12:00:00.000Z" + } + ], + "status": "active" + } + ], + "docs": [ + { + "id": "931d78d6-6931-4570-a7ae-058a344dc105", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-12T11:00:00.000Z", + "updatedAt": "2026-01-12T11:00:00.000Z", + "slug": "pacte-atelier-du-canal", + "title": "Notre Pacte", + "role": "pact", + "description": "Notre contrat social — sacralisé, jamais immuable." + }, + { + "id": "3a7b054a-ce09-42ca-ae94-a57d99230854", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-19T11:00:00.000Z", + "updatedAt": "2026-01-19T11:00:00.000Z", + "slug": "reglement-interieur", + "title": "Règlement intérieur", + "role": "reference", + "description": "Les règles de vie du local et des ateliers — huit clauses, amendables par décision." + } + ], + "clauses": [ + { + "id": "8d173627-9d2c-47f5-b972-9854626c416a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "Préambule", + "position": 1, + "code": "P1", + "title": "Autonomie, Équilibre, Liaison", + "inertia": "max", + "currentVersionId": "fb0b3662-73c6-4c4b-85ff-44621789c564" + }, + { + "id": "ba7b0679-61f4-404b-ade5-bc5ef73c387f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "A — Finalité", + "position": 2, + "code": "A1", + "title": "Notre finalité", + "inertia": "max", + "currentVersionId": "78b2e345-251b-4678-acba-c64b3603225f" + }, + { + "id": "9cc188ff-5006-43ac-9fc1-f232d66d4910", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 3, + "code": "B1", + "title": "Petit groupe", + "inertia": "standard", + "currentVersionId": "1001bb59-b377-4d3e-aa52-3133210ac8b1", + "settingKey": "triage.smallGroupMax" + }, + { + "id": "f26c1d66-6591-4d38-a4ad-1c0eea221a4d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 4, + "code": "B2", + "title": "Grand corps", + "inertia": "standard", + "currentVersionId": "2007ccdd-5f5b-463c-802e-cc0b34c6a7e4", + "settingKey": "triage.collectiveMin" + }, + { + "id": "20b30b90-b784-456e-a233-e3a4fa26b0a9", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 5, + "code": "B3", + "title": "Consentement direct", + "inertia": "standard", + "currentVersionId": "20e117ec-35ae-4af5-bcf1-eed3fd7f323f", + "settingKey": "triage.consentMax" + }, + { + "id": "44437252-8e01-4881-b375-1a6368a284cc", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 6, + "code": "B4", + "title": "Fenêtre d'objection", + "inertia": "standard", + "currentVersionId": "0e6703f2-e37c-44c7-b852-e48d9c25d0ca", + "settingKey": "triage.objectionWindowHours" + }, + { + "id": "1973ea50-a0d0-4610-ae74-5664f628cd67", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 7, + "code": "B5", + "title": "Fenêtre d'avis", + "inertia": "standard", + "currentVersionId": "79c079b1-c400-4eb0-a379-bee9a1645e7c", + "settingKey": "triage.adviceWindowHours" + }, + { + "id": "4d3114ef-d2ce-4dd7-b19f-ace2a39a3b60", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 8, + "code": "B6", + "title": "Temps de formulation", + "inertia": "standard", + "currentVersionId": "cf3e6996-20c0-49c9-8473-09e9bd56180e", + "settingKey": "triage.framingDays" + }, + { + "id": "356773bd-2cef-41b4-9905-b6a97f2fb197", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 9, + "code": "B7", + "title": "Affluence", + "inertia": "standard", + "currentVersionId": "8c093ca8-67cc-4d63-8814-6ff8cdb04de7", + "settingKey": "triage.concernEscalateRatio" + }, + { + "id": "c420e31e-f246-4577-b9db-3ab072d688a6", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 10, + "code": "B8", + "title": "Récurrence", + "inertia": "standard", + "currentVersionId": "d3306e55-d82a-49e2-896b-1d5bf932428c", + "settingKey": "triage.recurrenceThreshold" + }, + { + "id": "0451d643-65cf-4b0e-9c94-eca636adafa3", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 11, + "code": "B9", + "title": "L'épreuve du réel", + "inertia": "standard", + "currentVersionId": "c763c602-a22d-4fbe-8684-effff21c0161", + "settingKey": "triage.reviewDelayDays" + }, + { + "id": "1c0976d4-9c1b-4d53-8750-48d9fd175a86", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "B — Le chemin des décisions", + "position": 12, + "code": "B10", + "title": "La matière avant le vote", + "inertia": "high", + "currentVersionId": "e82588ba-8bb5-44cf-86e5-05ae2847771f", + "settingKey": "triage.requireEffects" + }, + { + "id": "8f4c903f-ca7d-4348-bef1-d7add8aa5214", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "C — Nos protocoles", + "position": 13, + "code": "C1", + "title": "Le consentement, socle commun", + "inertia": "high", + "currentVersionId": "c6deae06-a98c-4984-915c-dd582bb21e6b", + "settingKey": "protocols.consent" + }, + { + "id": "5342dea4-9789-47d2-9c25-bbafbc6679f3", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "C — Nos protocoles", + "position": 14, + "code": "C2", + "title": "Le vote nuancé", + "inertia": "high", + "currentVersionId": "f198c834-7bb8-4dc2-89fc-dca0e6283945", + "settingKey": "protocols.nuanced" + }, + { + "id": "88be8e43-3828-4919-84d4-8e1a63d9b3bc", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "C — Nos protocoles", + "position": 15, + "code": "C3", + "title": "Les grands corps décident en nuancé", + "inertia": "high", + "currentVersionId": "df82efd7-5437-4522-806b-251a2270a7db", + "settingKey": "protocols.large" + }, + { + "id": "a901241e-a583-4a3d-a04c-637497a6c7dd", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "C — Nos protocoles", + "position": 16, + "code": "C4", + "title": "Le réglage collectif", + "inertia": "high", + "currentVersionId": "b6f3df49-06a5-4087-b151-0898836c4a15", + "settingKey": "protocols.parametric" + }, + { + "id": "78327bb7-98e0-4ada-8de7-33fd6c323849", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "docId": "931d78d6-6931-4570-a7ae-058a344dc105", + "section": "C — Nos protocoles", + "position": 17, + "code": "C5", + "title": "L'élection sans candidat", + "inertia": "high", + "currentVersionId": "a5b998ac-e055-4f62-ab1b-fa7a387616e2", + "settingKey": "protocols.election" + }, + { + "id": "51ca3f57-d2fa-492c-9881-dbb78fa2360a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "docId": "3a7b054a-ce09-42ca-ae94-a57d99230854", + "section": "Vie du local", + "position": 1, + "code": "RI-1", + "title": "Ouverture du local", + "inertia": "low", + "currentVersionId": "2c1f1c62-215b-4d72-85b7-5d9df54a20c3" + }, + { + "id": "754c2984-4eca-4289-8fcd-aabb5152675b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "docId": "3a7b054a-ce09-42ca-ae94-a57d99230854", + "section": "Vie du local", + "position": 2, + "code": "RI-2", + "title": "Rangement partagé", + "inertia": "low", + "currentVersionId": "a80b60d6-d685-48a5-9cc7-286a4cb06a73" + }, + { + "id": "180b8be4-52ad-4ab9-bfa2-7dfc6ef094a1", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "docId": "3a7b054a-ce09-42ca-ae94-a57d99230854", + "section": "Matériel", + "position": 3, + "code": "RI-3", + "title": "Prêt du matériel", + "inertia": "standard", + "currentVersionId": "7aa04acc-bd70-44cb-a688-9892bed5b496" + }, + { + "id": "8606f567-ebf8-4e61-a015-287c14a673ea", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "docId": "3a7b054a-ce09-42ca-ae94-a57d99230854", + "section": "Vie commune", + "position": 4, + "code": "RI-4", + "title": "Accueil des nouveaux", + "inertia": "standard", + "currentVersionId": "2e0b6dc6-eb1e-4fe0-8be3-01119049f0a6" + }, + { + "id": "1cdb3683-a76c-473b-bac2-e8a763f89b74", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "docId": "3a7b054a-ce09-42ca-ae94-a57d99230854", + "section": "Adhésion", + "position": 5, + "code": "RI-5", + "title": "Cotisation annuelle", + "inertia": "high", + "currentVersionId": "6a7b3679-dbd8-4314-8943-6095a7aaa9a3" + }, + { + "id": "d5e4b8fc-cd9d-4efd-a2ef-492752da7f97", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "docId": "3a7b054a-ce09-42ca-ae94-a57d99230854", + "section": "Sécurité", + "position": 6, + "code": "RI-6", + "title": "Usage des machines", + "inertia": "high", + "currentVersionId": "7331eb61-898f-4fc0-8f0a-d8a8922c1ca2" + }, + { + "id": "0b798f89-0b9b-4285-b3ff-e3aa4eaf9787", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "docId": "3a7b054a-ce09-42ca-ae94-a57d99230854", + "section": "Adhésion", + "position": 7, + "code": "RI-7", + "title": "Non-lucrativité", + "inertia": "max", + "currentVersionId": "f4c82cc1-898d-4178-b1e3-a51d8b8af7c4" + }, + { + "id": "2d8c6e71-61ed-49d4-99ad-0cc7d5ee9d4c", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "docId": "3a7b054a-ce09-42ca-ae94-a57d99230854", + "section": "Vie commune", + "position": 8, + "code": "RI-8", + "title": "Respect et bienveillance", + "inertia": "max", + "currentVersionId": "676e1c17-b66c-401d-aa28-54d2e7a6ce69" + } + ], + "versions": [ + { + "id": "fb0b3662-73c6-4c4b-85ff-44621789c564", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "8d173627-9d2c-47f5-b972-9854626c416a", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Trois boussoles guident l'Atelier du Canal : l'autonomie de chacun·e dans ses gestes, l'équilibre entre les ateliers et les personnes, la liaison avec le quartier qui nous entoure.", + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "78b2e345-251b-4678-acba-c64b3603225f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "ba7b0679-61f4-404b-ade5-bc5ef73c387f", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "L'Atelier du Canal existe pour l'entraide de quartier : des ateliers partagés — bois, textile, vélo — où l'on apprend, répare et transmet ensemble, à prix libre et à portée de tous.", + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "1001bb59-b377-4d3e-aa52-3133210ac8b1", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "9cc188ff-5006-43ac-9fc1-f232d66d4910", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Jusqu'à quatre personnes concernées, on se parle et on décide ensemble, sans ouvrir de session.", + "settingValue": 4, + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "2007ccdd-5f5b-463c-802e-cc0b34c6a7e4", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "f26c1d66-6591-4d38-a4ad-1c0eea221a4d", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "À partir de cinquante personnes concernées, la décision passe par le protocole des grands corps.", + "settingValue": 50, + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "20e117ec-35ae-4af5-bcf1-eed3fd7f323f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "20b30b90-b784-456e-a233-e3a4fa26b0a9", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Jusqu'à sept personnes concernées, le consentement est notre chemin naturel.", + "settingValue": 7, + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "0e6703f2-e37c-44c7-b852-e48d9c25d0ca", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "44437252-8e01-4881-b375-1a6368a284cc", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Une journée pour dire « ça me va » ou objecter — nos décisions ne traînent pas, elles restent révisables.", + "settingValue": 24, + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "79c079b1-c400-4eb0-a379-bee9a1645e7c", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "1973ea50-a0d0-4610-ae74-5664f628cd67", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Deux jours pour recueillir les avis avant de décider en écoutant.", + "settingValue": 48, + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "cf3e6996-20c0-49c9-8473-09e9bd56180e", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "4d3114ef-d2ce-4dd7-b19f-ace2a39a3b60", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Quatorze jours pour s'instruire et formuler des contre-propositions quand la décision structure.", + "settingValue": 14, + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "8c093ca8-67cc-4d63-8814-6ff8cdb04de7", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "356773bd-2cef-41b4-9905-b6a97f2fb197", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Quand la moitié des concerné·es de second lieu se déclarent, le périmètre doit être traité : élargir, ou motiver publiquement son maintien.", + "settingValue": 0.5, + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "d3306e55-d82a-49e2-896b-1d5bf932428c", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "c420e31e-f246-4577-b9db-3ab072d688a6", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Trois décisions semblables en trois mois : le Fil propose un mandat ou une règle.", + "settingValue": 3, + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "c763c602-a22d-4fbe-8684-effff21c0161", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "0451d643-65cf-4b0e-9c94-eca636adafa3", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Quatre-vingt-dix jours après une décision structurante : le réel a-t-il suivi ?", + "settingValue": 90, + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "e82588ba-8bb5-44cf-86e5-05ae2847771f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "1c0976d4-9c1b-4d53-8750-48d9fd175a86", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Toute décision engageante ou structurante s'ouvre avec au moins un effet recherché — on ne vote pas sur du vide.", + "settingValue": "binding", + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "c6deae06-a98c-4984-915c-dd582bb21e6b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "8f4c903f-ca7d-4348-bef1-d7add8aa5214", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Le consentement est notre protocole de base : une proposition passe quand plus personne ne s’y oppose.", + "settingValue": "0a23358c-94bd-40b1-af99-96800e946081", + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "f198c834-7bb8-4dc2-89fc-dca0e6283945", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "5342dea4-9789-47d2-9c25-bbafbc6679f3", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Quand le consentement ne suffit plus, six nuances valent mieux que deux camps.", + "settingValue": "ab3c947a-76ff-438e-87dd-24c6d053afac", + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "df82efd7-5437-4522-806b-251a2270a7db", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "88be8e43-3828-4919-84d4-8e1a63d9b3bc", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Même nombreux·ses, nous refusons le pour/contre : le grand corps décide en nuancé — jamais deux camps, jamais un perdant.", + "settingValue": "ab3c947a-76ff-438e-87dd-24c6d053afac", + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "b6f3df49-06a5-4087-b151-0898836c4a15", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "a901241e-a583-4a3d-a04c-637497a6c7dd", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Quand la question est un nombre, un pourcentage ou un montant, on décide au curseur : chacun·e règle, la médiane éclaire, le garant cristallise.", + "settingValue": "ddfabf00-4b0d-4ee0-98b2-bb51b6845c17", + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "a5b998ac-e055-4f62-ab1b-fa7a387616e2", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T18:00:00.000Z", + "updatedAt": "2026-01-15T18:00:00.000Z", + "clauseId": "78327bb7-98e0-4ada-8de7-33fd6c323849", + "decisionId": "f25c44df-59e1-48ef-bffb-d06d058632b1", + "versionLabel": "v1", + "content": "Les responsabilités se confient par désignation : sans candidature, avec vote blanc, et le départage par les personnes — jamais par l'outil.", + "settingValue": "d9af660a-5fea-4984-9ada-a3df18220783", + "status": "current", + "adoptedAt": "2026-01-15T18:00:00.000Z" + }, + { + "id": "2c1f1c62-215b-4d72-85b7-5d9df54a20c3", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "clauseId": "51ca3f57-d2fa-492c-9881-dbb78fa2360a", + "decisionId": "2822b935-48db-483d-851b-eb38b7d8659d", + "versionLabel": "v1", + "content": "Le local ouvre les mercredis et samedis après-midi ; toute personne détentrice d’une clé peut ouvrir en plus, en le notant sur le tableau.", + "status": "current", + "adoptedAt": "2026-01-22T18:30:00.000Z" + }, + { + "id": "a80b60d6-d685-48a5-9cc7-286a4cb06a73", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "clauseId": "754c2984-4eca-4289-8fcd-aabb5152675b", + "decisionId": "2822b935-48db-483d-851b-eb38b7d8659d", + "versionLabel": "v1", + "content": "Chaque atelier laisse l’établi propre et les outils à leur place — la personne suivante commence sans chercher.", + "status": "current", + "adoptedAt": "2026-01-22T18:30:00.000Z" + }, + { + "id": "7aa04acc-bd70-44cb-a688-9892bed5b496", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "clauseId": "180b8be4-52ad-4ab9-bfa2-7dfc6ef094a1", + "decisionId": "2822b935-48db-483d-851b-eb38b7d8659d", + "versionLabel": "v1", + "content": "Les outils s’empruntent une semaine, inscrits au cahier des prêts ; les machines fixes restent au local.", + "status": "current", + "adoptedAt": "2026-01-22T18:30:00.000Z" + }, + { + "id": "2e0b6dc6-eb1e-4fe0-8be3-01119049f0a6", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "clauseId": "8606f567-ebf8-4e61-a015-287c14a673ea", + "decisionId": "2822b935-48db-483d-851b-eb38b7d8659d", + "versionLabel": "v1", + "content": "Toute nouvelle personne est accueillie lors d’un samedi d’ouverture et repart avec le petit guide du local.", + "status": "current", + "adoptedAt": "2026-01-22T18:30:00.000Z" + }, + { + "id": "6a7b3679-dbd8-4314-8943-6095a7aaa9a3", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "clauseId": "1cdb3683-a76c-473b-bac2-e8a763f89b74", + "decisionId": "2822b935-48db-483d-851b-eb38b7d8659d", + "versionLabel": "v1", + "content": "La cotisation annuelle est affichée à l’entrée ; un tarif solidaire est proposé sans justificatif.", + "status": "current", + "adoptedAt": "2026-01-22T18:30:00.000Z" + }, + { + "id": "7331eb61-898f-4fc0-8f0a-d8a8922c1ca2", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "clauseId": "d5e4b8fc-cd9d-4efd-a2ef-492752da7f97", + "decisionId": "2822b935-48db-483d-851b-eb38b7d8659d", + "versionLabel": "v1", + "content": "Les machines dangereuses — scie, dégauchisseuse — s’utilisent à deux, jamais seul·e dans le local.", + "status": "current", + "adoptedAt": "2026-01-22T18:30:00.000Z" + }, + { + "id": "f4c82cc1-898d-4178-b1e3-a51d8b8af7c4", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "clauseId": "0b798f89-0b9b-4285-b3ff-e3aa4eaf9787", + "decisionId": "2822b935-48db-483d-851b-eb38b7d8659d", + "versionLabel": "v1", + "content": "L’Atelier ne vend rien : les participations sont libres et financent le local, jamais des personnes.", + "status": "current", + "adoptedAt": "2026-01-22T18:30:00.000Z" + }, + { + "id": "676e1c17-b66c-401d-aa28-54d2e7a6ce69", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T18:30:00.000Z", + "updatedAt": "2026-01-22T18:30:00.000Z", + "clauseId": "2d8c6e71-61ed-49d4-99ad-0cc7d5ee9d4c", + "decisionId": "2822b935-48db-483d-851b-eb38b7d8659d", + "versionLabel": "v1", + "content": "On se parle avec respect, on répare avant de remplacer, on transmet ce qu’on sait.", + "status": "current", + "adoptedAt": "2026-01-22T18:30:00.000Z" + }, + { + "id": "ebf55919-e76f-48c5-ab6d-261678c92efe", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-07T10:00:00.000Z", + "updatedAt": "2026-08-07T10:00:00.000Z", + "clauseId": "8606f567-ebf8-4e61-a015-287c14a673ea", + "decisionId": "6bae3695-3fc5-4f70-8fbe-3b8a8076b416", + "versionLabel": "v2-a — binôme tournant", + "content": "Toute nouvelle personne est accueillie par un binôme tournant du cercle Ateliers, qui la suit sur ses trois premières visites et lui remet le petit guide du local.", + "status": "proposed" + }, + { + "id": "0b3acf19-291d-47f3-be5f-29fbfbb4cd3a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-08T14:30:00.000Z", + "updatedAt": "2026-08-08T14:30:00.000Z", + "clauseId": "8606f567-ebf8-4e61-a015-287c14a673ea", + "decisionId": "6bae3695-3fc5-4f70-8fbe-3b8a8076b416", + "versionLabel": "v2-b — permanence mensuelle", + "content": "Une permanence d’accueil se tient le premier samedi du mois : visite du local, présentation des ateliers, remise du petit guide et parrainage proposé.", + "status": "proposed" + } + ], + "protocols": [ + { + "id": "0a23358c-94bd-40b1-af99-96800e946081", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T10:00:00.000Z", + "updatedAt": "2026-01-15T10:00:00.000Z", + "name": "Consentement", + "method": "consent", + "description": "Zéro objection maintenue : la proposition passe quand plus personne ne s'y oppose — notre socle, partout.", + "durationDays": 7, + "ballot": "open", + "formula": { + "majorityPct": 100, + "baseExponent": 0.1, + "gradientExponent": 0, + "constantBase": 0 + }, + "modeParams": "D7M100B.1G0", + "pactClauseId": "8f4c903f-ca7d-4348-bef1-d7add8aa5214" + }, + { + "id": "ab3c947a-76ff-438e-87dd-24c6d053afac", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T10:05:00.000Z", + "updatedAt": "2026-01-15T10:05:00.000Z", + "name": "Vote nuancé", + "method": "nuanced", + "description": "Six nuances plutôt que deux camps : la distribution éclaire, le seuil constate.", + "durationDays": 14, + "ballot": "open", + "formula": { + "majorityPct": 50, + "baseExponent": 0.1, + "gradientExponent": 0.2, + "constantBase": 0, + "nuancedMinParticipants": 4, + "nuancedThresholdPct": 60 + }, + "modeParams": "D14M50B.1G.2", + "pactClauseId": "5342dea4-9789-47d2-9c25-bbafbc6679f3" + }, + { + "id": "ddfabf00-4b0d-4ee0-98b2-bb51b6845c17", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T10:10:00.000Z", + "updatedAt": "2026-01-15T10:10:00.000Z", + "name": "Réglage collectif", + "method": "parametric", + "description": "Décider au curseur : chacun·e règle, la médiane basse éclaire, le garant cristallise.", + "durationDays": 14, + "ballot": "open", + "formula": { + "majorityPct": 50, + "baseExponent": 0.1, + "gradientExponent": 0.2, + "constantBase": 0, + "parametricMinParticipants": 4 + }, + "modeParams": "D14M50B.1G.2", + "pactClauseId": "a901241e-a583-4a3d-a04c-637497a6c7dd" + }, + { + "id": "d9af660a-5fea-4984-9ada-a3df18220783", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T10:15:00.000Z", + "updatedAt": "2026-01-15T10:15:00.000Z", + "name": "Élection sans candidat", + "method": "election", + "description": "Sans candidature : chacun·e désigne, le blanc compte pour la participation ; en cas d'égalité, vous départagez — jamais l'outil.", + "durationDays": 14, + "ballot": "open", + "formula": { + "majorityPct": 50, + "baseExponent": 0.1, + "gradientExponent": 0.2, + "constantBase": 0, + "electionMinParticipants": 5, + "tieBreak": "runoff" + }, + "modeParams": "D14M50B.1G.2", + "pactClauseId": "78327bb7-98e0-4ada-8de7-33fd6c323849" + } + ], + "sessions": [ + { + "id": "8fe7ed8f-9b19-451b-b4ab-b5c5852e7f6f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-15T19:00:00.000Z", + "updatedAt": "2026-01-29T19:00:00.000Z", + "decisionId": "bb83d0b7-8eac-4a51-97fc-e6e087bd2d80", + "protocolId": "d9af660a-5fea-4984-9ada-a3df18220783", + "corpusPersonIds": [ + "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "318cf576-aa00-4cba-be63-e62551b4693e", + "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "4e1455b5-9697-48f9-b559-6edfe889b339", + "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "cd7b8066-88ab-47af-9d53-acb467a5ea44" + ], + "corpusSize": 12, + "opensAt": "2026-01-15T19:00:00.000Z", + "closesAt": "2026-01-29T19:00:00.000Z", + "status": "closed", + "outcome": "adopted" + }, + { + "id": "09bd70f1-61cf-4ccb-a69e-9b4ec230457b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "9a7bc593-654a-415c-b3d1-e7fdc7a510a5", + "protocolId": "ab3c947a-76ff-438e-87dd-24c6d053afac", + "corpusPersonIds": [ + "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "318cf576-aa00-4cba-be63-e62551b4693e", + "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "4e1455b5-9697-48f9-b559-6edfe889b339", + "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "cd7b8066-88ab-47af-9d53-acb467a5ea44" + ], + "corpusSize": 12, + "opensAt": "2026-08-09T09:00:00.000Z", + "closesAt": "2026-08-23T09:00:00.000Z", + "status": "open" + }, + { + "id": "bd6039d7-ae78-4119-80b4-3da7535c1127", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-04-30T09:00:00.000Z", + "updatedAt": "2026-05-07T09:00:00.000Z", + "decisionId": "c172cc05-571a-4d75-84b3-5bb602582534", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "corpusPersonIds": [ + "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "318cf576-aa00-4cba-be63-e62551b4693e", + "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "4e1455b5-9697-48f9-b559-6edfe889b339", + "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "cd7b8066-88ab-47af-9d53-acb467a5ea44" + ], + "corpusSize": 12, + "opensAt": "2026-04-30T09:00:00.000Z", + "closesAt": "2026-05-07T09:00:00.000Z", + "status": "closed", + "outcome": "adopted" + }, + { + "id": "9e8a7a6b-27d1-4f38-98f1-6cd471961ba6", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T09:00:00.000Z", + "updatedAt": "2026-08-10T09:00:00.000Z", + "decisionId": "0b07b85b-22c0-4fa1-8ce2-4b62dc518065", + "protocolId": "0a23358c-94bd-40b1-af99-96800e946081", + "corpusPersonIds": [ + "4e1455b5-9697-48f9-b559-6edfe889b339", + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "dfe07fca-dc13-4ebc-9e11-0dced155e13f" + ], + "corpusSize": 5, + "opensAt": "2026-08-10T09:00:00.000Z", + "closesAt": "2026-08-17T09:00:00.000Z", + "status": "open" + }, + { + "id": "3acb94fb-4c4a-4c1c-9320-e714c8aee013", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T09:00:00.000Z", + "updatedAt": "2026-08-04T09:00:00.000Z", + "decisionId": "aea1c674-665a-416e-b1dc-fc015a5fe1eb", + "protocolId": "ddfabf00-4b0d-4ee0-98b2-bb51b6845c17", + "corpusPersonIds": [ + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "318cf576-aa00-4cba-be63-e62551b4693e", + "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "d0a27b00-af87-4715-9bde-20cdbe2f29c5" + ], + "corpusSize": 11, + "opensAt": "2026-08-04T09:00:00.000Z", + "closesAt": "2026-08-18T09:00:00.000Z", + "status": "open" + }, + { + "id": "6947b008-eeec-431a-8c8d-3a41067f5da0", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-26T09:00:00.000Z", + "updatedAt": "2026-08-09T09:00:00.000Z", + "decisionId": "38e46122-8147-4503-9105-f90a07055456", + "protocolId": "ddfabf00-4b0d-4ee0-98b2-bb51b6845c17", + "corpusPersonIds": [ + "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "318cf576-aa00-4cba-be63-e62551b4693e", + "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "4e1455b5-9697-48f9-b559-6edfe889b339", + "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "cd7b8066-88ab-47af-9d53-acb467a5ea44" + ], + "corpusSize": 12, + "opensAt": "2026-07-26T09:00:00.000Z", + "closesAt": "2026-08-09T09:00:00.000Z", + "status": "frozen" + }, + { + "id": "9be75f32-09ed-4120-be43-1f30186828c0", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-16T18:00:00.000Z", + "updatedAt": "2026-06-30T18:00:00.000Z", + "decisionId": "ed6dc706-29f9-4520-9131-b8f6a88efaea", + "protocolId": "ab3c947a-76ff-438e-87dd-24c6d053afac", + "corpusPersonIds": [ + "62789d12-de5a-43bf-b7f4-111c642ec156", + "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "f790d11d-a84f-4ed1-80c9-7077b870e099", + "318cf576-aa00-4cba-be63-e62551b4693e", + "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "cd7b8066-88ab-47af-9d53-acb467a5ea44" + ], + "corpusSize": 7, + "opensAt": "2026-06-16T18:00:00.000Z", + "closesAt": "2026-06-30T18:00:00.000Z", + "status": "closed", + "outcome": "rejected" + } + ], + "votes": [ + { + "id": "573fd12d-878a-4395-b691-339868ad3cfb", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-20T10:00:00.000Z", + "updatedAt": "2026-01-20T10:00:00.000Z", + "sessionId": "8fe7ed8f-9b19-451b-b4ab-b5c5852e7f6f", + "voterId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "choicePersonId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f" + }, + { + "id": "1f0c3ca0-2732-4d1d-be7c-9a2898f51d4e", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-20T12:00:00.000Z", + "updatedAt": "2026-01-20T12:00:00.000Z", + "sessionId": "8fe7ed8f-9b19-451b-b4ab-b5c5852e7f6f", + "voterId": "343a60a7-add5-46b1-b6d6-9093ed2ad559", + "choicePersonId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f" + }, + { + "id": "a6384179-acc9-4410-beae-6c24d6c4bdce", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-21T09:00:00.000Z", + "updatedAt": "2026-01-21T09:00:00.000Z", + "sessionId": "8fe7ed8f-9b19-451b-b4ab-b5c5852e7f6f", + "voterId": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "choicePersonId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f" + }, + { + "id": "816b28ad-b44d-4ad7-b5a8-ba8db9f1aaec", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-22T17:00:00.000Z", + "updatedAt": "2026-01-22T17:00:00.000Z", + "sessionId": "8fe7ed8f-9b19-451b-b4ab-b5c5852e7f6f", + "voterId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "choicePersonId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f" + }, + { + "id": "6f98d412-5fcd-4b89-b485-aa4fcd9f6c9a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-23T11:00:00.000Z", + "updatedAt": "2026-01-23T11:00:00.000Z", + "sessionId": "8fe7ed8f-9b19-451b-b4ab-b5c5852e7f6f", + "voterId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "choicePersonId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "comment": "Marcel tient déjà les comptes du club de pétanque — les chiffres ne lui font pas peur." + }, + { + "id": "12384f05-28fc-48cb-90cc-0f429535cd19", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-24T15:00:00.000Z", + "updatedAt": "2026-01-24T15:00:00.000Z", + "sessionId": "8fe7ed8f-9b19-451b-b4ab-b5c5852e7f6f", + "voterId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "choicePersonId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a" + }, + { + "id": "9222144b-1bea-4b24-a84a-cebaf9350540", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-01-25T10:00:00.000Z", + "updatedAt": "2026-01-25T10:00:00.000Z", + "sessionId": "8fe7ed8f-9b19-451b-b4ab-b5c5852e7f6f", + "voterId": "4e1455b5-9697-48f9-b559-6edfe889b339" + }, + { + "id": "d91210a4-f178-4ee3-b1f3-7fa659c4a575", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T14:00:00.000Z", + "updatedAt": "2026-08-09T14:00:00.000Z", + "sessionId": "09bd70f1-61cf-4ccb-a69e-9b4ec230457b", + "voterId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "value": 5 + }, + { + "id": "2db58e4d-9efe-4902-8de7-00ee2b9b2907", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-09T19:00:00.000Z", + "updatedAt": "2026-08-09T19:00:00.000Z", + "sessionId": "09bd70f1-61cf-4ccb-a69e-9b4ec230457b", + "voterId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "value": 4 + }, + { + "id": "f16436bc-8a2e-4d0e-bf6f-57a02e2eb027", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T09:30:00.000Z", + "updatedAt": "2026-08-10T09:30:00.000Z", + "sessionId": "09bd70f1-61cf-4ccb-a69e-9b4ec230457b", + "voterId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "value": 3 + }, + { + "id": "db3fdf11-02c5-433c-bf68-7f7be172888f", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T11:00:00.000Z", + "updatedAt": "2026-08-10T11:00:00.000Z", + "sessionId": "09bd70f1-61cf-4ccb-a69e-9b4ec230457b", + "voterId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "value": 1, + "comment": "Encore une cotisation — je préférerais un échange ponctuel d’outillage pour commencer." + }, + { + "id": "f5c3b03e-f37b-447d-bae5-09b585fb90c2", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-10T16:00:00.000Z", + "updatedAt": "2026-08-10T16:00:00.000Z", + "sessionId": "09bd70f1-61cf-4ccb-a69e-9b4ec230457b", + "voterId": "4e1455b5-9697-48f9-b559-6edfe889b339", + "value": 0, + "comment": "Je crains qu’on y perde notre autonomie de quartier — tout se décidera à Paris." + }, + { + "id": "0bb69b20-7a8b-45f8-9740-d9b2c163be76", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T14:00:00.000Z", + "updatedAt": "2026-08-04T14:00:00.000Z", + "sessionId": "3acb94fb-4c4a-4c1c-9320-e714c8aee013", + "voterId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "values": [ + 40, + 30, + 20 + ] + }, + { + "id": "baa8527f-2899-40d1-bea5-7c46f463171d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-04T18:00:00.000Z", + "updatedAt": "2026-08-04T18:00:00.000Z", + "sessionId": "3acb94fb-4c4a-4c1c-9320-e714c8aee013", + "voterId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "values": [ + 35, + 35, + 20 + ] + }, + { + "id": "f62a0705-3530-4646-951c-79606f6baad7", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-05T09:00:00.000Z", + "updatedAt": "2026-08-05T09:00:00.000Z", + "sessionId": "3acb94fb-4c4a-4c1c-9320-e714c8aee013", + "voterId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "values": [ + 45, + 25, + 15 + ] + }, + { + "id": "c1833e5f-bd27-4256-851f-f4480310db5d", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-05T19:00:00.000Z", + "updatedAt": "2026-08-05T19:00:00.000Z", + "sessionId": "3acb94fb-4c4a-4c1c-9320-e714c8aee013", + "voterId": "dfe07fca-dc13-4ebc-9e11-0dced155e13f", + "values": [ + 30, + 30, + 25 + ] + }, + { + "id": "8c0ef38b-d8c6-4b1b-bb82-d16f7ac06a7a", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-06T10:00:00.000Z", + "updatedAt": "2026-08-06T10:00:00.000Z", + "sessionId": "3acb94fb-4c4a-4c1c-9320-e714c8aee013", + "voterId": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "values": [ + 40, + 20, + 25 + ] + }, + { + "id": "b28a709c-1350-4804-ae38-5d296dadfcff", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-07T17:00:00.000Z", + "updatedAt": "2026-08-07T17:00:00.000Z", + "sessionId": "3acb94fb-4c4a-4c1c-9320-e714c8aee013", + "voterId": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "values": [ + 50, + 20, + 10 + ], + "comment": "Le vélo tourne au ralenti depuis mars — je remets sur le bois." + }, + { + "id": "8df4b39c-84e2-430d-a08e-0a3a2da03c04", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-27T10:00:00.000Z", + "updatedAt": "2026-07-27T10:00:00.000Z", + "sessionId": "6947b008-eeec-431a-8c8d-3a41067f5da0", + "voterId": "3c5e8e6c-c66d-4796-81bd-937ce602ba1a", + "values": [ + 30, + 12 + ] + }, + { + "id": "3db61605-d927-41cb-aa06-b4b04b13b4db", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-07-29T18:00:00.000Z", + "updatedAt": "2026-07-29T18:00:00.000Z", + "sessionId": "6947b008-eeec-431a-8c8d-3a41067f5da0", + "voterId": "d0a27b00-af87-4715-9bde-20cdbe2f29c5", + "values": [ + 25, + 10 + ] + }, + { + "id": "18440391-58c8-4588-8fd5-ffc624a82abc", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-02T15:00:00.000Z", + "updatedAt": "2026-08-02T15:00:00.000Z", + "sessionId": "6947b008-eeec-431a-8c8d-3a41067f5da0", + "voterId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "values": [ + 30, + 15 + ] + }, + { + "id": "f7e96701-cfcf-4483-8e8a-a6a12e8d6129", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-08-07T20:00:00.000Z", + "updatedAt": "2026-08-07T20:00:00.000Z", + "sessionId": "6947b008-eeec-431a-8c8d-3a41067f5da0", + "voterId": "cd7b8066-88ab-47af-9d53-acb467a5ea44", + "values": [ + 35, + 15 + ] + }, + { + "id": "57f7989d-8429-4995-bf20-cbac9f3b2d1b", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-18T10:00:00.000Z", + "updatedAt": "2026-06-18T10:00:00.000Z", + "sessionId": "9be75f32-09ed-4120-be43-1f30186828c0", + "voterId": "f790d11d-a84f-4ed1-80c9-7077b870e099", + "value": 1, + "comment": "Le budget partirait dans une machine que deux personnes savent faire tourner." + }, + { + "id": "b14410ef-f645-4018-a721-5dc627539fbd", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-19T14:00:00.000Z", + "updatedAt": "2026-06-19T14:00:00.000Z", + "sessionId": "9be75f32-09ed-4120-be43-1f30186828c0", + "voterId": "7589bff0-c5bd-4052-9144-bed92b0e7a3a", + "value": 2 + }, + { + "id": "47c7e26c-3ad2-4154-a038-51977d993701", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-20T09:00:00.000Z", + "updatedAt": "2026-06-20T09:00:00.000Z", + "sessionId": "9be75f32-09ed-4120-be43-1f30186828c0", + "voterId": "d0377c93-f2b0-4265-b5f4-4c34e0d85596", + "value": 1, + "comment": "Je préfère qu’on répare nos machines à coudre d’abord." + }, + { + "id": "f20a33fc-d9b2-4db5-a954-6043fb4480d8", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-21T18:00:00.000Z", + "updatedAt": "2026-06-21T18:00:00.000Z", + "sessionId": "9be75f32-09ed-4120-be43-1f30186828c0", + "voterId": "318cf576-aa00-4cba-be63-e62551b4693e", + "value": 4 + }, + { + "id": "89c4d718-0223-42cd-9d08-21874941a922", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-24T11:00:00.000Z", + "updatedAt": "2026-06-24T11:00:00.000Z", + "sessionId": "9be75f32-09ed-4120-be43-1f30186828c0", + "voterId": "62789d12-de5a-43bf-b7f4-111c642ec156", + "value": 0, + "comment": "On croule déjà sous les objets à entretenir — pas un de plus." + }, + { + "id": "28e542eb-78ef-4824-b82c-bb7b246f07bb", + "collectiveId": "c5ccb2dd-6d99-43b9-bc1a-f6c37cc67d94", + "createdAt": "2026-06-27T16:00:00.000Z", + "updatedAt": "2026-06-27T16:00:00.000Z", + "sessionId": "9be75f32-09ed-4120-be43-1f30186828c0", + "voterId": "781e0bb0-8006-4fe9-9501-f9c4533b216e", + "value": 3 + } + ] +} diff --git a/frontend/app/engine/impact.ts b/frontend/app/engine/impact.ts new file mode 100644 index 0000000..70698dc --- /dev/null +++ b/frontend/app/engine/impact.ts @@ -0,0 +1,101 @@ +/** + * Perimeter engine — who is concerned by THIS decision. + * + * Pure functions — no store, no I/O. The store assembles the inputs and + * persists the result as Concern rows (origin 'computed'). + * + * The computed union (BLUEPRINT-V2.md « Périmètres et inclusion ») : + * members of the scoped circles ∪ persons named by the author + * ∪ holders of active mandates whose domain intersects the scoped circles. + * Every inclusion carries its human-readable French reason — auditable in + * one tap, never « la machine t'a désigné ». + * + * MEMBERSHIP CHOICE (v2, documented): circle membership is NOMINATIVE per + * circle (Circle.memberIds is the ONLY membership rule). Circle nesting + * (parentCircleId, « couches d'oignon ») does NOT propagate membership: + * a parent circle does NOT automatically include the members of its child + * circles, nor the reverse. Nesting serves ONE gesture — widening the + * perimeter by one notch (« élargir d'un cran ») — which explicitly adds + * the parent circle to the scope; the computation never infers it. + */ + +import type { Circle, Decision, Id, Mandate } from '~/types/domain' + +export interface ConcernedEntry { + personId: Id + /** French reason shown on tap (becomes Concern.reason). */ + reason: string +} + +/** + * Flatten circle ids into the set of their DIRECT nominative members. + * + * NON-PROPAGATION (see module header): only Circle.memberIds of the listed + * circles count. Members of child (or parent) circles are NOT included — + * widening is a scope gesture, never an automatic inference. + * Unknown circle ids are ignored (robustness over crash). + */ +export function expandCircleMembers(circleIds: Id[], circles: Circle[]): Set { + const members = new Set() + for (const circleId of circleIds) { + const circle = circles.find(c => c.id === circleId) + if (!circle) continue + for (const personId of circle.memberIds) members.add(personId) + } + return members +} + +/** + * Compute the concerned persons of a decision scope, each with its reason. + * + * Deduplication: first reason wins, with source priority + * circle membership > named by author > mandate holder. + * The author is EXCLUDED from the list (they decide, they are not + * « concerned » by their own perimeter). + * + * Mandates count only when status 'active' and when their domain.circleIds + * INTERSECTS the scoped circles (any shared circle is enough — a mandate + * holder is concerned as soon as the decision touches their domain). + */ +export function computeConcerned( + scope: Decision['scope'], + circles: Circle[], + mandates: Mandate[], + authorId: Id, +): ConcernedEntry[] { + // Map preserves insertion order; first reason wins (priority by pass order). + const concerned = new Map() + + // 1. Members of the scoped circles (highest priority reason). + for (const circleId of scope.circleIds) { + const circle = circles.find(c => c.id === circleId) + if (!circle) continue + for (const personId of circle.memberIds) { + if (personId === authorId) continue + if (!concerned.has(personId)) { + concerned.set(personId, `membre du cercle ${circle.name}`) + } + } + } + + // 2. Persons named by the author. + for (const personId of scope.personIds) { + if (personId === authorId) continue + if (!concerned.has(personId)) { + concerned.set(personId, 'nommé·e par l\'auteur') + } + } + + // 3. Holders of active mandates whose domain intersects the scoped circles. + const scopedCircleIds = new Set(scope.circleIds) + for (const mandate of mandates) { + if (mandate.status !== 'active') continue + if (!mandate.domain.circleIds.some(id => scopedCircleIds.has(id))) continue + if (mandate.holderId === authorId) continue + if (!concerned.has(mandate.holderId)) { + concerned.set(mandate.holderId, `titulaire du mandat ${mandate.title}`) + } + } + + return [...concerned].map(([personId, reason]) => ({ personId, reason })) +} diff --git a/frontend/app/engine/parametric.ts b/frontend/app/engine/parametric.ts new file mode 100644 index 0000000..4260a25 --- /dev/null +++ b/frontend/app/engine/parametric.ts @@ -0,0 +1,416 @@ +/** + * Parametric decision engine — « Réglage collectif » (collective tuning). + * + * Pure functions, no I/O — the single implementation (BLUEPRINT-V2.md Δ2, Δ3, + * Δ15, Δ16). Everything here is math over ParamSpec + vote vectors; the human + * gesture (crystallization) lives in state.ts/UI, never here. + * + * LOCKED SPECS (blueprint repairs): + * - LOW median, element by element: for an even vote count, take the LOWER + * central element (index floor((n-1)/2) after ascending sort). Invariant: + * every median value is a value someone actually voted, so the step is + * honored BY CONSTRUCTION — « une position que chacun aurait pu proposer ». + * - constraint 'sum100' requires EXACTLY ONE 'share' param with derived:true + * (the absorption variable), resolved linearly: 100 − Σ other shares. + * 'slider' params live outside the constraint and pass through untouched. + * - At vote time the resolved derived must stay within its bounds, otherwise + * the vote is rejected (validateVote). + * - At crystallization the derived is never aggregated: it is resolved from + * the median of the voted shares; if it exits its bounds ⇒ clamp to the + * violated bound + PROPORTIONAL renormalization of the non-derived shares + * (each multiplied by (100 − clampedDerived) / Σ median shares) so the + * sum-100 invariant is restored. The renormalized shares may leave the + * step grid — accepted and documented: this is the one specified exception. + * - Degenerate cases: 0 votes ⇒ baseline vector, never an empty screen. + * - computeMyImpact 'linear-share' NEVER invents a number: missing attribute + * or empty declaring corpus ⇒ null. + * - detectBimodality is a simple documented heuristic, NEVER blocking: + * it returns false on any degenerate input instead of throwing. + * + * Code and comments in English; thrown error messages in French (UI-facing). + */ + +import type { ParamDef, ParamSpec } from '../types/domain' + +/** Blueprint limit: a small manipulable space (SejeteralO lesson). */ +const MAX_PARAMS = 7 + +/** + * Absolute tolerance for floating-point comparisons (bounds and step grid). + * Vote values are human-scale (percent shares, bounded sliders), so an + * absolute epsilon is safe: 0.1 + 0.2 must be accepted as a 0.3 step value. + */ +const FLOAT_EPS = 1e-6 + +/** Params that are actually voted, in spec order (derived excluded). */ +function votableParams(spec: ParamSpec): ParamDef[] { + return spec.params.filter(p => p.derived !== true) +} + +/** + * Guarded indexed access (project compiles with noUncheckedIndexedAccess). + * Every call site is protected by a prior length check or loop bound — + * this throw is an internal-invariant guard, not a reachable user error. + */ +function at(arr: number[], i: number): number { + const v = arr[i] + if (v === undefined) { + throw new Error('Incohérence interne : index hors du vecteur.') + } + return v +} + +// --------------------------------------------------------------------------- +// validateParamSpec +// --------------------------------------------------------------------------- + +/** + * Validate a ParamSpec at creation time. Throws (French message) when: + * - no param, or more than 7 params; + * - a business label is missing (never raw a, b, c); + * - bounds are inconsistent (min >= max) or step is not strictly positive; + * - a baseline lies outside its own bounds; + * - constraint 'sum100' does not have EXACTLY ONE 'share' param with + * derived:true (0 or 2+ derived, or derived on a 'slider'); + * - a derived param exists without a constraint able to resolve it. + */ +export function validateParamSpec(spec: ParamSpec): void { + if (spec.params.length === 0) { + throw new Error('Au moins un paramètre est requis.') + } + if (spec.params.length > MAX_PARAMS) { + throw new Error( + `Trop de paramètres : ${spec.params.length} (maximum ${MAX_PARAMS}).`, + ) + } + + for (const p of spec.params) { + if (typeof p.label !== 'string' || p.label.trim() === '') { + throw new Error(`Libellé métier manquant pour le paramètre « ${p.key} ».`) + } + if (!(p.min < p.max)) { + throw new Error( + `Bornes incohérentes pour « ${p.key} » : min (${p.min}) doit être strictement inférieur à max (${p.max}).`, + ) + } + if (!(p.step > 0)) { + throw new Error( + `Pas invalide pour « ${p.key} » : ${p.step} (doit être strictement positif).`, + ) + } + if (p.baseline !== undefined && (p.baseline < p.min || p.baseline > p.max)) { + throw new Error( + `Statu quo hors bornes pour « ${p.key} » : ${p.baseline} (bornes ${p.min}–${p.max}).`, + ) + } + } + + const derived = spec.params.filter(p => p.derived === true) + + if (spec.constraint === 'sum100') { + if (derived.some(p => p.kind !== 'share')) { + throw new Error( + 'Un paramètre dérivé doit être une part (kind « share »), pas un curseur.', + ) + } + if (derived.length === 0) { + throw new Error( + 'Contrainte sum100 : exactement une part dérivée est requise (aucune trouvée).', + ) + } + if (derived.length > 1) { + throw new Error( + `Contrainte sum100 : exactement une part dérivée est requise (${derived.length} trouvées).`, + ) + } + } else if (derived.length > 0) { + throw new Error( + 'Paramètre dérivé sans contrainte : rien ne permet de le résoudre.', + ) + } +} + +// --------------------------------------------------------------------------- +// validateVote +// --------------------------------------------------------------------------- + +/** + * Validate one vote vector against the spec. + * `values` follows the order of spec.params EXCLUDING derived params + * (Vote.values contract). Throws (French message) when: + * - the vector length does not match the number of votable params; + * - a value is not a finite number (NaN / ±Infinity sanitization); + * - a value is out of bounds or off the step grid (FLOAT_EPS tolerance); + * - constraint 'sum100': the resolved derived (100 − Σ voted shares) would + * exit its own [min, max] bounds ⇒ the vote is rejected. + */ +export function validateVote(spec: ParamSpec, values: number[]): void { + const votable = votableParams(spec) + + if (values.length !== votable.length) { + throw new Error( + `Nombre de valeurs invalide : ${values.length} reçues, ${votable.length} attendues.`, + ) + } + + votable.forEach((p, i) => { + const v = values[i] + if (typeof v !== 'number' || !Number.isFinite(v)) { + throw new Error( + `Valeur invalide pour « ${p.label} » : un nombre fini est attendu.`, + ) + } + if (v < p.min - FLOAT_EPS || v > p.max + FLOAT_EPS) { + throw new Error( + `Valeur hors bornes pour « ${p.label} » : ${v} (bornes ${p.min}–${p.max}).`, + ) + } + // Step grid: v must equal min + k×step for an integer k (float tolerance). + const k = Math.round((v - p.min) / p.step) + if (Math.abs(p.min + k * p.step - v) > FLOAT_EPS) { + throw new Error( + `Valeur non alignée sur le pas pour « ${p.label} » : ${v} (pas de ${p.step} depuis ${p.min}).`, + ) + } + }) + + if (spec.constraint === 'sum100') { + const derivedParam = spec.params.find(p => p.derived === true) + if (derivedParam) { + let shareSum = 0 + votable.forEach((p, i) => { + if (p.kind === 'share') shareSum += at(values, i) + }) + const resolved = 100 - shareSum + if ( + resolved < derivedParam.min - FLOAT_EPS + || resolved > derivedParam.max + FLOAT_EPS + ) { + throw new Error( + `La part calculée « ${derivedParam.label} » sortirait de ses bornes : ${resolved} (bornes ${derivedParam.min}–${derivedParam.max}).`, + ) + } + } + } +} + +// --------------------------------------------------------------------------- +// resolveDerived +// --------------------------------------------------------------------------- + +/** + * Expand a votable vector into the COMPLETE vector in spec.params order. + * The derived share (sum100) is resolved linearly: 100 − Σ other shares. + * 'slider' params live outside the constraint and pass through untouched. + * Throws on a length mismatch (misuse guard — same message as validateVote). + */ +export function resolveDerived(spec: ParamSpec, values: number[]): number[] { + const votable = votableParams(spec) + + if (values.length !== votable.length) { + throw new Error( + `Nombre de valeurs invalide : ${values.length} reçues, ${votable.length} attendues.`, + ) + } + + let shareSum = 0 + votable.forEach((p, i) => { + if (p.kind === 'share') shareSum += at(values, i) + }) + + let cursor = 0 + return spec.params.map(p => (p.derived === true ? 100 - shareSum : at(values, cursor++))) +} + +// --------------------------------------------------------------------------- +// medianByElement +// --------------------------------------------------------------------------- + +/** + * LOW median, element by element. + * Each column is sorted ascending and the element at index floor((n-1)/2) is + * taken — for an even n this is the LOWER of the two central elements. + * Invariant: every median value is a value actually voted by someone, so the + * step grid is honored by construction (« une position que chacun aurait pu + * proposer »). + * 0 votes ⇒ [] (the caller falls back to the baseline vector). + */ +export function medianByElement(votesValues: number[][]): number[] { + const n = votesValues.length + if (n === 0) return [] + + const width = votesValues[0]?.length ?? 0 + const lowMedianIndex = Math.floor((n - 1) / 2) + const medians: number[] = [] + + for (let j = 0; j < width; j++) { + const column = votesValues.map(v => at(v, j)).sort((a, b) => a - b) + medians.push(at(column, lowMedianIndex)) + } + return medians +} + +// --------------------------------------------------------------------------- +// crystallize +// --------------------------------------------------------------------------- + +/** + * Compute the crystallized position: LOW median of the VOTED vectors, then + * derived resolution. Returns the complete vector in spec.params order. + * + * sum100 repair (locked spec): the derived is never aggregated — it is + * resolved from the median of the voted shares. If it exits its bounds: + * - clamp it to the violated bound; + * - renormalize the non-derived shares PROPORTIONALLY to restore sum 100: + * each share is multiplied by (100 − clampedDerived) / Σ median shares. + * (Renormalized shares may leave the step grid — accepted, documented.) + * - degenerate sub-case Σ median shares = 0: proportionality is undefined, + * the remainder (100 − clampedDerived) is spread equally instead. + * 'slider' params are outside the constraint and are never renormalized. + * + * 0 votes ⇒ the baseline vector (spec.params[i].baseline ?? min) — never an + * empty screen; the crystallization GESTURE itself stays human (Δ3). + */ +export function crystallize(spec: ParamSpec, votesValues: number[][]): number[] { + if (votesValues.length === 0) { + return spec.params.map(p => p.baseline ?? p.min) + } + + const median = medianByElement(votesValues) + const full = resolveDerived(spec, median) + + if (spec.constraint !== 'sum100') return full + + const derivedIndex = spec.params.findIndex(p => p.derived === true) + const derivedParam = spec.params[derivedIndex] + if (derivedIndex === -1 || derivedParam === undefined) { + return full // unreachable on a validated spec + } + const resolved = at(full, derivedIndex) + + const withinBounds + = resolved >= derivedParam.min - FLOAT_EPS + && resolved <= derivedParam.max + FLOAT_EPS + if (withinBounds) return full + + // Clamp to the violated bound, then restore the sum-100 invariant. + const clamped = Math.min(Math.max(resolved, derivedParam.min), derivedParam.max) + const remainder = 100 - clamped + + let shareSum = 0 + let shareCount = 0 + spec.params.forEach((p, i) => { + if (p.derived !== true && p.kind === 'share') { + shareSum += at(full, i) + shareCount++ + } + }) + + return full.map((v, i) => { + if (i === derivedIndex) return clamped + const p = spec.params[i] + if (p === undefined || p.kind !== 'share') return v // sliders pass through untouched + if (shareSum === 0) return remainder / shareCount // degenerate: equal spread + return v * (remainder / shareSum) // proportional renormalization + }) +} + +// --------------------------------------------------------------------------- +// computeMyImpact +// --------------------------------------------------------------------------- + +/** One line of the « Pour moi » card: my quota for one share param. */ +export interface ImpactLine { + key: string + label: string + amount: number +} + +/** « Pour moi » card content — per share param + total. */ +export interface MyImpact { + perParam: ImpactLine[] + total: number +} + +/** + * 'linear-share' personal impact (Δ15 — « Pour moi » card). + * Applies ONLY when constraint is 'sum100' AND resources.amount is set AND + * spec.impactAttrKey is set. For EACH share param p (derived included): + * amount(p) = resources.amount × value(p) / 100 + * myQuota(p) = amount(p) × myAttr / Σ corpusAttrs + * `fullValues` is the COMPLETE vector in spec.params order (resolveDerived / + * crystallize output). 'slider' params are outside the constraint: no line. + * + * Returns null — NEVER an invented number — when myAttr is undefined, + * when Σ corpusAttrs is 0 (nobody declared), or on a malformed input. + */ +export function computeMyImpact( + spec: ParamSpec, + resources: { amount?: number }, + fullValues: number[], + myAttr: number | undefined, + corpusAttrs: number[], +): MyImpact | null { + if (spec.constraint !== 'sum100') return null + if (!resources.amount || !Number.isFinite(resources.amount)) return null + if (!spec.impactAttrKey) return null + if (myAttr === undefined || !Number.isFinite(myAttr)) return null + if (fullValues.length !== spec.params.length) return null // misuse guard + + const attrSum = corpusAttrs.reduce( + (sum, a) => sum + (Number.isFinite(a) ? a : 0), + 0, + ) + if (attrSum === 0) return null + + const amount = resources.amount + const perParam: ImpactLine[] = [] + let total = 0 + + spec.params.forEach((p, i) => { + if (p.kind !== 'share') return + const paramAmount = (amount * at(fullValues, i)) / 100 + const myQuota = (paramAmount * myAttr) / attrSum + perParam.push({ key: p.key, label: p.label, amount: myQuota }) + total += myQuota + }) + + return { perParam, total } +} + +// --------------------------------------------------------------------------- +// detectBimodality +// --------------------------------------------------------------------------- + +/** + * Simple documented heuristic over ONE param's voted values — NEVER blocking + * (it only feeds the non-blocking banner and the crystallization reminder, + * Δ16): two distinct positions are detected when, after ascending sort, + * the LARGEST gap between consecutive values satisfies ALL of: + * - n >= 4 (below that, no distribution to speak of); + * - gap > 40% of the total range (max − min); + * - at least 2 values on EACH side of the gap (a single outlier is not a + * second position). + * Non-finite values are ignored; any degenerate input returns false. + */ +export function detectBimodality(values: number[]): boolean { + const sorted = values.filter(v => Number.isFinite(v)).sort((a, b) => a - b) + const n = sorted.length + if (n < 4) return false + + const range = at(sorted, n - 1) - at(sorted, 0) + if (range <= 0) return false + + let maxGap = 0 + for (let i = 0; i < n - 1; i++) { + const gap = at(sorted, i + 1) - at(sorted, i) + if (gap > maxGap) maxGap = gap + } + if (maxGap <= 0.4 * range) return false + + // The max gap must split the values 2+ / 2+ (ties: any qualifying position). + for (let i = 1; i <= n - 3; i++) { + if (at(sorted, i + 1) - at(sorted, i) === maxGap) return true + } + return false +} diff --git a/frontend/app/engine/settings.ts b/frontend/app/engine/settings.ts new file mode 100644 index 0000000..588234e --- /dev/null +++ b/frontend/app/engine/settings.ts @@ -0,0 +1,167 @@ +/** + * Pact settings resolution — the Pact IS the settings store (Δ10). + * + * resolveSettings() reads the Pact clauses that carry a settingKey, finds the + * 'current' ClauseVersion bearing a settingValue for each, and produces the + * resolved CollectiveSettings consumed by triage.ts and state.ts. Pure + * function — NOT a settings table: every threshold is a voted clause. + * + * Rules (BLUEPRINT-V2.md « Modèle de domaine » + Δ10): + * - any absent key falls back to SETTINGS_DEFAULTS; + * - invalid values (wrong type, negative number, unknown enum member) fall + * back to the default SILENTLY — a corrupted Pact never crashes the engine; + * - any unresolved OPTIONAL protocolByRange key stays undefined: the caller + * falls back to the consent protocol; + * - a missing 'protocols.consent' (corrupted bundle) still returns a full + * settings object with protocolByRange.consent === '' — hasConsentProtocol() + * lets the triage route 'advice' with the banner « Aucun protocole — crée-le + * ou décide sur avis », NEVER a crash. + */ + +import type { + Clause, + ClauseVersion, + CollectiveSettings, + Id, + InertiaPreset, + Json, +} from '../types/domain' + +/** Default triage settings — used for every key the Pact does not resolve. */ +export const SETTINGS_DEFAULTS = { + smallGroupMax: 5, + collectiveMin: 50, + consentMax: 7, + objectionWindowHours: 48, + adviceWindowHours: 72, + framingDays: 14, + concernEscalateRatio: 0.5, + recurrenceThreshold: 3, + reviewDelayDays: 90, + requireEffects: 'binding', +} as const satisfies CollectiveSettings['triage'] + +const INERTIA_PRESETS: readonly InertiaPreset[] = ['low', 'standard', 'high', 'max'] + +/** Most recent wins when a clause carries several 'current' versions (data anomaly). */ +function stampOf(version: ClauseVersion): string { + return version.adoptedAt ?? version.updatedAt +} + +/** The settingValue of the clause's 'current' version, if any. */ +function currentValueOf(clause: Clause, versions: ClauseVersion[]): Json | undefined { + let best: ClauseVersion | undefined + for (const version of versions) { + if (version.clauseId !== clause.id || version.archivedAt) continue + if (version.status !== 'current' || version.settingValue === undefined) continue + if (!best || stampOf(version) > stampOf(best)) best = version + } + return best?.settingValue +} + +/** First non-archived clause holding this settingKey with a resolvable current value. */ +function resolveRaw(key: string, clauses: Clause[], versions: ClauseVersion[]): Json | undefined { + for (const clause of clauses) { + if (clause.settingKey !== key || clause.archivedAt) continue + const value = currentValueOf(clause, versions) + if (value !== undefined) return value + } + return undefined +} + +/** Wrong type or negative number ⇒ default, silently. */ +function numberOrDefault(value: Json | undefined, fallback: number): number { + return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback +} + +/** Unknown enum member ⇒ default, silently. */ +function requireEffectsOrDefault(value: Json | undefined): 'none' | 'structural' | 'binding' { + return value === 'none' || value === 'structural' || value === 'binding' + ? value + : SETTINGS_DEFAULTS.requireEffects +} + +/** Protocol ids must be non-empty strings — anything else counts as unresolved. */ +function idOrUndefined(value: Json | undefined): Id | undefined { + return typeof value === 'string' && value.trim().length > 0 ? value : undefined +} + +/** + * Resolve the collective's settings from its Pact clauses. + * + * Recognized setting keys: + * triage.smallGroupMax · triage.collectiveMin · triage.consentMax · + * triage.objectionWindowHours · triage.adviceWindowHours · + * triage.framingDays · triage.concernEscalateRatio · + * triage.recurrenceThreshold · triage.reviewDelayDays · + * triage.requireEffects · protocols.consent · protocols.nuanced · + * protocols.large · protocols.parametric · protocols.election · + * protocols.clauseByInertia.{low,standard,high,max} + */ +export function resolveSettings(clauses: Clause[], versions: ClauseVersion[]): CollectiveSettings { + const raw = (key: string): Json | undefined => resolveRaw(key, clauses, versions) + + const triage: CollectiveSettings['triage'] = { + smallGroupMax: numberOrDefault(raw('triage.smallGroupMax'), SETTINGS_DEFAULTS.smallGroupMax), + collectiveMin: numberOrDefault(raw('triage.collectiveMin'), SETTINGS_DEFAULTS.collectiveMin), + consentMax: numberOrDefault(raw('triage.consentMax'), SETTINGS_DEFAULTS.consentMax), + objectionWindowHours: numberOrDefault( + raw('triage.objectionWindowHours'), + SETTINGS_DEFAULTS.objectionWindowHours, + ), + adviceWindowHours: numberOrDefault( + raw('triage.adviceWindowHours'), + SETTINGS_DEFAULTS.adviceWindowHours, + ), + framingDays: numberOrDefault(raw('triage.framingDays'), SETTINGS_DEFAULTS.framingDays), + concernEscalateRatio: numberOrDefault( + raw('triage.concernEscalateRatio'), + SETTINGS_DEFAULTS.concernEscalateRatio, + ), + recurrenceThreshold: numberOrDefault( + raw('triage.recurrenceThreshold'), + SETTINGS_DEFAULTS.recurrenceThreshold, + ), + reviewDelayDays: numberOrDefault(raw('triage.reviewDelayDays'), SETTINGS_DEFAULTS.reviewDelayDays), + requireEffects: requireEffectsOrDefault(raw('triage.requireEffects')), + } + + // Consent is the MANDATORY invariant key. When unfindable (corrupted + // bundle), we still return a complete object with consent === '' so the + // caller can degrade gracefully (hasConsentProtocol) — never a crash. + const protocolByRange: CollectiveSettings['protocolByRange'] = { + consent: idOrUndefined(raw('protocols.consent')) ?? '', + } + + const nuanced = idOrUndefined(raw('protocols.nuanced')) + if (nuanced) protocolByRange.nuanced = nuanced + const large = idOrUndefined(raw('protocols.large')) + if (large) protocolByRange.large = large + const parametric = idOrUndefined(raw('protocols.parametric')) + if (parametric) protocolByRange.parametric = parametric + const election = idOrUndefined(raw('protocols.election')) + if (election) protocolByRange.election = election + + // The Ğ1 heritage lives here, intact: the map is exposed only when ALL four + // presets resolve (the type is a complete Record — a partial map would lie). + // Otherwise the caller falls back to consent, like any optional key. + const byInertia: Partial> = {} + for (const preset of INERTIA_PRESETS) { + const id = idOrUndefined(raw(`protocols.clauseByInertia.${preset}`)) + if (id) byInertia[preset] = id + } + if (INERTIA_PRESETS.every((preset) => byInertia[preset] !== undefined)) { + protocolByRange.clauseByInertia = byInertia as Record + } + + return { triage, protocolByRange } +} + +/** + * True when the collective has a resolvable consent protocol. + * False ⇒ the triage routes 'advice' with the banner + * « Aucun protocole — crée-le ou décide sur avis » — never a crash. + */ +export function hasConsentProtocol(settings: CollectiveSettings): boolean { + return settings.protocolByRange.consent.trim().length > 0 +} diff --git a/frontend/app/engine/state.ts b/frontend/app/engine/state.ts new file mode 100644 index 0000000..6b3000a --- /dev/null +++ b/frontend/app/engine/state.ts @@ -0,0 +1,248 @@ +/** + * The ONE state machine of the decision (Δ29). + * + * Every mutation goes through canTransition(decision, to, ctx): the doctrinal + * guards (subsidiarity, instruction, consent) are tested code, not intentions. + * The context is assembled by the store; the function stays pure and testable. + * + * Guards, in order (BLUEPRINT-V2.md « Cycle de vie » + Δ13, Δ17, Δ27, Δ29): + * a) unknown transition; + * b) boundary — windowSuspendedAt suspends any exit from objection/advice; + * c) influx — concernEscalateRatio reached ⇒ widen or scopeKeptNote; + * d) matter — requireEffects at collective session opening; + * e) resources — « Ce que ça engage » at window/session opening; + * f) assent — non-easy objection windows adopt only on an explicit third-party + * agreement, never by pure silence; + * g) open objection — no adoption over an open objection; + * h) dossier — framing→closed only when every element child is terminal; + * i) crystallization — parametric sessions close by a dated human gesture, + * the engine NEVER crystallizes. + * + * Every refusal reason is one French sentence whose subject is the collective + * or the person — never the engine. + */ + +import type { + Assent, + CollectiveSettings, + Concern, + Decision, + DecisionStatus, + ISODate, + Objection, + VoteSession, +} from '../types/domain' + +/** Allowed transitions — statuses absent from the table are terminal. */ +export const TRANSITIONS: Readonly>> = { + draft: ['advice', 'objection', 'framing', 'voting', 'adopted', 'transmitted', 'rejected'], + advice: ['adopted', 'voting'], + objection: ['adopted', 'framing', 'voting'], + framing: ['voting', 'closed'], + voting: ['adopted', 'rejected'], + adopted: ['revoked', 'closed'], +} + +/** Terminal statuses of an element child — condition of the dossier closure. */ +export const TERMINAL_STATUSES: readonly DecisionStatus[] = [ + 'adopted', + 'rejected', + 'revoked', + 'closed', + 'transmitted', +] + +/** Window states — a boundary objection suspends any exit from these. */ +const WINDOW_STATUSES: readonly DecisionStatus[] = ['objection', 'advice'] + +/** Sources whose closing is subject to the influx guard. */ +const INFLUX_SOURCES: readonly DecisionStatus[] = ['advice', 'objection', 'voting'] + +/** Adoption/closure targets watched by the influx guard. */ +const INFLUX_TARGETS: readonly DecisionStatus[] = ['adopted', 'rejected', 'closed'] + +const REASONS = { + unknown: 'La décision ne peut pas prendre ce chemin depuis son état actuel.', + boundary: + 'La frontière est contestée — le compte à rebours reste suspendu jusqu’à l’intégration ou une réponse motivée.', + influx: 'Le périmètre déborde — élargis d’un cran ou motive publiquement son maintien.', + matterMissing: 'Le collectif s’instruit avant de voter — formule au moins un effet recherché.', + matterTarget: + 'Une décision structurante se mesure — donne une cible à au moins un effet recherché.', + resources: + 'Le collectif doit savoir ce que ça engage — écris la note de ressources avant d’ouvrir.', + assent: 'Il manque un accord explicite — la fenêtre se prolonge.', + openObjection: 'Une objection reste ouverte — le collectif l’entend avant d’adopter.', + dossierEmpty: 'Le dossier n’a pas d’éléments — découpe-le avant de le clore.', + dossierPending: + 'Des éléments du dossier sont encore en cours — le dossier se clôt quand tous ont abouti.', + crystallization: 'Les votes sont figés — la cristallisation attend son geste.', +} as const + +export interface TransitionContext { + concerns: Concern[] + settings: CollectiveSettings + session?: VoteSession + children?: Decision[] + assents?: Assent[] + objections?: Objection[] + now: ISODate +} + +export type TransitionResult = { ok: true } | { ok: false; reason: string } + +export type WindowOutcome = 'adopt' | 'extend' | 'wait' + +function refuse(reason: string): TransitionResult { + return { ok: false, reason } +} + +/** ≥1 non-archived Assent on this decision from someone other than the author. */ +function hasThirdPartyAssent(decision: Decision, assents: Assent[] | undefined): boolean { + return (assents ?? []).some( + (assent) => + assent.decisionId === decision.id && + !assent.archivedAt && + assent.personId !== decision.authorId, + ) +} + +/** + * Can this decision move to `to`? Pure — the store assembles ctx. + * Refusals carry one French sentence (subject: the collective or the person). + */ +export function canTransition( + decision: Decision, + to: DecisionStatus, + ctx: TransitionContext, +): TransitionResult { + const from = decision.status + + // ── (a) Unknown transition ───────────────────────────────────────────── + // A window state may explicitly return to itself (re-arming after a + // resolved boundary objection, extension of a window) — nothing else loops. + const isWindowSelfReturn = WINDOW_STATUSES.includes(from) && to === from + if (!isWindowSelfReturn && !(TRANSITIONS[from] ?? []).includes(to)) { + return refuse(REASONS.unknown) + } + + // ── (b) Boundary — the contestation of frontiers precedes substance ──── + if (WINDOW_STATUSES.includes(from) && decision.windowSuspendedAt && to !== from) { + return refuse(REASONS.boundary) + } + + // ── (c) Influx — the scale-up is DECIDED, never evaporated (Δ17) ─────── + if (INFLUX_SOURCES.includes(from) && INFLUX_TARGETS.includes(to)) { + const concerns = ctx.concerns.filter( + (concern) => concern.decisionId === decision.id && !concern.archivedAt, + ) + const computed = concerns.filter((concern) => concern.origin === 'computed').length + const declared = concerns.filter((concern) => concern.origin === 'declared').length + const ratioReached = + computed > 0 && declared >= ctx.settings.triage.concernEscalateRatio * computed + if (ratioReached && !decision.scopeKeptNote) { + return refuse(REASONS.influx) + } + } + + const opensCollectiveSession = (from === 'draft' || from === 'framing') && to === 'voting' + + // ── (d) Matter — no engaging collective vote without instruction ─────── + if (opensCollectiveSession && decision.route === 'collective') { + const mode = ctx.settings.triage.requireEffects + const underGuard = + mode === 'binding' + ? decision.weight === 'binding' || decision.weight === 'structural' + : mode === 'structural' && decision.weight === 'structural' + if (underGuard) { + const effects = decision.brief?.effects ?? [] + if (effects.length === 0) { + return refuse(REASONS.matterMissing) + } + const hasMeasurableEffect = effects.some( + (effect) => effect.target !== undefined && effect.target.trim().length > 0, + ) + if (decision.weight === 'structural' && !hasMeasurableEffect) { + return refuse(REASONS.matterTarget) + } + } + } + + // ── (e) Resources — « Ce que ça engage » lives at the opening ────────── + const opensWindowOrSession = + opensCollectiveSession || (from === 'draft' && (to === 'objection' || to === 'advice')) + if ( + opensWindowOrSession && + decision.weight !== 'light' && + decision.route !== 'solo' && + decision.route !== 'record' + ) { + const note = decision.resources?.note ?? '' + if (note.trim().length === 0) { + return refuse(REASONS.resources) + } + } + + // ── (f) Assent — outside easy, agreement is a gesture, not silence (Δ27) + if (from === 'objection' && to === 'adopted' && decision.reversibility !== 'easy') { + if (!hasThirdPartyAssent(decision, ctx.assents)) { + return refuse(REASONS.assent) + } + } + + // ── (g) Open objection — never adopted over someone's maintained voice ─ + if (from === 'objection' && to === 'adopted') { + const hasOpenObjection = (ctx.objections ?? []).some( + (objection) => + objection.decisionId === decision.id && + !objection.archivedAt && + objection.status === 'open', + ) + if (hasOpenObjection) { + return refuse(REASONS.openObjection) + } + } + + // ── (h) Dossier — the closure is a steward gesture on a complete map (Δ13) + if (from === 'framing' && to === 'closed') { + const elements = (ctx.children ?? []).filter( + (child) => + child.chainKind === 'element' && + child.parentDecisionId === decision.id && + !child.archivedAt, + ) + if (elements.length === 0) { + return refuse(REASONS.dossierEmpty) + } + if (elements.some((child) => !TERMINAL_STATUSES.includes(child.status))) { + return refuse(REASONS.dossierPending) + } + } + + // ── (i) Crystallization — the engine NEVER crystallizes ──────────────── + // A parametric session is recognized by decision.paramSpec, or by the + // 'frozen' status (only parametric sessions ever freeze). + if (from === 'voting' && (to === 'adopted' || to === 'rejected') && ctx.session) { + const isParametric = decision.paramSpec !== undefined || ctx.session.status === 'frozen' + const crystallized = ctx.session.status === 'closed' && !!ctx.session.crystallizedById + if (isParametric && !crystallized) { + return refuse(REASONS.crystallization) + } + } + + return { ok: true } +} + +/** + * Outcome of an objection window at its deadline, WITHOUT open objection + * (the caller handles open objections — escalation or suspension): + * - suspended boundary ⇒ 'wait' (the countdown is not running); + * - easy ⇒ 'adopt' (silence counts as agreement — only there); + * - otherwise ⇒ 'adopt' on a third-party Assent, else 'extend' by one notch + * (+objectionWindowHours, Fil reminder) — never adoption by pure silence. + */ +export function windowOutcome(decision: Decision, ctx: TransitionContext): WindowOutcome { + if (decision.windowSuspendedAt) return 'wait' + if (decision.reversibility === 'easy') return 'adopt' + return hasThirdPartyAssent(decision, ctx.assents) ? 'adopt' : 'extend' +} diff --git a/frontend/app/engine/threshold.ts b/frontend/app/engine/threshold.ts index 87b895f..fc2aca0 100644 --- a/frontend/app/engine/threshold.ts +++ b/frontend/app/engine/threshold.ts @@ -108,3 +108,106 @@ export function techcommThreshold(cotecSize: number, exponent: number = 0.1): nu } return Math.ceil(cotecSize ** exponent) } + +// ───────────────────────────────────────────────────────────── +// Election — simple plurality (BLUEPRINT-V2.md Δ28, « Modalités » #4) +// ───────────────────────────────────────────────────────────── + +import type { FormulaParams, Id, Vote } from '~/types/domain' + +/** + * Outcome of an election tally. + * + * Discriminated union on `outcome`: + * - 'elected' — a single person leads by simple plurality. + * - 'tie' — several persons share the top count. The engine NEVER + * breaks a tie (no randomness, no first-come): the closure + * flow proposes a chained runoff among `exAequoIds`, or a + * draw only if the Pact planned it (FormulaParams.tieBreak). + * - 'rejected' — reason 'quorum': participants (blanks included) below + * electionMinParticipants; `required` = that quorum. + * reason 'no-designation': quorum reached (or absent) but + * every vote is blank — nobody was designated, and an + * empty tie would be meaningless; `required` still carries + * the quorum (0 when none) for display purposes. + */ +export type ElectionOutcome = + | { + outcome: 'elected' + winnerId: Id + counts: Record + blanks: number + participants: number + } + | { + outcome: 'tie' + exAequoIds: Id[] + counts: Record + blanks: number + participants: number + } + | { + outcome: 'rejected' + reason: 'quorum' | 'no-designation' + participants: number + required: number + } + +/** + * Tally an election by simple plurality. + * + * CONTRACT — the caller passes the LAST ACTIVE votes only: one vote per + * voter, `supersedesVoteId` chains already resolved (the store filters + * superseded votes). The engine does NOT deduplicate by voterId; + * `participants` is simply `votes.length`. + * + * Rules (Δ28): + * - A vote without `choicePersonId` is a BLANK: it counts for + * participation (quorum), never for designation. + * - Quorum: when `formula.electionMinParticipants` is set and + * participants (blanks included) < quorum ⇒ rejected ('quorum'). + * - Designation by simple PLURALITY of the non-blank votes. + * - Tie at the top ⇒ 'tie' with `exAequoIds` sorted (lexicographic — + * a deterministic display order, NEVER a tie-break: the engine does + * not pick a winner among equals, no randomness, no first-come). + * - Zero designation (all blanks) ⇒ rejected ('no-designation'). + * + * @param votes - Last active votes of the session (see contract above) + * @param formula - Protocol formula params (only electionMinParticipants is read) + * @returns The election outcome — never a tie silently broken + */ +export function electionResult( + votes: Vote[], + formula: Pick, +): ElectionOutcome { + const participants = votes.length + const quorum = formula.electionMinParticipants ?? 0 + + if (formula.electionMinParticipants !== undefined && participants < formula.electionMinParticipants) { + return { outcome: 'rejected', reason: 'quorum', participants, required: formula.electionMinParticipants } + } + + const counts: Record = {} + let blanks = 0 + for (const vote of votes) { + if (vote.choicePersonId) { + counts[vote.choicePersonId] = (counts[vote.choicePersonId] ?? 0) + 1 + } else { + blanks++ // blank: participation only, never designation + } + } + + const designatedIds = Object.keys(counts) + if (designatedIds.length === 0) { + return { outcome: 'rejected', reason: 'no-designation', participants, required: quorum } + } + + const topCount = Math.max(...designatedIds.map(id => counts[id]!)) + const leaders = designatedIds.filter(id => counts[id] === topCount) + + if (leaders.length === 1) { + return { outcome: 'elected', winnerId: leaders[0]!, counts, blanks, participants } + } + + return { outcome: 'tie', exAequoIds: [...leaders].sort(), counts, blanks, participants } +} diff --git a/frontend/app/engine/triage.ts b/frontend/app/engine/triage.ts new file mode 100644 index 0000000..c965b4c --- /dev/null +++ b/frontend/app/engine/triage.ts @@ -0,0 +1,341 @@ +/** + * Routing engine — UI name « Le chemin » (BLUEPRINT-V2.md « Algorithme de triage »). + * + * Pure function triage(input, ctx, settings) → Verdict. No store, no I/O: + * the store assembles TriageContext, the engine only reasons on it. + * Rule codes (R-U, R0a…R6) NEVER reach the card: the UI shows the one-sentence + * French explanation alone; codes live in the « pourquoi ? » disclosure, + * the PV and the derogation journal. + * + * Rule order — first match wins: + * R-U (urgency overlay) · R0a already decided · R0b my mandate · + * R0c someone else's mandate · R2 self only · R3 targeted clause · + * R4 small reversible group · R5 collective default · R6 recurrence overlay. + * + * DOCUMENTED INTERPRETATION CHOICES: + * - R0b comes BEFORE R2: a selfOnly decision whose scoped circles are covered + * by my mandate still routes 'mandate' (the mandate trace duty prevails). + * - R0a yields ONLY when the author explicitly targets the clause + * (amendsClauseId set): contesting the rule IS R3, and R3 looks the clause + * up in ctx.matchingClauses — R0a fires for a bare match. + * - Specified fallback (Δ10): protocolByRange.consent unresolvable ⇒ route + * 'advice' with the « Aucun protocole » banner, prior to EVERYTHING except + * R2 (solo needs no protocol) and R0a (record needs no protocol). + * Its rule code is 'R5' — it degrades the collective default. + * - Urgency refused (urgent && irreversible): the underlying route is kept + * with its MINIMAL windows (never doubled), rule 'R-U', refusal sentence. + * - R6 is an overlay: it sets Verdict.suggestion but never changes route or + * rule; 'claim-mandate' wins over 'protocolize' when both thresholds match. + */ + +import type { + Clause, + CollectiveSettings, + Id, + Mandate, + TriageContext, + TriageInput, + Verdict, +} from '~/types/domain' +import { + BINARY_COST, + METHOD_LABELS, + NO_PROTOCOL_BANNER, + PARAMETRIC_ALT, + RECORD_ALT, + URGENT_BADGE, + URGENT_REFUSED, +} from '~/lexicon' + +// TODO-lexicon: alternative costs missing from app/lexicon.ts (frozen file) — +// move these two constants there once it thaws. +const PARAMETRIC_COST = 'formuler les curseurs avant de voter' +const RECORD_COST = 'aucune fenêtre — la décision est déjà prise' + +/** Verdict before the permanent alternatives are attached. */ +type BaseVerdict = Omit + +// ───────────────────────────────────────────────────────────── +// Parametric hint — number / % / amount (€, DU) in the capture sentence +// ───────────────────────────────────────────────────────────── + +const DIGIT_PERCENT_CURRENCY = /\d|%|€/ +// Ğ1 universal dividend — uppercase only: the French article « du » never counts. +const DU_UNIT = /\bDU\b/ + +/** Detect a number, percentage or amount (€, DU, digits) in the title. */ +export function detectParametricHint(title: string): boolean { + return DIGIT_PERCENT_CURRENCY.test(title) || DU_UNIT.test(title) +} + +// ───────────────────────────────────────────────────────────── +// Main entry +// ───────────────────────────────────────────────────────────── + +export function triage( + input: TriageInput, + ctx: TriageContext, + settings: CollectiveSettings, +): Verdict { + const hint = detectParametricHint(input.title) + + let base = routeIgnoringUrgency(input, ctx, settings) + + // R-U — urgency overlay. + if (input.urgent) { + if (input.reversibility === 'irreversible') { + // Refusal: normal route, MINIMAL windows (never doubled), no chain. + base = { ...base, rule: 'R-U', explanation: URGENT_REFUSED } + } else { + // Conservatory: the lightest legitimate route NOW, doubled window, + // chained ratification (created by the store when validating). + base = { + ...base, + rule: 'R-U', + explanation: `${URGENT_BADGE}.`, + conservatoryChain: true, + ...(base.windowHours !== undefined ? { windowHours: base.windowHours * 2 } : {}), + } + } + } + + // R6 — recurrence & maturation overlay (never blocking, never re-routing). + const suggestion = recurrenceOverlay(input, ctx, settings) + + return { + ...base, + ...(hint ? { parametricHint: true } : {}), + ...(suggestion ? { suggestion } : {}), + alternatives: buildAlternatives(hint), + } +} + +// ───────────────────────────────────────────────────────────── +// Rules R0a → R5 (urgency stripped) +// ───────────────────────────────────────────────────────────── + +function routeIgnoringUrgency( + input: TriageInput, + ctx: TriageContext, + settings: CollectiveSettings, +): BaseVerdict { + // R0a — already decided. Skipped when the author explicitly targets the + // clause (that is R3 — contesting the rule). Route 'record' proposed as + // principal: act under the standing rule, and keep the trace. + const matched = ctx.matchingClauses[0] + if (matched && !input.amendsClauseId) { + return { + route: 'record', + rule: 'R0a', + explanation: `C'est déjà décidé (${matched.code}, ${matched.title}) — agis, ou conteste la règle.`, + reviewRequired: false, + engravingSuggested: false, + } + } + + // Specified fallback (Δ10): no consent protocol resolvable — prior to + // everything except R2/record. Rule code 'R5': the collective default, + // degraded to advice because the collective has no protocol yet. + if (!settings.protocolByRange.consent) { + if (input.scope.selfOnly) return r2SelfOnly(input) + return { + route: 'advice', + rule: 'R5', + explanation: `${NO_PROTOCOL_BANNER}.`, + windowHours: settings.triage.adviceWindowHours, + reviewRequired: false, + engravingSuggested: false, + } + } + + // R0b — my mandate covers. Before R2: even selfOnly, the mandate trace + // duty prevails. Empty scope.circleIds never counts as covered (a trivial + // ⊆ match would hand every uncircled decision to the first mandate). + const myMandate = coveringMandate(input, ctx.myActiveMandates) + if (myMandate) { + return { + route: 'mandate', + rule: 'R0b', + explanation: `Ton mandat ${myMandate.title} couvre — décide, c'est tracé.`, + windowHours: settings.triage.objectionWindowHours, + reviewRequired: false, + engravingSuggested: false, + } + } + + // R0c — someone else's mandate covers. TriageContext carries no person + // names: the mandate TITLE names the power, the store resolves the holder. + const otherMandate = coveringMandate(input, ctx.otherActiveMandates) + if (otherMandate) { + return { + route: 'transmit', + rule: 'R0c', + explanation: `Le mandat ${otherMandate.title} couvre — transmets à sa ou son titulaire.`, + reviewRequired: false, + engravingSuggested: false, + } + } + + // R2 — self only. + if (input.scope.selfOnly) return r2SelfOnly(input) + + // R3 — targeted clause: its inertia applies, vote of those it governs. + if (input.amendsClauseId) return r3TargetedClause(input, ctx, settings) + + const n = ctx.computedConcernedIds.length + + // R4 — small reversible group: ask for advice, then decide. + if (input.reversibility === 'easy' && n <= settings.triage.smallGroupMax) { + const plural = n > 1 ? 's' : '' + return { + route: 'advice', + rule: 'R4', + explanation: `Réversible et ${n} personne${plural} concernée${plural} — demande leur avis puis décide.`, + windowHours: settings.triage.adviceWindowHours, + reviewRequired: false, + engravingSuggested: false, + } + } + + // R5 — collective default, modality by perimeter size. + return r5Collective(input, n, settings) +} + +/** R2 — « Personne d'autre n'est concerné ». Zero window. On irreversible the + * engine SUGGESTS a review (reviewRequired) — the UI shows it pre-checked but + * REMOVABLE: on oneself the tool never interposes; the pre-check is a UI fact, + * the engine only recommends. */ +function r2SelfOnly(input: TriageInput): BaseVerdict { + return { + route: 'solo', + rule: 'R2', + explanation: 'Personne d\'autre n\'est concerné — décide.', + reviewRequired: input.reversibility === 'irreversible', + engravingSuggested: false, + } +} + +/** R3 — amendsClauseId set. Protocol resolved by the clause's inertia + * (clauseByInertia), specified fallback → consent. The clause is looked up in + * ctx.matchingClauses (the Q0 index); if absent, consent applies and the + * sentence names « cette règle » instead of a code. */ +function r3TargetedClause( + input: TriageInput, + ctx: TriageContext, + settings: CollectiveSettings, +): BaseVerdict { + const clause: Clause | undefined = ctx.matchingClauses.find( + c => c.id === input.amendsClauseId, + ) + const protocolId: Id + = (clause && settings.protocolByRange.clauseByInertia?.[clause.inertia]) + ?? settings.protocolByRange.consent + const name = clause ? clause.code : 'cette règle' + return { + route: 'collective', + rule: 'R3', + explanation: `Tu proposes une version de ${name} — son inertie s'applique : vote de ceux qu'elle gouverne.`, + protocolId, + ...(input.weight === 'structural' ? { framingDays: settings.triage.framingDays } : {}), + reviewRequired: false, + engravingSuggested: false, + } +} + +/** R5 — modality by computed perimeter size, protocols resolved by the Pact + * with the specified fallback → consent. */ +function r5Collective( + input: TriageInput, + n: number, + settings: CollectiveSettings, +): BaseVerdict { + const range = settings.protocolByRange + let protocolId: Id + let explanation: string + + if (n <= settings.triage.consentMax) { + protocolId = range.consent + explanation = `Vous êtes ${n} — un tour d'accord suffit : sans objection, c'est adopté.` + } else if (n <= settings.triage.collectiveMin) { + protocolId = range.nuanced ?? range.consent + explanation = `Vous êtes ${n} — vote nuancé : chacun se prononce en nuances, pas en camps.` + } else { + protocolId = range.large ?? range.consent + explanation = `Vous êtes ${n} — la modalité que votre Pacte a choisie s'applique.` + } + + const structural = input.weight === 'structural' + return { + route: 'collective', + rule: 'R5', + explanation, + protocolId, + ...(structural ? { framingDays: settings.triage.framingDays } : {}), + reviewRequired: structural || input.reversibility === 'irreversible', + engravingSuggested: structural, + } +} + +// ───────────────────────────────────────────────────────────── +// R6 overlay & permanent alternatives +// ───────────────────────────────────────────────────────────── + +/** R6 — recurrence & maturation. Overlay only: a suggestion, never a route. + * 'claim-mandate' (recent adopted look-alikes) wins over 'protocolize' + * (recorded look-alikes) when both thresholds are reached. */ +function recurrenceOverlay( + input: TriageInput, + ctx: TriageContext, + settings: CollectiveSettings, +): Verdict['suggestion'] | undefined { + const threshold = settings.triage.recurrenceThreshold + if (ctx.similarRecentCount >= threshold) { + return { + kind: 'claim-mandate', + prefill: { + title: input.title, + domainTags: input.tags, + domainCircleIds: input.scope.circleIds, + }, + } + } + if (ctx.similarRecordedCount >= threshold) { + return { + kind: 'protocolize', + prefill: { title: input.title, tags: input.tags }, + } + } + return undefined +} + +/** The permanent « Je choisis autrement » alternatives — ALWAYS present. + * Parametric comes FIRST when a number/%/amount was detected (parametricHint); + * otherwise record leads (the observatory gesture stays one tap away). */ +function buildAlternatives(parametricFirst: boolean): Verdict['alternatives'] { + const parametric = { + route: 'collective' as const, + label: PARAMETRIC_ALT, + cost: PARAMETRIC_COST, + } + const record = { route: 'record' as const, label: RECORD_ALT, cost: RECORD_COST } + const binary = { + route: 'collective' as const, + label: METHOD_LABELS.binary, + cost: BINARY_COST, + } + return parametricFirst ? [parametric, record, binary] : [record, parametric, binary] +} + +// ───────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────── + +/** A mandate covers when scope.circleIds is a NON-EMPTY subset of its + * domain.circleIds. Defensive status filter: only 'active' mandates count. */ +function coveringMandate(input: TriageInput, mandates: Mandate[]): Mandate | undefined { + const { circleIds } = input.scope + if (circleIds.length === 0) return undefined + return mandates.find( + m => m.status === 'active' && circleIds.every(id => m.domain.circleIds.includes(id)), + ) +} diff --git a/frontend/tests/engine/election.spec.ts b/frontend/tests/engine/election.spec.ts new file mode 100644 index 0000000..5f0bb18 --- /dev/null +++ b/frontend/tests/engine/election.spec.ts @@ -0,0 +1,168 @@ +/** + * electionResult — simple plurality (BLUEPRINT-V2.md Δ28, « Modalités » #4). + * + * Contract under test: the caller passes the LAST ACTIVE votes only + * (supersedesVoteId chains resolved by the store) — one vote per voter. + * Blank vote = no choicePersonId: counts for participation, never for + * designation. The engine NEVER breaks a tie. + */ +import { describe, expect, it } from 'vitest' +import type { Vote } from '../../app/types/domain' +import { electionResult } from '../../app/engine/threshold' + +let seq = 0 + +/** Build a minimal last-active Vote; omit choicePersonId for a blank. */ +function vote(choicePersonId?: string): Vote { + seq++ + return { + id: `vote-${seq}`, + collectiveId: 'col-1', + createdAt: '2026-08-11T00:00:00Z', + updatedAt: '2026-08-11T00:00:00Z', + sessionId: 'session-1', + voterId: `voter-${seq}`, + choicePersonId, + } +} + +/** n votes for the same person (or n blanks when personId is undefined). */ +function votes(n: number, personId?: string): Vote[] { + return Array.from({ length: n }, () => vote(personId)) +} + +describe('electionResult — clear plurality', () => { + it('elects the most designated person; blanks count in participation only', () => { + const ballot = [...votes(3, 'alice'), ...votes(2, 'bob'), ...votes(1)] + const result = electionResult(ballot, { electionMinParticipants: 5 }) + + expect(result).toEqual({ + outcome: 'elected', + winnerId: 'alice', + counts: { alice: 3, bob: 2 }, + blanks: 1, + participants: 6, + }) + }) + + it('a tie below the top does not prevent election', () => { + const ballot = [...votes(3, 'alice'), ...votes(2, 'bob'), ...votes(2, 'carol')] + const result = electionResult(ballot, {}) + + expect(result.outcome).toBe('elected') + if (result.outcome === 'elected') { + expect(result.winnerId).toBe('alice') + expect(result.counts).toEqual({ alice: 3, bob: 2, carol: 2 }) + } + }) + + it('works without any quorum configured (electionMinParticipants undefined)', () => { + const result = electionResult([vote('alice')], {}) + expect(result.outcome).toBe('elected') + }) +}) + +describe('electionResult — tie: the engine NEVER breaks it', () => { + it('two-way tie at the top => tie with sorted exAequoIds', () => { + const ballot = [...votes(2, 'bob'), ...votes(2, 'alice'), ...votes(1, 'carol')] + const result = electionResult(ballot, { electionMinParticipants: 3 }) + + expect(result).toEqual({ + outcome: 'tie', + exAequoIds: ['alice', 'bob'], // sorted — deterministic order, not a tie-break + counts: { alice: 2, bob: 2, carol: 1 }, + blanks: 0, + participants: 5, + }) + }) + + it('three-way tie => all three ex aequo, sorted regardless of arrival order', () => { + const ballot = [...votes(2, 'carol'), ...votes(2, 'alice'), ...votes(2, 'bob'), ...votes(1)] + const result = electionResult(ballot, { electionMinParticipants: 4 }) + + expect(result.outcome).toBe('tie') + if (result.outcome === 'tie') { + expect(result.exAequoIds).toEqual(['alice', 'bob', 'carol']) + expect(result.blanks).toBe(1) + expect(result.participants).toBe(7) + } + }) + + it('never designates a winner among equals (no random, no first-come)', () => { + // Same ballot tallied twice must yield the exact same tie. + const ballot = [...votes(1, 'bob'), ...votes(1, 'alice')] + const first = electionResult(ballot, {}) + const second = electionResult(ballot, {}) + expect(first).toEqual(second) + expect(first.outcome).toBe('tie') + }) +}) + +describe('electionResult — rejections', () => { + it('quorum not reached => rejected with reason quorum and required', () => { + const ballot = [...votes(2, 'alice'), ...votes(1)] + const result = electionResult(ballot, { electionMinParticipants: 5 }) + + expect(result).toEqual({ + outcome: 'rejected', + reason: 'quorum', + participants: 3, + required: 5, + }) + }) + + it('quorum reached but all blanks => rejected with reason no-designation', () => { + const ballot = votes(4) + const result = electionResult(ballot, { electionMinParticipants: 3 }) + + expect(result).toEqual({ + outcome: 'rejected', + reason: 'no-designation', + participants: 4, + required: 3, + }) + }) + + it('zero votes without quorum => rejected no-designation (required 0)', () => { + const result = electionResult([], {}) + expect(result).toEqual({ + outcome: 'rejected', + reason: 'no-designation', + participants: 0, + required: 0, + }) + }) +}) + +describe('electionResult — blanks and participation', () => { + it('quorum reached THANKS to blanks: blanks count for participation', () => { + // 3 designations alone would miss the quorum of 5; 2 blanks complete it. + const ballot = [...votes(3, 'alice'), ...votes(2)] + const result = electionResult(ballot, { electionMinParticipants: 5 }) + + expect(result).toEqual({ + outcome: 'elected', + winnerId: 'alice', + counts: { alice: 3 }, + blanks: 2, + participants: 5, + }) + }) + + it('exact quorum boundary: participants === required passes', () => { + const ballot = [...votes(1, 'alice'), ...votes(1)] + const result = electionResult(ballot, { electionMinParticipants: 2 }) + expect(result.outcome).toBe('elected') + }) + + it('blanks never appear in counts', () => { + const ballot = [...votes(2, 'alice'), ...votes(3)] + const result = electionResult(ballot, {}) + if (result.outcome === 'elected') { + expect(Object.keys(result.counts)).toEqual(['alice']) + expect(result.blanks).toBe(3) + } else { + throw new Error(`expected elected, got ${result.outcome}`) + } + }) +}) diff --git a/frontend/tests/engine/impact.spec.ts b/frontend/tests/engine/impact.spec.ts new file mode 100644 index 0000000..1d97c5a --- /dev/null +++ b/frontend/tests/engine/impact.spec.ts @@ -0,0 +1,158 @@ +/** + * Tests du moteur de périmètre — computeConcerned / expandCircleMembers. + * Fonctions pures : fixtures assemblées à la main, aucun store. + */ +import { describe, expect, it } from 'vitest' +import { computeConcerned, expandCircleMembers } from '../../app/engine/impact' +import type { Circle, Decision, Mandate } from '../../app/types/domain' + +const T0 = '2026-01-01T00:00:00Z' +const T9 = '2027-01-01T00:00:00Z' +const AUTHOR = 'p-author' + +function mkCircle(over: Partial = {}): Circle { + return { + id: 'c1', + collectiveId: 'col', + createdAt: T0, + updatedAt: T0, + name: 'Forgerons', + purpose: '', + memberIds: [], + domains: [], + ...over, + } +} + +function mkMandate(over: Partial = {}): Mandate { + return { + id: 'm1', + collectiveId: 'col', + createdAt: T0, + updatedAt: T0, + title: 'Modération', + holderId: 'p-holder', + originDecisionId: 'd0', + domain: { circleIds: ['c1'], tags: [] }, + startsAt: T0, + endsAt: T9, + electorCircleId: 'c1', + nominationMethod: 'consent', + reports: [], + status: 'active', + ...over, + } +} + +function mkScope(over: Partial = {}): Decision['scope'] { + return { selfOnly: false, circleIds: [], personIds: [], ...over } +} + +describe('computeConcerned — l\'union calculée, chaque entrée avec sa raison', () => { + it('unit les trois sources : cercles, personnes nommées, titulaires de mandats', () => { + const circles = [mkCircle({ id: 'c1', name: 'Forgerons', memberIds: ['p1', 'p2'] })] + const mandates = [mkMandate({ holderId: 'p4', title: 'Modération' })] + const scope = mkScope({ circleIds: ['c1'], personIds: ['p3'] }) + + const result = computeConcerned(scope, circles, mandates, AUTHOR) + + expect(result).toHaveLength(4) + expect(result).toContainEqual({ personId: 'p1', reason: 'membre du cercle Forgerons' }) + expect(result).toContainEqual({ personId: 'p2', reason: 'membre du cercle Forgerons' }) + expect(result).toContainEqual({ personId: 'p3', reason: 'nommé·e par l\'auteur' }) + expect(result).toContainEqual({ personId: 'p4', reason: 'titulaire du mandat Modération' }) + }) + + it('dédoublonne — la première raison gagne, priorité cercle > nommé > mandat', () => { + const circles = [mkCircle({ id: 'c1', name: 'Forgerons', memberIds: ['p1'] })] + // p1 est membre du cercle ET nommé ET titulaire d'un mandat intersectant. + const mandates = [mkMandate({ holderId: 'p1' })] + const scope = mkScope({ circleIds: ['c1'], personIds: ['p1'] }) + + const result = computeConcerned(scope, circles, mandates, AUTHOR) + + expect(result).toEqual([{ personId: 'p1', reason: 'membre du cercle Forgerons' }]) + }) + + it('dédoublonne — nommé·e gagne sur titulaire de mandat', () => { + const mandates = [mkMandate({ holderId: 'p1', domain: { circleIds: ['c1'], tags: [] } })] + const scope = mkScope({ circleIds: ['c1'], personIds: ['p1'] }) + + const result = computeConcerned(scope, [], mandates, AUTHOR) + + expect(result).toEqual([{ personId: 'p1', reason: 'nommé·e par l\'auteur' }]) + }) + + it('exclut l\'auteur de toutes les sources', () => { + const circles = [mkCircle({ id: 'c1', memberIds: [AUTHOR, 'p1'] })] + const mandates = [mkMandate({ holderId: AUTHOR })] + const scope = mkScope({ circleIds: ['c1'], personIds: [AUTHOR] }) + + const result = computeConcerned(scope, circles, mandates, AUTHOR) + + expect(result.map(e => e.personId)).toEqual(['p1']) + }) + + it('n\'inclut un titulaire que si le domaine du mandat intersecte les cercles du périmètre', () => { + const mandates = [ + mkMandate({ id: 'm1', holderId: 'p4', domain: { circleIds: ['c9'], tags: [] } }), + mkMandate({ id: 'm2', holderId: 'p5', title: 'Trésorerie', domain: { circleIds: ['c2', 'c1'], tags: [] } }), + ] + const scope = mkScope({ circleIds: ['c1'] }) + + const result = computeConcerned(scope, [], mandates, AUTHOR) + + expect(result).toEqual([{ personId: 'p5', reason: 'titulaire du mandat Trésorerie' }]) + }) + + it('ignore les mandats non actifs', () => { + const mandates = [mkMandate({ holderId: 'p4', status: 'expired' })] + const scope = mkScope({ circleIds: ['c1'] }) + + expect(computeConcerned(scope, [], mandates, AUTHOR)).toEqual([]) + }) + + it('ignore sans erreur un cercle inconnu dans le périmètre', () => { + const scope = mkScope({ circleIds: ['c-fantome'], personIds: ['p1'] }) + + const result = computeConcerned(scope, [], [], AUTHOR) + + expect(result).toEqual([{ personId: 'p1', reason: 'nommé·e par l\'auteur' }]) + }) +}) + +describe('expandCircleMembers — appartenance nominative, sans propagation', () => { + it('réunit les membres directs de plusieurs cercles, dédoublonnés', () => { + const circles = [ + mkCircle({ id: 'c1', memberIds: ['p1', 'p2'] }), + mkCircle({ id: 'c2', name: 'Cuisine', memberIds: ['p2', 'p3'] }), + ] + + const members = expandCircleMembers(['c1', 'c2'], circles) + + expect(members).toEqual(new Set(['p1', 'p2', 'p3'])) + }) + + it('ne propage PAS l\'appartenance des cercles enfants au parent (emboîtement ≠ appartenance)', () => { + // v2 : l'appartenance est nominative par cercle ; l'emboîtement + // (parentCircleId) sert l'élargissement d'un cran, jamais le calcul. + const parent = mkCircle({ id: 'c-parent', name: 'Village', memberIds: ['p1'] }) + const child = mkCircle({ + id: 'c-child', + name: 'Hameau', + memberIds: ['p2'], + parentCircleId: 'c-parent', + }) + + expect(expandCircleMembers(['c-parent'], [parent, child])).toEqual(new Set(['p1'])) + // Ni dans l'autre sens : l'enfant n'hérite pas des membres du parent. + expect(expandCircleMembers(['c-child'], [parent, child])).toEqual(new Set(['p2'])) + // L'élargissement est un geste explicite : ajouter le cercle au périmètre. + expect(expandCircleMembers(['c-parent', 'c-child'], [parent, child])) + .toEqual(new Set(['p1', 'p2'])) + }) + + it('ignore un identifiant de cercle inconnu', () => { + expect(expandCircleMembers(['c-fantome'], [mkCircle()])).toEqual(new Set()) + }) +}) diff --git a/frontend/tests/engine/parametric.spec.ts b/frontend/tests/engine/parametric.spec.ts new file mode 100644 index 0000000..4c91af8 --- /dev/null +++ b/frontend/tests/engine/parametric.spec.ts @@ -0,0 +1,480 @@ +/** + * Suite du moteur paramétrique — « Réglage collectif » (BLUEPRINT-V2.md + * Δ2, Δ3, Δ15, Δ16). Specs verrouillées testées ici : + * médiane BASSE élément par élément (pair ⇒ élément central inférieur, + * step respecté par construction), sum100 avec EXACTEMENT une part dérivée, + * clamp + renormalisation proportionnelle à la cristallisation, + * « Pour moi » linear-share (jamais de chiffre inventé), bimodalité + * heuristique jamais bloquante, dégénérescences 0 vote. + */ +import type { ParamSpec } from '../../app/types/domain' +import { describe, expect, it } from 'vitest' +import { + computeMyImpact, + crystallize, + detectBimodality, + medianByElement, + resolveDerived, + validateParamSpec, + validateVote, +} from '../../app/engine/parametric' + +// --------------------------------------------------------------------------- +// Specs de référence +// --------------------------------------------------------------------------- + +/** Spec sum100 : 2 parts votées + 1 part dérivée large (bornes 0–100). */ +function sum100Spec(overrides: Partial = {}): ParamSpec { + return { + constraint: 'sum100', + params: [ + { key: 'bois', label: 'Atelier bois', kind: 'share', min: 0, max: 100, step: 5 }, + { key: 'metal', label: 'Atelier métal', kind: 'share', min: 0, max: 100, step: 5 }, + { key: 'reserve', label: 'Réserve (calculé)', kind: 'share', min: 0, max: 100, step: 1, derived: true }, + ], + ...overrides, + } +} + +/** Spec sans contrainte : 2 curseurs bornés. */ +function sliderSpec(): ParamSpec { + return { + constraint: 'none', + params: [ + { key: 'duree', label: 'Durée (mois)', kind: 'slider', min: 1, max: 24, step: 1, baseline: 12 }, + { key: 'taux', label: 'Taux (%)', kind: 'slider', min: 0, max: 10, step: 0.5 }, + ], + } +} + +// --------------------------------------------------------------------------- +// medianByElement — médiane basse élément par élément +// --------------------------------------------------------------------------- + +describe('medianByElement — médiane basse élément par élément', () => { + it('nombre impair de votes : élément central classique', () => { + expect(medianByElement([[10], [30], [20]])).toEqual([20]) + }) + + it('nombre pair de votes : élément central INFÉRIEUR (médiane basse)', () => { + // Colonne triée [10, 20, 30, 40] : index floor((4-1)/2) = 1 ⇒ 20, pas 25. + expect(medianByElement([[40], [10], [30], [20]])).toEqual([20]) + }) + + it('la médiane paire est la valeur votée inférieure des deux centrales', () => { + const votes = [[15], [25], [40], [60]] + const [median] = medianByElement(votes) + expect(median).toBe(25) // jamais 32.5 (moyenne des centrales) + expect(votes.flat()).toContain(median) // une position que quelqu'un a votée + }) + + it('step respecté par construction : la médiane appartient à la grille votée', () => { + // Votes conformes au pas 5 : la médiane basse est l'un d'eux, donc conforme. + const votes = [[15], [25], [40], [60]] + const [median] = medianByElement(votes) + expect((median - 0) % 5).toBe(0) + }) + + it('élément par élément : chaque colonne a sa propre médiane', () => { + const votes = [ + [10, 90], + [20, 80], + [30, 70], + ] + expect(medianByElement(votes)).toEqual([20, 80]) + }) + + it('deux votes : médiane basse = le plus petit des deux', () => { + expect(medianByElement([[50, 10], [40, 20]])).toEqual([40, 10]) + }) + + it('0 vote ⇒ [] (l’appelant gère la baseline)', () => { + expect(medianByElement([])).toEqual([]) + }) +}) + +// --------------------------------------------------------------------------- +// validateParamSpec +// --------------------------------------------------------------------------- + +describe('validateParamSpec — validation de la spec à la création', () => { + it('accepte une spec sum100 valide (exactement une part dérivée)', () => { + expect(() => validateParamSpec(sum100Spec())).not.toThrow() + }) + + it('accepte une spec de curseurs sans contrainte', () => { + expect(() => validateParamSpec(sliderSpec())).not.toThrow() + }) + + it('rejette plus de 7 paramètres', () => { + const spec: ParamSpec = { + constraint: 'none', + params: Array.from({ length: 8 }, (_, i) => ({ + key: `p${i}`, + label: `Paramètre ${i}`, + kind: 'slider' as const, + min: 0, + max: 10, + step: 1, + })), + } + expect(() => validateParamSpec(spec)).toThrow(/maximum 7/) + }) + + it('rejette une spec sans aucun paramètre', () => { + expect(() => validateParamSpec({ constraint: 'none', params: [] })).toThrow(/Au moins un paramètre/) + }) + + it('rejette un libellé métier manquant', () => { + const spec = sliderSpec() + spec.params[0].label = ' ' + expect(() => validateParamSpec(spec)).toThrow(/Libellé métier manquant/) + }) + + it('rejette sum100 sans part dérivée (0 derived)', () => { + const spec = sum100Spec() + delete spec.params[2].derived + expect(() => validateParamSpec(spec)).toThrow(/exactement une part dérivée/) + }) + + it('rejette sum100 avec deux parts dérivées (2 derived)', () => { + const spec = sum100Spec() + spec.params[1].derived = true + expect(() => validateParamSpec(spec)).toThrow(/2 trouvées/) + }) + + it('rejette un derived posé sur un curseur (kind slider)', () => { + const spec = sum100Spec() + spec.params[2].kind = 'slider' + expect(() => validateParamSpec(spec)).toThrow(/part/) + }) + + it('rejette un derived sans contrainte pour le résoudre', () => { + const spec = sliderSpec() + spec.params[1].derived = true + expect(() => validateParamSpec(spec)).toThrow(/sans contrainte/) + }) + + it('rejette des bornes incohérentes (min >= max)', () => { + const spec = sliderSpec() + spec.params[0].min = 24 + spec.params[0].max = 24 + expect(() => validateParamSpec(spec)).toThrow(/Bornes incohérentes/) + }) + + it('rejette un pas nul ou négatif (step <= 0)', () => { + const spec = sliderSpec() + spec.params[1].step = 0 + expect(() => validateParamSpec(spec)).toThrow(/Pas invalide/) + spec.params[1].step = -1 + expect(() => validateParamSpec(spec)).toThrow(/Pas invalide/) + }) + + it('rejette une baseline hors bornes', () => { + const spec = sliderSpec() + spec.params[0].baseline = 25 // max 24 + expect(() => validateParamSpec(spec)).toThrow(/hors bornes/) + }) +}) + +// --------------------------------------------------------------------------- +// validateVote +// --------------------------------------------------------------------------- + +describe('validateVote — validation d’un vote', () => { + it('accepte un vote valide (ordre des params hors derived)', () => { + expect(() => validateVote(sum100Spec(), [60, 30])).not.toThrow() + expect(() => validateVote(sliderSpec(), [12, 2.5])).not.toThrow() + }) + + it('rejette une longueur fausse (le derived ne se vote pas)', () => { + expect(() => validateVote(sum100Spec(), [60, 30, 10])).toThrow(/Nombre de valeurs invalide/) + expect(() => validateVote(sum100Spec(), [60])).toThrow(/Nombre de valeurs invalide/) + }) + + it('rejette NaN et Infinity (sanitize)', () => { + expect(() => validateVote(sum100Spec(), [Number.NaN, 30])).toThrow(/nombre fini/) + expect(() => validateVote(sum100Spec(), [60, Number.POSITIVE_INFINITY])).toThrow(/nombre fini/) + expect(() => validateVote(sliderSpec(), [Number.NEGATIVE_INFINITY, 2])).toThrow(/nombre fini/) + }) + + it('rejette une valeur hors bornes', () => { + expect(() => validateVote(sliderSpec(), [0, 2])).toThrow(/hors bornes/) // min 1 + expect(() => validateVote(sliderSpec(), [12, 10.5])).toThrow(/hors bornes/) // max 10 + }) + + it('rejette une valeur non alignée sur le pas', () => { + expect(() => validateVote(sum100Spec(), [62, 30])).toThrow(/pas/) // pas de 5 + expect(() => validateVote(sliderSpec(), [12, 2.3])).toThrow(/pas/) // pas de 0.5 + }) + + it('tolérance flottante : 0.1 + 0.2 est accepté sur une grille de 0.1', () => { + const spec: ParamSpec = { + constraint: 'none', + params: [{ key: 't', label: 'Taux', kind: 'slider', min: 0, max: 1, step: 0.1 }], + } + expect(() => validateVote(spec, [0.1 + 0.2])).not.toThrow() // 0.30000000000000004 + }) + + it('sum100 : rejette le vote si le derived résolu sort de ses bornes', () => { + const spec = sum100Spec() + spec.params[2].max = 15 // réserve bornée [0, 15] + // 60 + 30 = 90 ⇒ derived 10 : OK ; 50 + 30 = 80 ⇒ derived 20 : rejet. + expect(() => validateVote(spec, [60, 30])).not.toThrow() + expect(() => validateVote(spec, [50, 30])).toThrow(/sortirait de ses bornes/) + // 105 votés ⇒ derived −5 < min 0 : rejet aussi. + expect(() => validateVote(spec, [70, 35])).toThrow(/sortirait de ses bornes/) + }) + + it('sum100 : le derived exactement sur sa borne est accepté', () => { + const spec = sum100Spec() + spec.params[2].max = 5 + expect(() => validateVote(spec, [65, 30])).not.toThrow() // derived = 5 pile + }) + + it('sum100 : un curseur hors contrainte ne compte pas dans la somme', () => { + const spec: ParamSpec = { + constraint: 'sum100', + params: [ + { key: 'duree', label: 'Durée (mois)', kind: 'slider', min: 1, max: 24, step: 1 }, + { key: 'bois', label: 'Atelier bois', kind: 'share', min: 0, max: 100, step: 5 }, + { key: 'reserve', label: 'Réserve (calculé)', kind: 'share', min: 0, max: 100, step: 1, derived: true }, + ], + } + // Le curseur vaut 24 : s'il comptait, derived = 100 − 24 − 60 = 16 ; + // il ne compte pas ⇒ derived = 40, dans [0, 100]. + expect(() => validateVote(spec, [24, 60])).not.toThrow() + }) +}) + +// --------------------------------------------------------------------------- +// resolveDerived +// --------------------------------------------------------------------------- + +describe('resolveDerived — résolution linéaire du derived', () => { + it('insère le derived à sa position dans l’ordre de spec.params', () => { + expect(resolveDerived(sum100Spec(), [60, 30])).toEqual([60, 30, 10]) + }) + + it('derived en position centrale : l’ordre est préservé', () => { + const spec: ParamSpec = { + constraint: 'sum100', + params: [ + { key: 'a', label: 'Part A', kind: 'share', min: 0, max: 100, step: 1 }, + { key: 'r', label: 'Réserve (calculé)', kind: 'share', min: 0, max: 100, step: 1, derived: true }, + { key: 'b', label: 'Part B', kind: 'share', min: 0, max: 100, step: 1 }, + ], + } + expect(resolveDerived(spec, [40, 35])).toEqual([40, 25, 35]) + }) + + it('les curseurs hors contrainte passent tels quels', () => { + expect(resolveDerived(sliderSpec(), [12, 2.5])).toEqual([12, 2.5]) + }) + + it('sum100 avec curseur : seul les parts entrent dans le calcul du derived', () => { + const spec: ParamSpec = { + constraint: 'sum100', + params: [ + { key: 'duree', label: 'Durée (mois)', kind: 'slider', min: 1, max: 24, step: 1 }, + { key: 'bois', label: 'Atelier bois', kind: 'share', min: 0, max: 100, step: 5 }, + { key: 'reserve', label: 'Réserve (calculé)', kind: 'share', min: 0, max: 100, step: 1, derived: true }, + ], + } + expect(resolveDerived(spec, [24, 60])).toEqual([24, 60, 40]) + }) + + it('rejette une longueur fausse', () => { + expect(() => resolveDerived(sum100Spec(), [60])).toThrow(/Nombre de valeurs invalide/) + }) +}) + +// --------------------------------------------------------------------------- +// crystallize +// --------------------------------------------------------------------------- + +describe('crystallize — médiane basse des parts votées puis résolution', () => { + it('cas nominal : médiane basse puis derived résolu, somme 100', () => { + const votes = [ + [50, 20], + [60, 30], + [70, 40], + ] + const out = crystallize(sum100Spec(), votes) + expect(out).toEqual([60, 30, 10]) + expect(out[0] + out[1] + out[2]).toBe(100) + }) + + it('derived dans ses bornes : aucun clamp, valeurs médianes intactes', () => { + const spec = sum100Spec() + spec.params[2].max = 15 + const out = crystallize(spec, [[60, 30], [65, 30], [55, 35]]) + expect(out).toEqual([60, 30, 10]) // derived 10 ≤ 15 : rien à clamper + }) + + it('cas chiffré exact : derived clampé à 5, parts renormalisées ×95/90', () => { + // Médiane des parts votées = {60, 30} ⇒ derived résolu 10 > max 5. + // Spec verrouillée : clamp à 5, puis parts × (100 − 5)/(60 + 30). + const spec = sum100Spec() + spec.params[2].max = 5 + const votes = [ + [50, 20], + [60, 30], + [70, 40], + ] + const out = crystallize(spec, votes) + expect(out[2]).toBe(5) + expect(out[0]).toBeCloseTo((60 * 95) / 90, 10) + expect(out[1]).toBeCloseTo((30 * 95) / 90, 10) + expect(out[0] + out[1] + out[2]).toBeCloseTo(100, 10) + }) + + it('clamp à la borne min : même renormalisation proportionnelle', () => { + // Médiane {60, 30} ⇒ derived 10 < min 20 : clamp à 20, parts × 80/90. + const spec = sum100Spec() + spec.params[2].min = 20 + const out = crystallize(spec, [[60, 30]]) + expect(out[2]).toBe(20) + expect(out[0]).toBeCloseTo((60 * 80) / 90, 10) + expect(out[1]).toBeCloseTo((30 * 80) / 90, 10) + expect(out[0] + out[1] + out[2]).toBeCloseTo(100, 10) + }) + + it('les curseurs hors contrainte ne sont jamais renormalisés', () => { + const spec: ParamSpec = { + constraint: 'sum100', + params: [ + { key: 'duree', label: 'Durée (mois)', kind: 'slider', min: 1, max: 24, step: 1 }, + { key: 'bois', label: 'Atelier bois', kind: 'share', min: 0, max: 100, step: 5 }, + { key: 'metal', label: 'Atelier métal', kind: 'share', min: 0, max: 100, step: 5 }, + { key: 'reserve', label: 'Réserve (calculé)', kind: 'share', min: 0, max: 5, step: 1, derived: true }, + ], + } + const out = crystallize(spec, [[12, 60, 30]]) + expect(out[0]).toBe(12) // le curseur passe tel quel + expect(out[3]).toBe(5) + expect(out[1] + out[2] + out[3]).toBeCloseTo(100, 10) + }) + + it('0 vote ⇒ vecteur des baselines (baseline ?? min)', () => { + const spec = sum100Spec() + spec.params[0].baseline = 50 + spec.params[2].baseline = 20 + // params[1] sans baseline ⇒ min (0). + expect(crystallize(spec, [])).toEqual([50, 0, 20]) + }) + + it('0 vote sans aucune baseline ⇒ vecteur des min', () => { + expect(crystallize(sliderSpec(), [])).toEqual([12, 0]) // duree a baseline 12, taux min 0 + }) + + it('sans contrainte : médiane basse simple, aucun derived', () => { + const out = crystallize(sliderSpec(), [[6, 2], [12, 4], [18, 3], [24, 5]]) + expect(out).toEqual([12, 3]) // médiane basse de chaque colonne + }) +}) + +// --------------------------------------------------------------------------- +// computeMyImpact +// --------------------------------------------------------------------------- + +describe('computeMyImpact — carte « Pour moi » (linear-share)', () => { + function impactSpec(): ParamSpec { + const spec = sum100Spec() + spec.impactAttrKey = 'heures/mois' + return spec + } + + it('cas nominal chiffré : quote-parts au prorata de l’attribut déclaré', () => { + // 1 200 € ; valeurs [50, 30, 20] ; moi 10 h, corpus [10, 20, 10] ⇒ Σ 40. + // montants : 600, 360, 240 ; ma quote-part = montant × 10/40. + const result = computeMyImpact(impactSpec(), { amount: 1200 }, [50, 30, 20], 10, [10, 20, 10]) + expect(result).not.toBeNull() + expect(result!.perParam).toEqual([ + { key: 'bois', label: 'Atelier bois', amount: 150 }, + { key: 'metal', label: 'Atelier métal', amount: 90 }, + { key: 'reserve', label: 'Réserve (calculé)', amount: 60 }, + ]) + expect(result!.total).toBeCloseTo(300, 10) + }) + + it('le derived est inclus dans les quote-parts (chaque part compte)', () => { + const result = computeMyImpact(impactSpec(), { amount: 1200 }, [50, 30, 20], 10, [10, 20, 10]) + expect(result!.perParam.map(l => l.key)).toContain('reserve') + }) + + it('null si mon attribut est absent — jamais de chiffre inventé', () => { + expect(computeMyImpact(impactSpec(), { amount: 1200 }, [50, 30, 20], undefined, [10, 20])).toBeNull() + }) + + it('null si Σ des attributs du corpus vaut 0', () => { + expect(computeMyImpact(impactSpec(), { amount: 1200 }, [50, 30, 20], 10, [0, 0, 0])).toBeNull() + expect(computeMyImpact(impactSpec(), { amount: 1200 }, [50, 30, 20], 10, [])).toBeNull() + }) + + it('null hors sum100, sans montant ou sans impactAttrKey', () => { + const noConstraint = sliderSpec() + noConstraint.impactAttrKey = 'heures/mois' + expect(computeMyImpact(noConstraint, { amount: 1200 }, [12, 2.5], 10, [10, 20])).toBeNull() + + expect(computeMyImpact(impactSpec(), {}, [50, 30, 20], 10, [10, 20])).toBeNull() + + const noKey = sum100Spec() // pas d'impactAttrKey + expect(computeMyImpact(noKey, { amount: 1200 }, [50, 30, 20], 10, [10, 20])).toBeNull() + }) + + it('les curseurs hors contrainte n’ont pas de quote-part', () => { + const spec: ParamSpec = { + constraint: 'sum100', + impactAttrKey: 'heures/mois', + params: [ + { key: 'duree', label: 'Durée (mois)', kind: 'slider', min: 1, max: 24, step: 1 }, + { key: 'bois', label: 'Atelier bois', kind: 'share', min: 0, max: 100, step: 5 }, + { key: 'reserve', label: 'Réserve (calculé)', kind: 'share', min: 0, max: 100, step: 1, derived: true }, + ], + } + const result = computeMyImpact(spec, { amount: 1000 }, [12, 60, 40], 5, [5, 5]) + expect(result!.perParam.map(l => l.key)).toEqual(['bois', 'reserve']) + expect(result!.total).toBeCloseTo(500, 10) // (600 + 400) × 5/10 + }) +}) + +// --------------------------------------------------------------------------- +// detectBimodality +// --------------------------------------------------------------------------- + +describe('detectBimodality — heuristique jamais bloquante', () => { + it('bimodal net : deux paquets séparés par un grand écart ⇒ vrai', () => { + // Triées : [10, 11, 12, 60, 61, 62] — étendue 52, gap max 48 > 40 %. + expect(detectBimodality([10, 60, 11, 61, 12, 62])).toBe(true) + }) + + it('distribution uniforme ⇒ faux', () => { + // Gaps réguliers de 10 sur une étendue de 40 : 10 < 16. + expect(detectBimodality([10, 20, 30, 40, 50])).toBe(false) + }) + + it('moins de 4 valeurs ⇒ toujours faux', () => { + expect(detectBimodality([0, 100])).toBe(false) + expect(detectBimodality([0, 50, 100])).toBe(false) + expect(detectBimodality([])).toBe(false) + }) + + it('un simple outlier (1 seule valeur d’un côté du gap) ⇒ faux', () => { + // Gap max 50 entre 0 et 50, mais une seule valeur à gauche. + expect(detectBimodality([0, 50, 51, 52, 53])).toBe(false) + }) + + it('valeurs toutes identiques (étendue nulle) ⇒ faux', () => { + expect(detectBimodality([5, 5, 5, 5])).toBe(false) + }) + + it('deux paires exactement de part et d’autre du gap ⇒ vrai (cas limite n=4)', () => { + expect(detectBimodality([0, 1, 99, 100])).toBe(true) + }) + + it('jamais bloquante : les valeurs non finies sont ignorées, pas d’exception', () => { + expect(() => detectBimodality([Number.NaN, Number.POSITIVE_INFINITY, 1, 2])).not.toThrow() + expect(detectBimodality([Number.NaN, 0, 1, 99, 100])).toBe(true) + }) +}) diff --git a/frontend/tests/engine/settings.spec.ts b/frontend/tests/engine/settings.spec.ts new file mode 100644 index 0000000..df1b110 --- /dev/null +++ b/frontend/tests/engine/settings.spec.ts @@ -0,0 +1,226 @@ +/** + * resolveSettings — the Pact IS the settings store (Δ10). + * Defaults, voted overrides, specified fallbacks, corrupted-bundle resilience. + */ +import { describe, expect, it } from 'vitest' +import { SETTINGS_DEFAULTS, hasConsentProtocol, resolveSettings } from '../../app/engine/settings' +import type { Clause, ClauseVersion, Json } from '../../app/types/domain' + +const T0 = '2026-08-01T00:00:00.000Z' +const T1 = '2026-08-05T00:00:00.000Z' + +let seq = 0 + +function makeClause(settingKey: string, overrides: Partial = {}): Clause { + seq += 1 + return { + id: `clause-${seq}`, + collectiveId: 'col-1', + createdAt: T0, + updatedAt: T0, + docId: 'doc-pact', + section: 'Réglages', + position: seq, + code: `S${seq}`, + title: settingKey, + inertia: 'standard', + settingKey, + ...overrides, + } +} + +function makeVersion( + clauseId: string, + settingValue: Json, + overrides: Partial = {}, +): ClauseVersion { + seq += 1 + return { + id: `version-${seq}`, + collectiveId: 'col-1', + createdAt: T0, + updatedAt: T0, + clauseId, + decisionId: 'dec-founding', + versionLabel: 'v1', + content: 'clause adoptée', + settingValue, + status: 'current', + adoptedAt: T0, + ...overrides, + } +} + +/** Build a Pact from [settingKey, settingValue] pairs and resolve it. */ +function resolveFrom(entries: [string, Json][]) { + const clauses = entries.map(([key]) => makeClause(key)) + const versions = clauses.map((clause, index) => makeVersion(clause.id, entries[index]![1])) + return resolveSettings(clauses, versions) +} + +describe('resolveSettings — les défauts complets', () => { + it('sans aucune clause, tous les seuils de triage prennent leur défaut', () => { + const settings = resolveSettings([], []) + expect(settings.triage).toEqual({ + smallGroupMax: 5, + collectiveMin: 50, + consentMax: 7, + objectionWindowHours: 48, + adviceWindowHours: 72, + framingDays: 14, + concernEscalateRatio: 0.5, + recurrenceThreshold: 3, + reviewDelayDays: 90, + requireEffects: 'binding', + }) + expect(settings.triage).toEqual(SETTINGS_DEFAULTS) + }) + + it('sans aucune clause, les protocoles optionnels restent indéfinis (repli → consent chez l’appelant)', () => { + const settings = resolveSettings([], []) + expect(settings.protocolByRange.nuanced).toBeUndefined() + expect(settings.protocolByRange.large).toBeUndefined() + expect(settings.protocolByRange.parametric).toBeUndefined() + expect(settings.protocolByRange.election).toBeUndefined() + expect(settings.protocolByRange.clauseByInertia).toBeUndefined() + }) +}) + +describe('resolveSettings — une clause adoptée change une valeur', () => { + it('la version courante d’une clause remplace le défaut', () => { + const settings = resolveFrom([['triage.smallGroupMax', 8]]) + expect(settings.triage.smallGroupMax).toBe(8) + expect(settings.triage.collectiveMin).toBe(50) // les autres clés gardent leur défaut + }) + + it('une version seulement proposée ne change rien', () => { + const clause = makeClause('triage.framingDays') + const version = makeVersion(clause.id, 21, { status: 'proposed', adoptedAt: undefined }) + expect(resolveSettings([clause], [version]).triage.framingDays).toBe(14) + }) + + it('une version remplacée ne change rien — seule la courante compte', () => { + const clause = makeClause('triage.consentMax') + const superseded = makeVersion(clause.id, 3, { status: 'superseded' }) + const current = makeVersion(clause.id, 9, { adoptedAt: T1 }) + expect(resolveSettings([clause], [superseded, current]).triage.consentMax).toBe(9) + }) + + it('deux versions courantes (anomalie) — la plus récemment adoptée gagne', () => { + const clause = makeClause('triage.objectionWindowHours') + const older = makeVersion(clause.id, 24, { adoptedAt: T0 }) + const newer = makeVersion(clause.id, 96, { adoptedAt: T1 }) + expect(resolveSettings([clause], [older, newer]).triage.objectionWindowHours).toBe(96) + }) + + it('une clause archivée est ignorée', () => { + const clause = makeClause('triage.smallGroupMax', { archivedAt: T1 }) + const version = makeVersion(clause.id, 12) + expect(resolveSettings([clause], [version]).triage.smallGroupMax).toBe(5) + }) +}) + +describe('resolveSettings — requireEffects résolu', () => { + it('« none » voté est appliqué', () => { + expect(resolveFrom([['triage.requireEffects', 'none']]).triage.requireEffects).toBe('none') + }) + + it('« structural » voté est appliqué', () => { + expect(resolveFrom([['triage.requireEffects', 'structural']]).triage.requireEffects).toBe( + 'structural', + ) + }) + + it('une valeur inconnue retombe sur « binding »', () => { + expect(resolveFrom([['triage.requireEffects', 'everything']]).triage.requireEffects).toBe( + 'binding', + ) + }) +}) + +describe('resolveSettings — le protocole de consentement', () => { + it('la clause protocols.consent résout l’identifiant', () => { + const settings = resolveFrom([['protocols.consent', 'proto-consent-1']]) + expect(settings.protocolByRange.consent).toBe('proto-consent-1') + expect(hasConsentProtocol(settings)).toBe(true) + }) + + it('consent introuvable (bundle corrompu) — objet complet, consent vide, jamais de crash', () => { + expect(() => resolveSettings([], [])).not.toThrow() + const settings = resolveSettings([], []) + expect(settings.protocolByRange.consent).toBe('') + expect(hasConsentProtocol(settings)).toBe(false) + expect(settings.triage.smallGroupMax).toBe(5) // le reste de l’objet est utilisable + }) + + it('consent d’un mauvais type compte comme introuvable', () => { + const settings = resolveFrom([['protocols.consent', 42]]) + expect(settings.protocolByRange.consent).toBe('') + expect(hasConsentProtocol(settings)).toBe(false) + }) +}) + +describe('resolveSettings — les protocoles optionnels et l’héritage Ğ1', () => { + it('nuanced, large, parametric et election se résolvent chacun', () => { + const settings = resolveFrom([ + ['protocols.consent', 'p-consent'], + ['protocols.nuanced', 'p-nuanced'], + ['protocols.large', 'p-large'], + ['protocols.parametric', 'p-parametric'], + ['protocols.election', 'p-election'], + ]) + expect(settings.protocolByRange.nuanced).toBe('p-nuanced') + expect(settings.protocolByRange.large).toBe('p-large') + expect(settings.protocolByRange.parametric).toBe('p-parametric') + expect(settings.protocolByRange.election).toBe('p-election') + }) + + it('clauseByInertia complet (4 presets) est exposé', () => { + const settings = resolveFrom([ + ['protocols.clauseByInertia.low', 'p-low'], + ['protocols.clauseByInertia.standard', 'p-standard'], + ['protocols.clauseByInertia.high', 'p-high'], + ['protocols.clauseByInertia.max', 'p-max'], + ]) + expect(settings.protocolByRange.clauseByInertia).toEqual({ + low: 'p-low', + standard: 'p-standard', + high: 'p-high', + max: 'p-max', + }) + }) + + it('clauseByInertia partiel reste indéfini — l’appelant retombe sur consent', () => { + const settings = resolveFrom([ + ['protocols.clauseByInertia.low', 'p-low'], + ['protocols.clauseByInertia.standard', 'p-standard'], + ['protocols.clauseByInertia.high', 'p-high'], + ]) + expect(settings.protocolByRange.clauseByInertia).toBeUndefined() + }) +}) + +describe('resolveSettings — les valeurs invalides retombent sur les défauts, en silence', () => { + it('un mauvais type sur une clé numérique est ignoré', () => { + expect(resolveFrom([['triage.smallGroupMax', 'huit']]).triage.smallGroupMax).toBe(5) + }) + + it('un nombre négatif est ignoré', () => { + expect(resolveFrom([['triage.framingDays', -3]]).triage.framingDays).toBe(14) + }) + + it('un ratio négatif est ignoré', () => { + expect(resolveFrom([['triage.concernEscalateRatio', -0.5]]).triage.concernEscalateRatio).toBe( + 0.5, + ) + }) + + it('null est ignoré', () => { + expect(resolveFrom([['triage.reviewDelayDays', null]]).triage.reviewDelayDays).toBe(90) + }) + + it('un identifiant de protocole vide compte comme non résolu', () => { + const settings = resolveFrom([['protocols.nuanced', ' ']]) + expect(settings.protocolByRange.nuanced).toBeUndefined() + }) +}) diff --git a/frontend/tests/engine/state.spec.ts b/frontend/tests/engine/state.spec.ts new file mode 100644 index 0000000..a6ad591 --- /dev/null +++ b/frontend/tests/engine/state.spec.ts @@ -0,0 +1,623 @@ +/** + * canTransition — the ONE state machine of the decision (Δ29). + * Every guard (a–i) tested with positive AND negative cases + windowOutcome. + */ +import { describe, expect, it } from 'vitest' +import { TRANSITIONS, canTransition, windowOutcome } from '../../app/engine/state' +import type { TransitionContext } from '../../app/engine/state' +import type { + Assent, + CollectiveSettings, + Concern, + Decision, + Objection, + ParamSpec, + VoteSession, +} from '../../app/types/domain' + +const T0 = '2026-08-01T00:00:00.000Z' +const NOW = '2026-08-11T12:00:00.000Z' + +let seq = 0 + +function makeEntity() { + seq += 1 + return { id: `id-${seq}`, collectiveId: 'col-1', createdAt: T0, updatedAt: T0 } +} + +function makeSettings(overrides: Partial = {}): CollectiveSettings { + return { + triage: { + smallGroupMax: 5, + collectiveMin: 50, + consentMax: 7, + objectionWindowHours: 48, + adviceWindowHours: 72, + framingDays: 14, + concernEscalateRatio: 0.5, + recurrenceThreshold: 3, + reviewDelayDays: 90, + requireEffects: 'binding', + ...overrides, + }, + protocolByRange: { consent: 'proto-consent' }, + } +} + +function makeDecision(overrides: Partial = {}): Decision { + return { + ...makeEntity(), + id: 'dec-1', + authorId: 'p-author', + title: 'Décision de test', + tags: [], + reversibility: 'easy', + weight: 'light', + urgent: false, + scope: { selfOnly: false, circleIds: ['circle-1'], personIds: [] }, + route: 'collective', + triageRule: 'R5', + routeOverridden: false, + status: 'draft', + stewardIds: [], + measurerIds: [], + visibility: 'scope', + ...overrides, + } +} + +function makeConcern(origin: Concern['origin'], overrides: Partial = {}): Concern { + const entity = makeEntity() + return { + ...entity, + decisionId: 'dec-1', + personId: `p-${entity.id}`, + origin, + reason: 'membre du cercle', + beforeSnapshot: true, + ...overrides, + } +} + +function makeAssent(personId: string, overrides: Partial = {}): Assent { + return { ...makeEntity(), decisionId: 'dec-1', personId, ...overrides } +} + +function makeObjection(status: Objection['status'], overrides: Partial = {}): Objection { + return { + ...makeEntity(), + decisionId: 'dec-1', + personId: 'p-objector', + kind: 'content', + argument: 'Je maintiens mon désaccord.', + status, + ...overrides, + } +} + +function makeSession(overrides: Partial = {}): VoteSession { + return { + ...makeEntity(), + decisionId: 'dec-1', + protocolId: 'proto-1', + corpusPersonIds: ['p-1', 'p-2', 'p-3'], + corpusSize: 3, + opensAt: T0, + closesAt: NOW, + status: 'open', + ...overrides, + } +} + +function makeCtx(overrides: Partial = {}): TransitionContext { + return { + concerns: [], + settings: makeSettings(), + assents: [], + objections: [], + now: NOW, + ...overrides, + } +} + +const PARAM_SPEC: ParamSpec = { + params: [ + { key: 'partA', label: 'Part ateliers', kind: 'share', min: 0, max: 100, step: 1 }, + { key: 'partB', label: 'Part réserve', kind: 'share', min: 0, max: 100, step: 1, derived: true }, + ], + constraint: 'sum100', +} + +/** 4 computed + 2 declared ⇒ ratio 0.5 atteint (2 ≥ 0.5 × 4). */ +function influxConcerns(): Concern[] { + return [ + makeConcern('computed'), + makeConcern('computed'), + makeConcern('computed'), + makeConcern('computed'), + makeConcern('declared'), + makeConcern('declared'), + ] +} + +describe('La table des transitions', () => { + it('draft ouvre les sept chemins', () => { + expect(TRANSITIONS.draft).toEqual([ + 'advice', + 'objection', + 'framing', + 'voting', + 'adopted', + 'transmitted', + 'rejected', + ]) + }) + + it('chaque état de travail connaît ses sorties', () => { + expect(TRANSITIONS.advice).toEqual(['adopted', 'voting']) + expect(TRANSITIONS.objection).toEqual(['adopted', 'framing', 'voting']) + expect(TRANSITIONS.framing).toEqual(['voting', 'closed']) + expect(TRANSITIONS.voting).toEqual(['adopted', 'rejected']) + expect(TRANSITIONS.adopted).toEqual(['revoked', 'closed']) + }) + + it('les états terminaux n’ont aucune sortie', () => { + expect(TRANSITIONS.rejected).toBeUndefined() + expect(TRANSITIONS.revoked).toBeUndefined() + expect(TRANSITIONS.closed).toBeUndefined() + expect(TRANSITIONS.transmitted).toBeUndefined() + }) +}) + +describe('Garde a — la transition inconnue est refusée', () => { + it('adopted → voting n’existe pas', () => { + const result = canTransition(makeDecision({ status: 'adopted' }), 'voting', makeCtx()) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason.length).toBeGreaterThan(0) + }) + + it('rejected est terminal — aucune sortie', () => { + expect(canTransition(makeDecision({ status: 'rejected' }), 'adopted', makeCtx()).ok).toBe(false) + }) + + it('draft → advice existe', () => { + const decision = makeDecision({ status: 'draft', route: 'advice' }) + expect(canTransition(decision, 'advice', makeCtx()).ok).toBe(true) + }) + + it('le retour explicite d’une fenêtre à son propre état est permis', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate' }) + expect(canTransition(decision, 'objection', makeCtx()).ok).toBe(true) + }) + + it('un état hors fenêtre ne boucle pas sur lui-même', () => { + expect(canTransition(makeDecision({ status: 'voting' }), 'voting', makeCtx()).ok).toBe(false) + }) +}) + +describe('Garde b — la frontière contestée suspend toute sortie', () => { + it('une fenêtre d’objection suspendue ne peut pas adopter', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', windowSuspendedAt: NOW }) + const result = canTransition(decision, 'adopted', makeCtx()) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('frontière') + }) + + it('une fenêtre d’avis suspendue ne peut pas non plus escalader vers le vote', () => { + const decision = makeDecision({ status: 'advice', route: 'advice', windowSuspendedAt: NOW }) + expect(canTransition(decision, 'voting', makeCtx()).ok).toBe(false) + }) + + it('le retour explicite au même état reste possible pendant la suspension', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', windowSuspendedAt: NOW }) + expect(canTransition(decision, 'objection', makeCtx()).ok).toBe(true) + }) + + it('sans suspension, la fenêtre s’adopte normalement', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate' }) + expect(canTransition(decision, 'adopted', makeCtx()).ok).toBe(true) + }) + + it('la frontière prime sur l’affluence (ordre des gardes)', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', windowSuspendedAt: NOW }) + const result = canTransition(decision, 'adopted', makeCtx({ concerns: influxConcerns() })) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('frontière') + }) +}) + +describe('Garde c — l’affluence exige un traitement du périmètre', () => { + it('ratio atteint sans scopeKeptNote — la session ne se clôt pas', () => { + const decision = makeDecision({ status: 'voting' }) + const result = canTransition(decision, 'adopted', makeCtx({ concerns: influxConcerns() })) + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toBe( + 'Le périmètre déborde — élargis d’un cran ou motive publiquement son maintien.', + ) + } + }) + + it('le maintien motivé publiquement (scopeKeptNote) débloque la clôture', () => { + const decision = makeDecision({ + status: 'voting', + scopeKeptNote: 'Le cercle Ateliers reste le bon périmètre : le budget est le sien.', + }) + expect(canTransition(decision, 'adopted', makeCtx({ concerns: influxConcerns() })).ok).toBe(true) + }) + + it('sous le ratio, la clôture passe', () => { + const concerns = [ + makeConcern('computed'), + makeConcern('computed'), + makeConcern('computed'), + makeConcern('computed'), + makeConcern('declared'), + ] + const decision = makeDecision({ status: 'voting' }) + expect(canTransition(decision, 'adopted', makeCtx({ concerns })).ok).toBe(true) + }) + + it('sans concerné calculé, la garde ne se déclenche jamais', () => { + const concerns = [makeConcern('declared'), makeConcern('declared'), makeConcern('declared')] + const decision = makeDecision({ status: 'voting' }) + expect(canTransition(decision, 'adopted', makeCtx({ concerns })).ok).toBe(true) + }) + + it('la garde vaut aussi pour la fenêtre d’avis', () => { + const decision = makeDecision({ status: 'advice', route: 'advice' }) + expect(canTransition(decision, 'adopted', makeCtx({ concerns: influxConcerns() })).ok).toBe(false) + }) + + it('la garde vaut pour le rejet d’un vote — clore, c’est clore', () => { + const decision = makeDecision({ status: 'voting' }) + expect(canTransition(decision, 'rejected', makeCtx({ concerns: influxConcerns() })).ok).toBe(false) + }) + + it('les concernés d’une autre décision ne comptent pas', () => { + const concerns = influxConcerns().map((concern) => ({ ...concern, decisionId: 'dec-2' })) + const decision = makeDecision({ status: 'voting' }) + expect(canTransition(decision, 'adopted', makeCtx({ concerns })).ok).toBe(true) + }) + + it('l’escalade vers framing ou voting reste ouverte — c’est le traitement, pas la clôture', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate' }) + expect(canTransition(decision, 'framing', makeCtx({ concerns: influxConcerns() })).ok).toBe(true) + }) +}) + +describe('Garde d — la matière (requireEffects) à l’ouverture de session collective', () => { + const resources = { note: 'Deux heures par semaine pendant un mois' } + + it('« binding » + poids binding sans effet recherché — refus', () => { + const decision = makeDecision({ status: 'draft', weight: 'binding', resources }) + const result = canTransition(decision, 'voting', makeCtx()) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('effet recherché') + }) + + it('« binding » + poids binding avec un effet recherché — ouverture', () => { + const decision = makeDecision({ + status: 'draft', + weight: 'binding', + resources, + brief: { effects: [{ label: 'Réduire le temps de réunion' }] }, + }) + expect(canTransition(decision, 'voting', makeCtx()).ok).toBe(true) + }) + + it('« binding » + poids structural sans cible mesurable — refus', () => { + const decision = makeDecision({ + status: 'draft', + weight: 'structural', + resources, + brief: { effects: [{ label: 'Assainir le budget' }] }, + }) + const result = canTransition(decision, 'voting', makeCtx()) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('cible') + }) + + it('« binding » + poids structural avec une cible — ouverture', () => { + const decision = makeDecision({ + status: 'draft', + weight: 'structural', + resources, + brief: { effects: [{ label: 'Assainir le budget', target: '≤ 400 € par mois' }] }, + }) + expect(canTransition(decision, 'voting', makeCtx()).ok).toBe(true) + }) + + it('« structural » : le poids binding n’est pas sous garde', () => { + const decision = makeDecision({ status: 'draft', weight: 'binding', resources }) + const ctx = makeCtx({ settings: makeSettings({ requireEffects: 'structural' }) }) + expect(canTransition(decision, 'voting', ctx).ok).toBe(true) + }) + + it('« structural » : le poids structural sans effet est refusé', () => { + const decision = makeDecision({ status: 'draft', weight: 'structural', resources }) + const ctx = makeCtx({ settings: makeSettings({ requireEffects: 'structural' }) }) + expect(canTransition(decision, 'voting', ctx).ok).toBe(false) + }) + + it('« none » : aucune exigence de matière, même structural', () => { + const decision = makeDecision({ status: 'draft', weight: 'structural', resources }) + const ctx = makeCtx({ settings: makeSettings({ requireEffects: 'none' }) }) + expect(canTransition(decision, 'voting', ctx).ok).toBe(true) + }) + + it('le poids light n’est jamais soumis à la matière', () => { + const decision = makeDecision({ status: 'draft', weight: 'light' }) + expect(canTransition(decision, 'voting', makeCtx()).ok).toBe(true) + }) + + it('la garde est réservée à la route collective', () => { + const decision = makeDecision({ status: 'draft', route: 'mandate', weight: 'binding', resources }) + expect(canTransition(decision, 'voting', makeCtx()).ok).toBe(true) + }) + + it('framing → voting est gardé de la même façon', () => { + const decision = makeDecision({ status: 'framing', weight: 'structural', resources }) + expect(canTransition(decision, 'voting', makeCtx()).ok).toBe(false) + }) +}) + +describe('Garde e — « Ce que ça engage » à l’ouverture de fenêtre ou de session', () => { + it('poids binding sans note de ressources — la fenêtre d’objection ne s’ouvre pas', () => { + const decision = makeDecision({ status: 'draft', route: 'mandate', weight: 'binding' }) + const result = canTransition(decision, 'objection', makeCtx()) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('engage') + }) + + it('avec la note, la fenêtre s’ouvre', () => { + const decision = makeDecision({ + status: 'draft', + route: 'mandate', + weight: 'binding', + resources: { note: 'Une demi-journée de l’équipe accueil' }, + }) + expect(canTransition(decision, 'objection', makeCtx()).ok).toBe(true) + }) + + it('une note blanche ne compte pas', () => { + const decision = makeDecision({ + status: 'draft', + route: 'mandate', + weight: 'binding', + resources: { note: ' ' }, + }) + expect(canTransition(decision, 'objection', makeCtx()).ok).toBe(false) + }) + + it('le poids light ouvre sans note', () => { + const decision = makeDecision({ status: 'draft', route: 'advice', weight: 'light' }) + expect(canTransition(decision, 'advice', makeCtx()).ok).toBe(true) + }) + + it('la route solo est exemptée', () => { + const decision = makeDecision({ status: 'draft', route: 'solo', weight: 'binding' }) + expect(canTransition(decision, 'advice', makeCtx()).ok).toBe(true) + }) + + it('la garde vit aussi à l’ouverture de session — après la matière', () => { + const decision = makeDecision({ + status: 'draft', + weight: 'binding', + brief: { effects: [{ label: 'Un effet recherché' }] }, + }) + const result = canTransition(decision, 'voting', makeCtx()) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('engage') + }) +}) + +describe('Garde f — l’accord explicite (Assent) hors du réversible', () => { + it('easy : le silence vaut accord, la fenêtre s’adopte sans Assent', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', reversibility: 'easy' }) + expect(canTransition(decision, 'adopted', makeCtx()).ok).toBe(true) + }) + + it('costly sans Assent — la fenêtre se prolonge', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', reversibility: 'costly' }) + const result = canTransition(decision, 'adopted', makeCtx()) + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toBe('Il manque un accord explicite — la fenêtre se prolonge.') + } + }) + + it('costly : l’accord de l’auteur seul ne suffit pas', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', reversibility: 'costly' }) + const ctx = makeCtx({ assents: [makeAssent('p-author')] }) + expect(canTransition(decision, 'adopted', ctx).ok).toBe(false) + }) + + it('costly : un accord d’un tiers concerné débloque l’adoption', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', reversibility: 'costly' }) + const ctx = makeCtx({ assents: [makeAssent('p-other')] }) + expect(canTransition(decision, 'adopted', ctx).ok).toBe(true) + }) + + it('irreversible sans Assent — refus aussi', () => { + const decision = makeDecision({ + status: 'objection', + route: 'mandate', + reversibility: 'irreversible', + }) + expect(canTransition(decision, 'adopted', makeCtx()).ok).toBe(false) + }) + + it('l’Assent d’une autre décision ne compte pas', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', reversibility: 'costly' }) + const ctx = makeCtx({ assents: [makeAssent('p-other', { decisionId: 'dec-2' })] }) + expect(canTransition(decision, 'adopted', ctx).ok).toBe(false) + }) +}) + +describe('Garde g — jamais d’adoption sur une objection ouverte', () => { + it('une objection ouverte bloque l’adoption, même en easy', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', reversibility: 'easy' }) + const result = canTransition(decision, 'adopted', makeCtx({ objections: [makeObjection('open')] })) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('objection') + }) + + it('une objection retirée ne bloque plus', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', reversibility: 'easy' }) + const ctx = makeCtx({ objections: [makeObjection('withdrawn')] }) + expect(canTransition(decision, 'adopted', ctx).ok).toBe(true) + }) + + it('une objection intégrée ne bloque plus', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', reversibility: 'easy' }) + const ctx = makeCtx({ objections: [makeObjection('integrated')] }) + expect(canTransition(decision, 'adopted', ctx).ok).toBe(true) + }) + + it('l’Assent d’un tiers ne contourne pas une objection ouverte', () => { + const decision = makeDecision({ status: 'objection', route: 'mandate', reversibility: 'costly' }) + const ctx = makeCtx({ assents: [makeAssent('p-other')], objections: [makeObjection('open')] }) + const result = canTransition(decision, 'adopted', ctx) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('objection') + }) +}) + +describe('Garde h — la clôture du dossier découpé', () => { + const parent = () => makeDecision({ id: 'dossier-1', status: 'framing' }) + + function element(status: Decision['status'], overrides: Partial = {}): Decision { + const entity = makeEntity() + return makeDecision({ + id: entity.id, + parentDecisionId: 'dossier-1', + chainKind: 'element', + status, + ...overrides, + }) + } + + it('sans élément, le dossier ne se clôt pas', () => { + const result = canTransition(parent(), 'closed', makeCtx({ children: [] })) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('dossier') + }) + + it('tous les éléments terminaux — le garant peut clore', () => { + const children = [ + element('adopted'), + element('rejected'), + element('revoked'), + element('closed'), + element('transmitted'), + ] + expect(canTransition(parent(), 'closed', makeCtx({ children })).ok).toBe(true) + }) + + it('un élément encore en vote retient le dossier', () => { + const children = [element('adopted'), element('voting')] + const result = canTransition(parent(), 'closed', makeCtx({ children })) + expect(result.ok).toBe(false) + if (!result.ok) expect(result.reason).toContain('en cours') + }) + + it('des enfants non-éléments ne font pas un dossier', () => { + const children = [element('adopted', { chainKind: 'revision' })] + expect(canTransition(parent(), 'closed', makeCtx({ children })).ok).toBe(false) + }) + + it('l’élément d’un autre dossier ne compte pas', () => { + const children = [element('adopted', { parentDecisionId: 'dossier-2' })] + expect(canTransition(parent(), 'closed', makeCtx({ children })).ok).toBe(false) + }) + + it('framing → voting du parent reste possible pendant que le dossier vit', () => { + const decision = makeDecision({ id: 'dossier-1', status: 'framing', weight: 'light' }) + expect(canTransition(decision, 'voting', makeCtx({ children: [element('voting')] })).ok).toBe(true) + }) +}) + +describe('Garde i — la cristallisation attend le geste du garant', () => { + const parametricDecision = () => makeDecision({ status: 'voting', paramSpec: PARAM_SPEC }) + + it('session figée — pas d’adoption sans le geste', () => { + const ctx = makeCtx({ session: makeSession({ status: 'frozen' }) }) + const result = canTransition(parametricDecision(), 'adopted', ctx) + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toBe('Les votes sont figés — la cristallisation attend son geste.') + } + }) + + it('session close sans crystallizedById — le moteur ne cristallise jamais', () => { + const ctx = makeCtx({ session: makeSession({ status: 'closed' }) }) + expect(canTransition(parametricDecision(), 'adopted', ctx).ok).toBe(false) + }) + + it('session close et geste daté — l’adoption passe', () => { + const ctx = makeCtx({ + session: makeSession({ status: 'closed', crystallizedById: 'p-steward', crystallizedAt: NOW }), + }) + expect(canTransition(parametricDecision(), 'adopted', ctx).ok).toBe(true) + }) + + it('le rejet (quorum non atteint) exige le même geste', () => { + const ctx = makeCtx({ session: makeSession({ status: 'frozen' }) }) + expect(canTransition(parametricDecision(), 'rejected', ctx).ok).toBe(false) + }) + + it('le rejet constaté au geste passe', () => { + const ctx = makeCtx({ + session: makeSession({ status: 'closed', crystallizedById: 'p-steward', crystallizedAt: NOW }), + }) + expect(canTransition(parametricDecision(), 'rejected', ctx).ok).toBe(true) + }) + + it('une session figée trahit le paramétrique même sans paramSpec', () => { + const decision = makeDecision({ status: 'voting' }) + const ctx = makeCtx({ session: makeSession({ status: 'frozen' }) }) + expect(canTransition(decision, 'adopted', ctx).ok).toBe(false) + }) + + it('une session non paramétrique se clôt automatiquement — pas de geste exigé', () => { + const decision = makeDecision({ status: 'voting' }) + const ctx = makeCtx({ session: makeSession({ status: 'closed' }) }) + expect(canTransition(decision, 'adopted', ctx).ok).toBe(true) + }) + + it('sans session dans le contexte, la garde ne s’applique pas', () => { + expect(canTransition(makeDecision({ status: 'voting' }), 'adopted', makeCtx()).ok).toBe(true) + }) +}) + +describe('windowOutcome — l’échéance des fenêtres d’objection', () => { + it('easy — le silence vaut accord, la fenêtre s’adopte', () => { + const decision = makeDecision({ status: 'objection', reversibility: 'easy' }) + expect(windowOutcome(decision, makeCtx())).toBe('adopt') + }) + + it('costly sans accord tiers — la fenêtre se prolonge d’un cran', () => { + const decision = makeDecision({ status: 'objection', reversibility: 'costly' }) + expect(windowOutcome(decision, makeCtx())).toBe('extend') + }) + + it('costly avec l’accord d’un tiers — la fenêtre s’adopte', () => { + const decision = makeDecision({ status: 'objection', reversibility: 'costly' }) + expect(windowOutcome(decision, makeCtx({ assents: [makeAssent('p-other')] }))).toBe('adopt') + }) + + it('l’accord de l’auteur seul ne compte pas', () => { + const decision = makeDecision({ status: 'objection', reversibility: 'costly' }) + expect(windowOutcome(decision, makeCtx({ assents: [makeAssent('p-author')] }))).toBe('extend') + }) + + it('frontière suspendue — la fenêtre attend, même en easy', () => { + const decision = makeDecision({ + status: 'objection', + reversibility: 'easy', + windowSuspendedAt: NOW, + }) + expect(windowOutcome(decision, makeCtx())).toBe('wait') + }) +}) diff --git a/frontend/tests/engine/triage.spec.ts b/frontend/tests/engine/triage.spec.ts new file mode 100644 index 0000000..f447705 --- /dev/null +++ b/frontend/tests/engine/triage.spec.ts @@ -0,0 +1,608 @@ +/** + * Tests du moteur de routage — « Le chemin ». + * Ordre des règles vérifié : R-U, R0a, R0b, R0c, R2, R3, R4, R5, R6. + * Fonctions pures : fixtures assemblées à la main, aucun store. + */ +import { describe, expect, it } from 'vitest' +import { detectParametricHint, triage } from '../../app/engine/triage' +import { + BINARY_COST, + PARAMETRIC_ALT, + RECORD_ALT, + URGENT_REFUSED, +} from '../../app/lexicon' +import type { + Clause, + CollectiveSettings, + Mandate, + TriageContext, + TriageInput, +} from '../../app/types/domain' + +const T0 = '2026-01-01T00:00:00Z' +const T9 = '2027-01-01T00:00:00Z' + +function mkSettings(over: { + triage?: Partial + protocolByRange?: Partial +} = {}): CollectiveSettings { + return { + triage: { + smallGroupMax: 5, + collectiveMin: 50, + consentMax: 7, + objectionWindowHours: 48, + adviceWindowHours: 72, + framingDays: 14, + concernEscalateRatio: 0.5, + recurrenceThreshold: 3, + reviewDelayDays: 90, + requireEffects: 'binding', + ...over.triage, + }, + protocolByRange: { consent: 'proto-consent', ...over.protocolByRange }, + } +} + +function mkInput(over: Partial = {}): TriageInput { + return { + title: 'Repeindre la salle', + tags: [], + scope: { selfOnly: false, circleIds: [], personIds: [] }, + reversibility: 'costly', + weight: 'light', + urgent: false, + ...over, + } +} + +function mkCtx(over: Partial = {}): TriageContext { + return { + myActiveMandates: [], + otherActiveMandates: [], + matchingClauses: [], + similarRecentCount: 0, + similarRecordedCount: 0, + computedConcernedIds: ['p1', 'p2', 'p3'], + ...over, + } +} + +function mkMandate(over: Partial = {}): Mandate { + return { + id: 'm1', + collectiveId: 'col', + createdAt: T0, + updatedAt: T0, + title: 'Modération', + holderId: 'p-holder', + originDecisionId: 'd0', + domain: { circleIds: ['c1', 'c2'], tags: [] }, + startsAt: T0, + endsAt: T9, + electorCircleId: 'c1', + nominationMethod: 'consent', + reports: [], + status: 'active', + ...over, + } +} + +function mkClause(over: Partial = {}): Clause { + return { + id: 'cl1', + collectiveId: 'col', + createdAt: T0, + updatedAt: T0, + docId: 'doc1', + section: 'E', + position: 9, + code: 'E9', + title: 'Acte Forgeron', + inertia: 'high', + ...over, + } +} + +const scoped = (circleIds: string[]): TriageInput['scope'] => + ({ selfOnly: false, circleIds, personIds: [] }) + +describe('detectParametricHint — nombre, %, montant dans la phrase', () => { + it('détecte un montant en euros', () => { + expect(detectParametricHint('Fixer la cotisation à 25 €')).toBe(true) + }) + + it('ne détecte rien dans une phrase sans chiffre', () => { + expect(detectParametricHint('Repeindre la salle')).toBe(false) + }) + + it('détecte un pourcentage', () => { + expect(detectParametricHint('Porter la part commune à douze %')).toBe(true) + }) + + it('détecte l\'unité DU en majuscules', () => { + expect(detectParametricHint('Verser un DU aux nouveaux venus')).toBe(true) + }) + + it('ne confond pas l\'article « du » avec l\'unité DU', () => { + expect(detectParametricHint('Changer du mobilier dans la salle')).toBe(false) + }) +}) + +describe('R-U — urgence', () => { + it('conservatoire : route sous-jacente calculée sans urgence, fenêtre doublée, chaîne posée', () => { + // Sans urgence, ce cas serait R4 (easy, 3 ≤ 5, fenêtre 72 h). + const verdict = triage( + mkInput({ urgent: true, reversibility: 'easy' }), + mkCtx(), + mkSettings(), + ) + + expect(verdict.rule).toBe('R-U') + expect(verdict.route).toBe('advice') + expect(verdict.conservatoryChain).toBe(true) + expect(verdict.windowHours).toBe(144) + expect(verdict.explanation).toBe('Décidé en urgence — le collectif ratifie.') + }) + + it('urgence × irréversible : refus — route normale, fenêtres minimales (jamais doublées)', () => { + // Sans urgence, ce cas serait R0b (mandat couvrant, fenêtre 48 h). + const verdict = triage( + mkInput({ urgent: true, reversibility: 'irreversible', scope: scoped(['c1']) }), + mkCtx({ myActiveMandates: [mkMandate()] }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R-U') + expect(verdict.route).toBe('mandate') + expect(verdict.explanation).toBe(URGENT_REFUSED) + expect(verdict.conservatoryChain).toBeUndefined() + expect(verdict.windowHours).toBe(48) + }) +}) + +describe('R0a — déjà décidé', () => { + it('clause en vigueur trouvée : consigner en principal, agir ou contester', () => { + const verdict = triage(mkInput(), mkCtx({ matchingClauses: [mkClause()] }), mkSettings()) + + expect(verdict.rule).toBe('R0a') + expect(verdict.route).toBe('record') + expect(verdict.explanation).toBe( + 'C\'est déjà décidé (E9, Acte Forgeron) — agis, ou conteste la règle.', + ) + expect(verdict.windowHours).toBeUndefined() + }) + + it('limite : viser explicitement la clause (amendsClauseId) passe à R3, pas R0a', () => { + // Choix documenté : contester la règle EST R3 — R0a ne bloque pas l'amendement. + const verdict = triage( + mkInput({ amendsClauseId: 'cl1' }), + mkCtx({ matchingClauses: [mkClause()] }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R3') + expect(verdict.route).toBe('collective') + }) +}) + +describe('R0b — mon mandat couvre', () => { + it('périmètre inclus dans le domaine du mandat : décide, c\'est tracé', () => { + const verdict = triage( + mkInput({ scope: scoped(['c1']) }), + mkCtx({ myActiveMandates: [mkMandate()] }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R0b') + expect(verdict.route).toBe('mandate') + expect(verdict.windowHours).toBe(48) + expect(verdict.explanation).toBe('Ton mandat Modération couvre — décide, c\'est tracé.') + }) + + it('limite : couverture partielle du périmètre ne suffit pas', () => { + const verdict = triage( + mkInput({ scope: scoped(['c1', 'c3']) }), + mkCtx({ myActiveMandates: [mkMandate({ domain: { circleIds: ['c1', 'c2'], tags: [] } })] }), + mkSettings(), + ) + + expect(verdict.rule).not.toBe('R0b') + }) + + it('ordre : selfOnly avec mandat couvrant ⇒ R0b gagne, car R0b précède R2', () => { + // Choix documenté : le devoir de trace du mandat prime sur le solo. + const verdict = triage( + mkInput({ scope: { selfOnly: true, circleIds: ['c1'], personIds: [] } }), + mkCtx({ myActiveMandates: [mkMandate()] }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R0b') + expect(verdict.route).toBe('mandate') + }) + + it('limite : périmètre sans cercle ⇒ jamais de couverture triviale', () => { + const verdict = triage( + mkInput({ scope: scoped([]) }), + mkCtx({ myActiveMandates: [mkMandate()] }), + mkSettings(), + ) + + expect(verdict.rule).not.toBe('R0b') + }) +}) + +describe('R0c — le mandat d\'un autre couvre', () => { + it('transmets à sa ou son titulaire — le titre du mandat nomme le pouvoir', () => { + const verdict = triage( + mkInput({ scope: scoped(['c1']) }), + mkCtx({ otherActiveMandates: [mkMandate({ holderId: 'p-other' })] }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R0c') + expect(verdict.route).toBe('transmit') + expect(verdict.explanation).toBe( + 'Le mandat Modération couvre — transmets à sa ou son titulaire.', + ) + }) + + it('ordre : mon mandat gagne sur celui d\'un autre (R0b avant R0c)', () => { + const verdict = triage( + mkInput({ scope: scoped(['c1']) }), + mkCtx({ + myActiveMandates: [mkMandate({ id: 'm-me', title: 'Trésorerie' })], + otherActiveMandates: [mkMandate({ id: 'm-other', holderId: 'p-other' })], + }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R0b') + }) + + it('limite : un mandat expiré ne couvre pas', () => { + const verdict = triage( + mkInput({ scope: scoped(['c1']) }), + mkCtx({ otherActiveMandates: [mkMandate({ status: 'expired' })] }), + mkSettings(), + ) + + expect(verdict.rule).not.toBe('R0c') + }) +}) + +describe('R2 — moi seul', () => { + it('selfOnly : décide, zéro fenêtre', () => { + const verdict = triage( + mkInput({ scope: { selfOnly: true, circleIds: [], personIds: [] } }), + mkCtx({ computedConcernedIds: [] }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R2') + expect(verdict.route).toBe('solo') + expect(verdict.explanation).toBe('Personne d\'autre n\'est concerné — décide.') + expect(verdict.windowHours).toBeUndefined() + expect(verdict.reviewRequired).toBe(false) + }) + + it('limite : irréversible sur soi ⇒ revoyure suggérée (pré-coche retirable côté UI)', () => { + // Le moteur suggère (reviewRequired) ; la pré-coche retirable est un fait + // d'UI — sur soi, l'outil ne s'interpose jamais. + const verdict = triage( + mkInput({ + scope: { selfOnly: true, circleIds: [], personIds: [] }, + reversibility: 'irreversible', + }), + mkCtx({ computedConcernedIds: [] }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R2') + expect(verdict.reviewRequired).toBe(true) + }) +}) + +describe('R3 — clause visée', () => { + const clauseSettings = mkSettings({ + protocolByRange: { + clauseByInertia: { + low: 'proto-low', + standard: 'proto-standard', + high: 'proto-high', + max: 'proto-max', + }, + }, + }) + + it('l\'inertie de la clause choisit le protocole', () => { + const verdict = triage( + mkInput({ amendsClauseId: 'cl1' }), + mkCtx({ matchingClauses: [mkClause({ inertia: 'high' })] }), + clauseSettings, + ) + + expect(verdict.rule).toBe('R3') + expect(verdict.route).toBe('collective') + expect(verdict.protocolId).toBe('proto-high') + expect(verdict.explanation).toBe( + 'Tu proposes une version de E9 — son inertie s\'applique : vote de ceux qu\'elle gouverne.', + ) + }) + + it('repli : sans clauseByInertia, le consentement s\'applique', () => { + const verdict = triage( + mkInput({ amendsClauseId: 'cl1' }), + mkCtx({ matchingClauses: [mkClause()] }), + mkSettings(), + ) + + expect(verdict.protocolId).toBe('proto-consent') + }) + + it('poids structurel ⇒ fenêtre de formulation posée', () => { + const verdict = triage( + mkInput({ amendsClauseId: 'cl1', weight: 'structural' }), + mkCtx({ matchingClauses: [mkClause()] }), + clauseSettings, + ) + + expect(verdict.framingDays).toBe(14) + }) + + it('limite : clause introuvable dans l\'index ⇒ consentement et phrase générique', () => { + const verdict = triage( + mkInput({ amendsClauseId: 'cl-inconnue' }), + mkCtx({ matchingClauses: [] }), + clauseSettings, + ) + + expect(verdict.rule).toBe('R3') + expect(verdict.protocolId).toBe('proto-consent') + expect(verdict.explanation).toBe( + 'Tu proposes une version de cette règle — son inertie s\'applique : vote de ceux qu\'elle gouverne.', + ) + }) +}) + +describe('R4 — petit cercle réversible', () => { + it('réversible et 3 personnes : demande leur avis puis décide (72 h)', () => { + const verdict = triage(mkInput({ reversibility: 'easy' }), mkCtx(), mkSettings()) + + expect(verdict.rule).toBe('R4') + expect(verdict.route).toBe('advice') + expect(verdict.windowHours).toBe(72) + expect(verdict.explanation).toBe( + 'Réversible et 3 personnes concernées — demande leur avis puis décide.', + ) + }) + + it('limite : accord du singulier pour 1 personne', () => { + const verdict = triage( + mkInput({ reversibility: 'easy' }), + mkCtx({ computedConcernedIds: ['p1'] }), + mkSettings(), + ) + + expect(verdict.explanation).toBe( + 'Réversible et 1 personne concernée — demande leur avis puis décide.', + ) + }) + + it('limite : au-delà de smallGroupMax, l\'escalade collective s\'applique', () => { + const verdict = triage( + mkInput({ reversibility: 'easy' }), + mkCtx({ computedConcernedIds: ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'] }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R5') + }) + + it('limite : coûteux à défaire ⇒ jamais R4, même à 3 personnes', () => { + const verdict = triage(mkInput({ reversibility: 'costly' }), mkCtx(), mkSettings()) + + expect(verdict.rule).toBe('R5') + }) +}) + +describe('R5 — collectif, modalité par taille du périmètre', () => { + const ids = (n: number): string[] => Array.from({ length: n }, (_, i) => `p${i}`) + const fullSettings = mkSettings({ + protocolByRange: { nuanced: 'proto-nuanced', large: 'proto-large' }, + }) + + it('n ≤ consentMax : un tour d\'accord suffit', () => { + const verdict = triage( + mkInput(), + mkCtx({ computedConcernedIds: ids(6) }), + fullSettings, + ) + + expect(verdict.rule).toBe('R5') + expect(verdict.route).toBe('collective') + expect(verdict.protocolId).toBe('proto-consent') + expect(verdict.explanation).toBe( + 'Vous êtes 6 — un tour d\'accord suffit : sans objection, c\'est adopté.', + ) + }) + + it('n ≤ collectiveMin : vote nuancé', () => { + const verdict = triage( + mkInput(), + mkCtx({ computedConcernedIds: ids(23) }), + fullSettings, + ) + + expect(verdict.protocolId).toBe('proto-nuanced') + expect(verdict.explanation).toBe( + 'Vous êtes 23 — vote nuancé : chacun se prononce en nuances, pas en camps.', + ) + }) + + it('repli : sans protocole nuancé, le consentement s\'applique', () => { + const verdict = triage(mkInput(), mkCtx({ computedConcernedIds: ids(23) }), mkSettings()) + + expect(verdict.protocolId).toBe('proto-consent') + }) + + it('n > collectiveMin : la modalité choisie par le Pacte', () => { + const verdict = triage( + mkInput(), + mkCtx({ computedConcernedIds: ids(60) }), + fullSettings, + ) + + expect(verdict.protocolId).toBe('proto-large') + expect(verdict.explanation).toBe( + 'Vous êtes 60 — la modalité que votre Pacte a choisie s\'applique.', + ) + }) + + it('repli : sans protocole large, le consentement s\'applique', () => { + const verdict = triage(mkInput(), mkCtx({ computedConcernedIds: ids(60) }), mkSettings()) + + expect(verdict.protocolId).toBe('proto-consent') + }) + + it('poids structurel ⇒ formulation + revoyure + gravure suggérée', () => { + const verdict = triage( + mkInput({ weight: 'structural' }), + mkCtx({ computedConcernedIds: ids(6) }), + fullSettings, + ) + + expect(verdict.framingDays).toBe(14) + expect(verdict.reviewRequired).toBe(true) + expect(verdict.engravingSuggested).toBe(true) + }) + + it('irréversible ⇒ revoyure exigée, sans gravure automatique', () => { + const verdict = triage( + mkInput({ reversibility: 'irreversible' }), + mkCtx({ computedConcernedIds: ids(6) }), + fullSettings, + ) + + expect(verdict.reviewRequired).toBe(true) + expect(verdict.engravingSuggested).toBe(false) + }) +}) + +describe('R6 — récurrence et maturation (surcouche, jamais bloquante)', () => { + it('3 décisions similaires récentes ⇒ suggère de réclamer un mandat, sans changer la route', () => { + const verdict = triage( + mkInput({ tags: ['cuisine', 'achats'] }), + mkCtx({ similarRecentCount: 3 }), + mkSettings(), + ) + + expect(verdict.rule).toBe('R5') + expect(verdict.suggestion?.kind).toBe('claim-mandate') + }) + + it('3 consignations similaires ⇒ suggère de protocoliser la pratique', () => { + const verdict = triage(mkInput(), mkCtx({ similarRecordedCount: 3 }), mkSettings()) + + expect(verdict.suggestion?.kind).toBe('protocolize') + }) + + it('les deux seuils atteints ⇒ réclamer un mandat passe en premier', () => { + const verdict = triage( + mkInput(), + mkCtx({ similarRecentCount: 3, similarRecordedCount: 3 }), + mkSettings(), + ) + + expect(verdict.suggestion?.kind).toBe('claim-mandate') + }) + + it('sous le seuil ⇒ aucune suggestion', () => { + const verdict = triage( + mkInput(), + mkCtx({ similarRecentCount: 2, similarRecordedCount: 2 }), + mkSettings(), + ) + + expect(verdict.suggestion).toBeUndefined() + }) +}) + +describe('Repli sans protocole de consentement (bundle corrompu)', () => { + const broken = mkSettings({ protocolByRange: { consent: '' } }) + + it('route sur avis avec la bannière — prioritaire même sur un mandat couvrant', () => { + const verdict = triage( + mkInput({ scope: scoped(['c1']) }), + mkCtx({ myActiveMandates: [mkMandate()] }), + broken, + ) + + expect(verdict.route).toBe('advice') + expect(verdict.explanation).toBe('Aucun protocole — crée-le ou décide sur avis.') + expect(verdict.windowHours).toBe(72) + }) + + it('sauf R2 : moi seul décide sans protocole', () => { + const verdict = triage( + mkInput({ scope: { selfOnly: true, circleIds: [], personIds: [] } }), + mkCtx(), + broken, + ) + + expect(verdict.rule).toBe('R2') + expect(verdict.route).toBe('solo') + }) + + it('sauf consigner : une clause en vigueur reste consignable sans protocole', () => { + const verdict = triage(mkInput(), mkCtx({ matchingClauses: [mkClause()] }), broken) + + expect(verdict.rule).toBe('R0a') + expect(verdict.route).toBe('record') + }) +}) + +describe('Alternatives permanentes — « Je choisis autrement »', () => { + it('toujours présentes : réglage, consigner, binaire avec son coût', () => { + const verdict = triage(mkInput(), mkCtx(), mkSettings()) + + expect(verdict.alternatives).toHaveLength(3) + const labels = verdict.alternatives.map(a => a.label) + expect(labels).toContain(PARAMETRIC_ALT) + expect(labels).toContain(RECORD_ALT) + const binary = verdict.alternatives.find(a => a.cost === BINARY_COST) + expect(binary).toBeDefined() + expect(binary?.route).toBe('collective') + }) + + it('présentes sur toutes les routes, solo et consigner comprises', () => { + const solo = triage( + mkInput({ scope: { selfOnly: true, circleIds: [], personIds: [] } }), + mkCtx(), + mkSettings(), + ) + const record = triage(mkInput(), mkCtx({ matchingClauses: [mkClause()] }), mkSettings()) + + expect(solo.alternatives).toHaveLength(3) + expect(record.alternatives).toHaveLength(3) + }) + + it('montant détecté ⇒ le réglage passe en premier et parametricHint est posé', () => { + const verdict = triage( + mkInput({ title: 'Fixer la cotisation à 25 €' }), + mkCtx(), + mkSettings(), + ) + + expect(verdict.parametricHint).toBe(true) + expect(verdict.alternatives[0]?.label).toBe(PARAMETRIC_ALT) + }) + + it('sans montant ⇒ pas de mise en avant du réglage', () => { + const verdict = triage(mkInput(), mkCtx(), mkSettings()) + + expect(verdict.parametricHint).toBeUndefined() + expect(verdict.alternatives[0]?.label).not.toBe(PARAMETRIC_ALT) + }) +}) diff --git a/frontend/tests/lexicon.spec.ts b/frontend/tests/lexicon.spec.ts new file mode 100644 index 0000000..63dc3ba --- /dev/null +++ b/frontend/tests/lexicon.spec.ts @@ -0,0 +1,142 @@ +/** + * Anti-lexicon guard (BLUEPRINT-V2.md Δ22). + * + * Two sweeps, both case- and accent-insensitive (NFD normalization): + * 1. Every UI-visible VALUE exported by app/lexicon.ts — strings, Record + * values, array items, and the return of template functions called with + * dummy params. Export NAMES and Record KEYS are code identifiers and + * are NOT tested (e.g. REVIEW_VERDICTS is a legal identifier even + * though « verdict » is forbidden in UI text). + * 2. The