v3 : déploiement (image unique, produit à la racine, maquette en sous-domaine) et documentation du chantier

- docker/app.Dockerfile : une image Rust + Svelte, la construction rejoue toute
  la boucle (fmt, clippy -D warnings, tests, svelte-check, vitest) ; utilisateur
  non-root, volume /data. docker/app-compose.yml pour sonic (fabio).
- .woodpecker/app.yml : produit sur decision.librodrome.org, attend la maquette,
  écarte tout conteneur qui revendique encore la racine, healthcheck du commit.
- .woodpecker/maquette.yml (ex .woodpecker.yml) : la maquette v2 telle quelle,
  sur maquette.decision.librodrome.org, mêmes volumes.
- .env.example de la maquette rangé dans maquette/ ; celui de la v3 à la racine.
- Docs : README, CONTRIBUTING (travail à deux), CLAUDE.md v3, HANDOFF,
  docs/chantiers (feuille de route, fiche, contrôles, onboarding de Chiara,
  demande à poka), docs/adr 0001–0006.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-21 03:50:44 +02:00
co-authored by Claude Opus 5.5
parent c2669a3d0b
commit 1f9c241b41
24 changed files with 1187 additions and 281 deletions
+19
View File
@@ -0,0 +1,19 @@
# Build context of docker/app.Dockerfile (the repository root).
# The maquette builds from its own context (maquette/), not from here.
.git
.remember
.env
.env.*
!.env.example
data
target
maquette
docs
web/node_modules
web/build
web/.svelte-kit
web/test-results
web/playwright-report
**/*.db
**/*.db-wal
**/*.db-shm
+28 -43
View File
@@ -1,49 +1,34 @@
# PostgreSQL
POSTGRES_DB=libredecision
POSTGRES_USER=libredecision
POSTGRES_PASSWORD=change-me-in-production
DATABASE_URL=postgresql+asyncpg://libredecision:change-me-in-production@localhost:5432/libredecision
# libreDecision v3 — variables du serveur (ld-server). Toutes facultatives en dev.
# Le serveur lit son environnement, pas de fichier : `set -a; . ./.env; set +a` avant
# `cargo run -p ld-server`. Le .env n'est jamais commité.
# Backend
SECRET_KEY=change-me-in-production-with-a-real-secret-key
DEBUG=true
# Écoute et dossiers
LD_BIND=0.0.0.0:8002
LD_DATA_DIR=data # SQLite, clone du dépôt des textes, clé de publication — données runtime
LD_WEB_DIR=web/build # interface compilée (npm run build)
LD_SEED_DIR=seeds
LD_PUBLIC_URL=http://127.0.0.1:3002
# Environment: development, staging, production
ENVIRONMENT=development
# Connexion Ğ1 — client OAuth2 de connect.monnaie-libre.fr (les deux sont requis, sinon désactivée)
# LD_CONNECT_URL=https://connect.monnaie-libre.fr
# LD_CONNECT_CLIENT_ID=
# LD_CONNECT_CLIENT_SECRET=
# Logging: DEBUG, INFO, WARNING, ERROR
LOG_LEVEL=INFO
# Accès (trustinWallet)
LD_ADMIN_G1_IDS= # ex. idty:8352 — administrateurs des accès, séparés par des virgules
LD_DEV_LOGIN=1 # dev local uniquement : connexion sans clé, membre de tous les groupes
LD_BOOTSTRAP_ADMIN=0 # 1 = au premier démarrage sans admin, écrit data/bootstrap-admin.txt (0600)
# CORS
CORS_ORIGINS=["http://localhost:3002"]
# Publication des textes en vigueur (dépôt git lu par les applis clientes)
# LD_PUBLISH_REMOTE=git@git.duniter.org:Yvv/libredecision-textes.git
# LD_PUBLISH_BRANCH=main
# LD_PUBLISH_KEY=data/keys/publish_ed25519 # générée au démarrage si absente
# LD_PUBLISH_AUTHOR=libreDecision
# LD_PUBLISH_EMAIL=decision@librodrome.org
# Database connection pool
DATABASE_POOL_SIZE=20
DATABASE_MAX_OVERFLOW=10
# Taille de la toile de confiance (W) — lue sur la chaîne, repli sur l'indexeur puis sur la valeur fixe
# LD_RPC_ENDPOINTS=https://rpc.duniter.org,https://g1.axiom-team.fr/ws/
# LD_INDEXERS=https://indexer.duniter.org/v1/graphql
# LD_WOT_FALLBACK=5745
# Auth / Sessions
SESSION_TTL_HOURS=24
CHALLENGE_EXPIRE_SECONDS=300
# Rate limiting (requests per minute)
RATE_LIMIT_DEFAULT=60
RATE_LIMIT_AUTH=10
RATE_LIMIT_VOTE=30
# Duniter V2 RPC
DUNITER_RPC_URL=wss://gdev.p2p.legal/ws
DUNITER_RPC_TIMEOUT_SECONDS=10
# Blockchain cache TTL (seconds)
BLOCKCHAIN_CACHE_TTL_SECONDS=3600
# IPFS
IPFS_API_URL=http://localhost:5001
IPFS_GATEWAY_URL=http://localhost:8080
IPFS_TIMEOUT_SECONDS=30
# Frontend
NUXT_PUBLIC_API_BASE=http://localhost:8002/api/v1
# Docker / Production
DOMAIN=decision.librodrome.org
RUST_LOG=info,sqlx=warn
+183
View File
@@ -0,0 +1,183 @@
# libreDecision v3 — le produit (Rust + Svelte) sur decision.librodrome.org.
# Une seule image : la construction rejoue toute la boucle (fmt, clippy -D warnings,
# tests Rust, svelte-check, vitest) — un contrôle rouge, pas d'image.
# Données runtime : volume nommé lpk-libredecision-app_data (SQLite, clone du dépôt
# des textes, clé de publication). Jamais `down -v`, jamais recréé.
when:
- branch: main
event: push
# La maquette libère d'abord la racine (elle part sur maquette.decision.librodrome.org) ;
# le produit se déploie ensuite, même si la maquette a échoué.
depends_on:
- maquette
runs_on: [success, failure]
steps:
- name: security-check
image: alpine:3.20
commands:
- |
if [ -f .env ]; then
echo "ERREUR: .env ne doit pas etre commite dans le depot"
exit 1
fi
- 'grep -q "^\.env$" .gitignore || (echo "ERREUR: .env manquant dans .gitignore" && exit 1)'
- echo "Security check OK"
# NOTE: volumes + pas de from_secret : compatible
- name: build-app
image: docker:27-cli
depends_on:
- security-check
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- BUILD=$(echo "$CI_COMMIT_SHA" | cut -c1-8)
- docker build -t libredecision-app:latest -f docker/app.Dockerfile --target production --build-arg LD_BUILD=$BUILD .
- echo "Image app construite ($BUILD)"
# NOTE: volumes + pas de from_secret : compatible
- name: sbom-generate
image: alpine:3.20
depends_on:
- build-app
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- apk add --no-cache curl
- curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin latest
- mkdir -p .reports
# spec 1.5 épinglée : syft « latest » émet plus récent que ce que le trivy du runner décode
- syft libredecision-app:latest -o cyclonedx-json@1.5 --file .reports/sbom-app.cyclonedx.json
- echo "SBOM genere"
# NOTE: volumes + pas de from_secret : compatible
- name: sbom-scan
image: aquasec/trivy:latest
depends_on:
- sbom-generate
volumes:
- /home/syoul/trivy-cache:/root/.cache/trivy
commands:
- trivy sbom --format json --output .reports/trivy-app.json .reports/sbom-app.cyclonedx.json
- echo "Scan CVE termine"
# NOTE: from_secret + pas de volumes : compatible
# failure: ignore — télémétrie, ne bloque pas le déploiement (cf. maquette.yml).
- name: sbom-publish
image: alpine/curl:latest
failure: ignore
depends_on:
- sbom-scan
environment:
DTRACK_TOKEN:
from_secret: dependency_track_token
DTRACK_DOMAIN:
from_secret: dtrack_domain
commands:
- |
VERSION=$(date +%Y-%m-%d)-$(echo "$CI_COMMIT_SHA" | cut -c1-8)
HTTP=$(curl -s -o /tmp/dtrack-resp.txt -w "%{http_code}" -X POST "https://$DTRACK_DOMAIN/api/v1/bom" \
-H "X-Api-Key: $DTRACK_TOKEN" \
-F "autoCreate=true" \
-F "projectName=libredecision-app" \
-F "projectVersion=$VERSION" \
-F "bom=@.reports/sbom-app.cyclonedx.json")
echo "HTTP $HTTP (app) : $(cat /tmp/dtrack-resp.txt)"
[ "$HTTP" -ge 200 ] && [ "$HTTP" -lt 300 ] || exit 1
# NOTE: volumes + pas de from_secret : compatible
# Fabio route via les labels SERVICE_* de docker/app-compose.yml.
- name: deploy
image: docker:27-cli
depends_on:
- sbom-publish
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /opt/libredecision-app:/opt/libredecision-app
commands:
- DOMAIN=decision.librodrome.org
- echo "APP_DOMAIN=$DOMAIN" > /opt/libredecision-app/.env
- cp docker/app-compose.yml /opt/libredecision-app/docker-compose.yml
# Tout conteneur qui revendique encore la racine (maquette pas encore déplacée,
# ou en échec) est arrêté : la racine est au produit, et fabio partagerait le trafic.
- |
for TAG in "urlprefix-$DOMAIN/*" "urlprefix-$DOMAIN/api/*"; do
for C in $(docker ps -q --filter "label=SERVICE_3000_TAGS=$TAG") $(docker ps -q --filter "label=SERVICE_8002_TAGS=$TAG"); do
NAME=$(docker inspect --format '{{.Name}}' "$C")
[ "$NAME" = "/lpk-libredecision-app" ] && continue
echo "Arret de $NAME (revendique $TAG)"
docker stop "$C"
done
done
# Arrêter avant le challenge ACME : la route hôte /* masquerait le webroot de sonic-acme-1.
- cd /opt/libredecision-app && docker compose stop
- |
ACME_EXIT=0
docker exec sonic-acme-1 /app/acme.sh \
--home /etc/acme.sh \
--issue -d "$DOMAIN" \
--webroot /usr/share/nginx/html \
--server letsencrypt \
--accountemail support+acme@asycn.io || ACME_EXIT=$?
if [ "$ACME_EXIT" -eq 0 ] || [ "$ACME_EXIT" -eq 2 ]; then
docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/fullchain.cer /host/certs/$DOMAIN-cert.pem
docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/$DOMAIN.key /host/certs/$DOMAIN-key.pem
echo "TLS OK (acme exit $ACME_EXIT)"
elif docker exec sonic-acme-1 test -f "/host/certs/$DOMAIN-cert.pem"; then
echo "AVERTISSEMENT: emission ACME en echec (exit $ACME_EXIT) - certificat existant conserve"
else
echo "ERREUR: acme.sh a echoue (exit $ACME_EXIT) et aucun certificat existant pour $DOMAIN"
exit 1
fi
# Image construite localement dans la pipeline : pas de docker compose pull
- cd /opt/libredecision-app && docker compose up -d --remove-orphans
- cd /opt/libredecision-app && docker compose ps
- name: test-deploy
image: docker:27-cli
depends_on:
- deploy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- |
STATUS=$(docker inspect --format '{{.State.Status}}' lpk-libredecision-app 2>/dev/null || echo "absent")
echo "lpk-libredecision-app : $STATUS"
[ "$STATUS" = "running" ] || { docker logs --tail 50 lpk-libredecision-app 2>&1; echo "FAIL: non running"; exit 1; }
echo "PASS: conteneur running"
# Le produit répond, et c'est bien ce commit qui répond.
- name: healthcheck
image: alpine:3.20
depends_on:
- test-deploy
commands:
- apk add --no-cache --quiet curl
- |
BUILD=$(echo "$CI_COMMIT_SHA" | cut -c1-8)
TARGET="https://decision.librodrome.org/api/health"
MAX=60
i=0
until [ $i -ge $MAX ]; do
BODY=$(curl -sS -m 10 "$TARGET" 2>/dev/null || true)
echo "Tentative $((i+1))/$MAX - $BODY"
case "$BODY" in
*'"ok":true'*"\"build\":\"$BUILD\""*|*"\"build\":\"$BUILD\""*'"ok":true'*)
echo "PASS: $BUILD repond sur $TARGET"; exit 0 ;;
esac
i=$((i+1))
sleep 10
done
echo "FAIL: $BUILD ne repond pas apres 10 minutes"
exit 1
- name: notify-failure
image: alpine:3.20
commands:
- 'echo "ECHEC pipeline app #$CI_PIPELINE_NUMBER sur $CI_COMMIT_BRANCH ($CI_COMMIT_SHA)"'
when:
- status: failure
+11 -8
View File
@@ -1,3 +1,8 @@
# La maquette Nuxt v2 (FastAPI + Postgres + IPFS), conservée telle quelle pour
# présenter le projet. Depuis la v3 elle vit sur maquette.decision.librodrome.org ;
# le produit (.woodpecker/app.yml) prend la racine decision.librodrome.org et
# attend la fin de ce workflow, qui libère la racine.
# Le nom de projet compose reste lpk-decision-main : mêmes volumes, aucune donnée perdue.
when:
- branch: main
event: push
@@ -18,7 +23,7 @@ steps:
- name: test-backend
image: python:3.11-slim
commands:
- cd backend
- cd maquette/backend
- pip install --no-cache-dir -r requirements.txt
- pytest app/tests/ -v --tb=short
@@ -27,7 +32,7 @@ steps:
commands:
# git requis : dépendance @yvv/nuxt-base en git+https (node:20-slim n'a pas git)
- apt-get update && apt-get install -y --no-install-recommends git ca-certificates
- cd frontend
- cd maquette/frontend
- npm ci
- npm run build
@@ -39,7 +44,7 @@ steps:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- docker build -t libredecision-backend:latest -f docker/backend.Dockerfile --target production .
- docker build -t libredecision-backend:latest -f maquette/docker/backend.Dockerfile --target production maquette
- echo "Image backend construite"
# NOTE: volumes + pas de from_secret : compatible
@@ -50,7 +55,7 @@ steps:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- docker build -t libredecision-frontend:latest -f docker/frontend.Dockerfile --target production .
- docker build -t libredecision-frontend:latest -f maquette/docker/frontend.Dockerfile --target production maquette
- echo "Image frontend construite"
# NOTE: volumes + pas de from_secret : compatible
@@ -116,14 +121,12 @@ steps:
depends_on:
- sbom-publish
environment:
APP_DOMAIN:
from_secret: app_domain
POSTGRES_PASSWORD:
from_secret: postgres_password
SECRET_KEY:
from_secret: secret_key
commands:
- echo "APP_DOMAIN=$APP_DOMAIN" > .env.deploy
- echo "APP_DOMAIN=maquette.decision.librodrome.org" > .env.deploy
- echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" >> .env.deploy
- echo "SECRET_KEY=$SECRET_KEY" >> .env.deploy
- OWNER=$(echo "$CI_REPO_OWNER" | tr 'A-Z' 'a-z') && REPO=$(echo "$CI_REPO_NAME" | tr 'A-Z' 'a-z') && BRANCH=$(echo "$CI_COMMIT_BRANCH" | tr 'A-Z/' 'a-z-') && echo "COMPOSE_PROJECT_NAME=$OWNER-$REPO-$BRANCH" >> .env.deploy
@@ -158,7 +161,7 @@ steps:
commands:
- cp .env.deploy /opt/libredecision/.env
- chmod 600 /opt/libredecision/.env
- cp docker/docker-compose.yml /opt/libredecision/docker-compose.yml
- cp maquette/docker/docker-compose.yml /opt/libredecision/docker-compose.yml
# Arreter avant le challenge ACME : libere le webroot pour sonic-acme-1
# L'org forge a ete renommee (EHV -> LPK) : le nom de projet compose a change,
# on arrete donc aussi l'ancienne stack (ehv-decision-main-*) pour eviter la
+77 -77
View File
@@ -1,100 +1,100 @@
# libreDecision
Boîte à outils de la première démocratie — décider ensemble, du trio à la population.
v2 local-first : 6 routes de décision, 5 modalités, documents sous vote permanent, mandats,
observatoire. Marque blanche par construction (seeds Ğ1 + Atelier du Canal).
**Référence produit : `docs/dev/BLUEPRINT-V2.md`** — toute décision de comportement s'y adosse.
Rendre possible la décision collective — en fonction du contexte, processus et modalités,
boîte à outils. **v3 : Rust + Svelte**, un chantier à la fois, chacun livré en production avant
le suivant (`docs/chantiers/README.md`).
**Référence du chantier en cours : `docs/chantiers/acte-certification/CHANTIER.md`** — toute
décision de comportement s'y adosse ; les décisions durables dans `docs/adr/`.
La maquette v2 (Nuxt + FastAPI) vit dans `maquette/`, **intacte** (Yvv la présente) — ne pas y
développer.
## Protocole de début de session
1. `git pull --rebase origin main`
2. Si l'objectif de la session n'est pas précisé, le demander
1. `git pull --rebase origin main` (puis la branche du chantier : `chantier/acte-certification`)
2. Lire `HANDOFF.md` (où on en est, ce qui attend Yvv)
3. Si l'objectif de la session n'est pas précisé, le demander
## Stack
- **Frontend** : Nuxt 4 (Vue 3, TypeScript, SPA — SSR off) + Nuxt UI v3 + Pinia + UnoCSS ;
package manager : npm ; port **3002** strict
- **Local-first** : IndexedDB (idb-keyval) via `app/data/persistence.ts` — une clé
`ld2:<collectiveId>` par collectif, export/import de bundles JSON, seeds par le même chemin
- **Backend v1** (FastAPI + SQLAlchemy async, port 8002) : conservé pour la **future synchro**
— non requis pour le dev frontend ; `runtimeConfig.public.apiBase` gardé dans nuxt.config
en prévision
- Déploiement : Docker multi-stage + Traefik ; CI Woodpecker
- **Serveur** : Rust edition 2024, toolchain **1.99** épinglée (`rust-toolchain.toml`) ;
rustup installé sans PATH → `export PATH=$HOME/.cargo/bin:$PATH`. axum 0.8, sqlx 0.8
(SQLite, requêtes runtime + `migrate!`), tokio, reqwest rustls.
- **Interface** : SvelteKit 2 / Svelte 5 (runes), adapter-static SPA, vite 7, vitest,
Playwright ; npm. Polices Literata (textes) + Nunito (interface), fontsource.
- **Ports** : dev interface **3002** (strict, proxy `/api` → 8002) · serveur **8002** ·
serveur jetable des e2e **8092**.
- **Déploiement** : une image (`docker/app.Dockerfile`), Woodpecker `ci.open.us.org` dépôt
LPK/decision → sonic (fabio). `.woodpecker/app.yml` = le produit sur
**decision.librodrome.org** ; `.woodpecker/maquette.yml` = la maquette sur
**maquette.decision.librodrome.org** (le produit attend la maquette, qui libère la racine).
Push sur `prod` (`gitea@git.open.us.org:EHV/decision.git`) branche `main` = déploiement.
## Structure
```
frontend/
app/
engine/ # moteurs PURS (zéro dépendance Nuxt) : threshold (+electionResult),
# nuanced, modeParams, parametric (+crystallize), settings, state (canTransition),
# triage, impact — import UNIQUEMENT via le barrel ~/engine
data/
persistence.ts # IndexedDB local-first
templates.ts # 7 gabarits de collectif (invariant : protocole Consentement partout)
seeds/ # duniter-g1.bundle.json (324 Ko) + atelier-du-canal.bundle.json
stores/
collective.ts # le tenant : collectif actif, état complet, cycle de vie des bundles
decisions.ts # façade d'actions sans état propre (LWW updatedAt, persist debounced)
composables/ # useMood (useLibreMood), useFeed (le Fil = sélecteur pur), useSearch
components/ # par domaine : chemin/ decisions/ votes/ texts/ mandates/ feed/
# onboarding/ common/ (primitives Ld*)
pages/ # 14 routes : / · decider · decisions (+[id], vote, observatoire) ·
# textes (+[slug], formules) · mandats (+[id], nouveau) ·
# creer (layout bare) · donnees
lexicon.ts # SOURCE UNIQUE des libellés FR + FORBIDDEN_UI_TERMS
types/domain.ts # le noyau du modèle
tests/ # 342 tests vitest : engine/, stores/, seeds/ (non-perte), lexicon.spec.ts
backend/ # v1 FastAPI conservé pour la synchro future — ne pas développer dessus
scripts/export_seed_bundle.py # extrait seed.py → duniter-g1.bundle.json (source vivante = bundle)
docker/ # compose + Dockerfiles
docs/dev/BLUEPRINT-V2.md # la référence produit v2
public/hexagram-tsing*.svg # sceau 井 (#48 Tsing)
crates/
ld-engine/ # PUR (zéro I/O) : threshold (formule inertielle, presets), edit (coquille vs fond,
# diff), assist (découpage, similarité, longueur), compose (adoption, aperçus)
trustin-access/ # règles d'accès de trustinWallet : sees / opens / manages_access, groupes par défaut
ld-server/
migrations/ # schéma SQLite — additif seulement
src/ # lib.rs (routes, csrf, run) · config.rs (LD_*) · auth.rs (connect OAuth2, invitations,
# dev, bootstrap) · docs.rs (lecture, aperçus) · proposals.rs (écrire, voter, signaler,
# traduire) · adoption.rs (balayage, verrou) · publish.rs (file → git) · wot.rs (W
# sur la chaîne) · seed.rs · web.rs (sert web/build)
tests/api.rs # scénarios bout en bout
seeds/ # acte-engagement-certification.json (trilingue, 33 articles, I1/E1/E2 en vigueur)
web/src/
lib/i18n/ # fr.ts SOURCE (types Key) · en.ts · es.ts — parité testée
lib/components/ # Sidebar, DocPanel, VoteView, ProposeView, UnitBlock, DeepList, Seal, Stamp…
routes/ # / · connexion · invitation · groupes · g/[group] · g/[group]/docs/[doc] (lecteur,
# panneau dans l'URL ?onglet=&article=) · acces (trustinWallet) · decisions/mandats/
# protocoles (à venir)
web/e2e/ # Playwright C1–C10 + M1 ; serve.sh = serveur jetable /tmp/ld-e2e sur :8092
docker/ # app.Dockerfile · app-compose.yml (sonic)
.woodpecker/ # app.yml · maquette.yml
docs/ # chantiers/ · adr/ · dev/BLUEPRINT-V2.md (doctrine de la maquette) · recherche/
maquette/ # v2 intacte (frontend Nuxt, backend FastAPI, docker, docs-v1)
```
## Données runtime
## Données runtime — jamais écrasées
- **postgres-data** / **ipfs-data** : volumes Docker du backend v1 — jamais écrasés
- `.env` racine : secrets backend — jamais commité
- **Production** : volume `lpk-libredecision-app_data` (`/data` : `libredecision.db`, `textes/`
= clone du dépôt publié, `keys/` = clé de publication, `bootstrap-admin.txt`). Jamais
`down -v`, jamais recréé.
- **Maquette** : volumes `lpk-decision-main_postgres-data` / `_ipfs-data` (inchangés).
- **Local** : `data/` (gitignoré) — le supprimer repart du seed. Les e2e utilisent `/tmp/ld-e2e`.
- Un document seedé n'est **jamais** réinstancié : dès qu'il existe en base, c'est de la donnée.
## Commandes
## Commandes — la boucle (tout vert = fait)
```bash
# Frontend (tout le dev v2)
cd frontend && npm run dev # :3002
npm test # vitest — 342 tests, référence à maintenir
npm run build # zéro erreur exigé avant commit
export PATH=$HOME/.cargo/bin:$PATH
cargo fmt --all --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace
cd web && npm run check && npm test && npm run build && npm run e2e # e2e : 11 contrôles
# Seeds : le bundle est la source vivante ; seed.py (backend) est remplacé par
cd backend && .venv/bin/python scripts/export_seed_bundle.py # régénère duniter-g1.bundle.json
# → test de non-perte bloquant : tests/seeds/duniter-g1.spec.ts
# Docker
docker compose -f docker/docker-compose.yml up
LD_DEV_LOGIN=1 cargo run -p ld-server # :8002, sert web/build
cd web && npm run dev # :3002 à chaud
docker build -f docker/app.Dockerfile --target production . # rejoue toute la boucle
```
## Conventions / pièges
- **UI français, code anglais** (variables, commentaires, docstrings)
- **lexicon.ts d'abord** : aucun libellé UI en dur si le lexicon peut le porter ; ton = tutoyer
la personne, sujet grammatical = la personne ou le collectif, jamais la formule ni le système
- **Anti-lexique** (`tests/lexicon.spec.ts`) : `FORBIDDEN_UI_TERMS` scanné sur lexicon +
templates `.vue` (insensible casse/accents) — « triage », « verdict », « déléguer »… restent
des identifiants de code, jamais des textes affichés
- **Invariants doctrinaux** : jamais de score sur une personne (attributs jamais affichés à
autrui, re-votes visibles du seul auteur) ; jamais de contenu adopté sans geste humain
(cristallisation, clôture de dossier, départage = gestes datés) ; repli universel → consent ;
dérogation asymétrique (alourdir libre, alléger motivé)
- **Budget 400 lignes max par page** (`app/pages/`)
- **Moteurs purs** : engine/ sans Nuxt/Pinia/DOM, import via `~/engine` seulement ; stores en
imports explicites (testables sous vitest nu) ; toute mutation d'état passe par
`canTransition(decision, to, ctx)`
- **Formule inertie** : `R = C + B^W + (M + (1−M)(1−(T/W)^G))·max(0, T−C)` —
`frontend/app/engine/threshold.ts` ; l'Atelier (`/textes/formules`) est la seule maison des
lettres W/T/M/B/G/C et de KaTeX
- **Mood system** : `useLibreMood()` (`composables/useMood.ts`) — **jamais** de `:global()`
dans `<style scoped>` pour les styles mood-dépendants (causa le bug dark mode veil)
- **Sceau** `井` (#48 Tsing) : `LdSeal` dans les layouts ; SVGs dans `public/`
- **CSS `drop-shadow()`** safe pour l'emboss ; `<filter>` SVG inline cause des artefacts
- **Composants** : `pathPrefix: false` — noms de fichiers uniques, auto-import
- **Domaine** : decision.librodrome.org (Woodpecker CI ; ancien dossier : Glibredecision)
- **UI français, code anglais** ; **trilingue** : aucune chaîne affichée en dur, tout passe par
`web/src/lib/i18n/fr.ts` puis en/es (test de parité + `FORBIDDEN_UI_TERMS`). Tutoyer ; le sujet
est la personne ou le groupe, jamais le système. Le français du texte voté fait foi.
- **Moteur pur** : une règle de vote / édition / composition s'écrit et se teste dans
`ld-engine` avant d'être câblée. Le serveur ne décide jamais l'accès seul : `trustin-access`.
- **Invariants** : une seule voix ne fait jamais passer un texte (C = 50 pour Monnaie libre) ;
une coquille ne demande pas de vote, un changement de fond si ; un signalement de
contradiction bloque ; l'inertie se vote au cran au-dessus ; le groupe d'essai ne publie jamais.
- **Écritures** : en-tête `x-requested-with: libredecision` obligatoire (CSRF) ; cookie
`ld_session`. Les tests d'API passent par `Client` (`tests/api.rs`).
- **Pièges vus** : `pkill -f target/debug/ld-server` tue son propre shell → `pkill -x ld-server` ;
un `from_secret` absent fait refuser tout le pipeline Woodpecker ; la route hôte `/*` de fabio
masque le webroot ACME → arrêter le service avant `acme.sh --issue` ; Playwright en locale
`fr-FR` (sinon l'interface part en anglais).
- **Design** : sans bordures, arrondis 16/12/20/14, ambiances `pierre` / `nuit`
(`data-mood`, posé avant le premier rendu), sceau hexagramme 井 #48 en SVG (`Seal`, `Stamp` —
jamais le glyphe, qui s'affiche mal). Mouvement réduit respecté.
- **Domaine** : decision.librodrome.org ; dépôt des textes `git.duniter.org/Yvv/libredecision-textes`.
+52 -80
View File
@@ -1,91 +1,63 @@
# Contribuer à libreDecision
La référence produit est `docs/dev/BLUEPRINT-V2.md` — toute contribution qui touche au
comportement doit s'y conformer (ou proposer un amendement du blueprint, argumenté).
On travaille **un chantier à la fois**, livré complet et en production avant le suivant
(`docs/chantiers/README.md`). Le chantier en cours a sa fiche :
`docs/chantiers/acte-certification/CHANTIER.md` — toute décision de comportement s'y adosse,
ou propose de l'amender, argumentée.
## Environnement
Première fois ? Commence par `docs/chantiers/acte-certification/ONBOARDING.md`.
## Les branches — travailler à deux
```
main = la production. Tout push sur prod/main déploie decision.librodrome.org.
chantier/acte-certification = la branche commune du binôme (Yvv · Chiara), toujours verte.
chiara/<sujet>, yvv/<sujet> = une branche courte par sujet, partie de la branche du chantier.
```
1. `git switch chantier/acte-certification && git pull --rebase origin chantier/acte-certification`
2. `git switch -c chiara/<sujet>` — un sujet, une branche, quelques commits.
3. Boucle verte (ci-dessous), puis push de la branche et demande de fusion vers
`chantier/acte-certification` sur la forge (`git.open.us.org/yvv/decision`).
4. Le chantier passe dans `main` quand la boucle est verte et les contrôles d'usage passés.
On ne pousse jamais directement sur `main`, et jamais de `--force` sur une branche partagée.
## La boucle — une tâche n'est faite que si tout est vert
```bash
# Frontend — suffit pour tout le développement v2 (local-first, aucun serveur requis)
cd frontend
npm install
npm run dev # http://localhost:3002 — port strict, jamais de fallback
npm test # vitest — 342 tests
npm run build # doit passer sans erreur avant tout commit
# Backend v1 (optionnel — conservé pour la future synchro)
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
export PATH=$HOME/.cargo/bin:$PATH
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cd web
npm run check # svelte-check, zéro avertissement
npm test # vitest : markdown, parité des catalogues i18n, anti-lexique
npm run build
npm run e2e # Playwright : C1–C10 (bureau) + M1 (mobile), serveur jetable sur :8092
```
L'image Docker rejoue les mêmes portes : un contrôle rouge, pas d'image, pas de déploiement.
## Conventions
- **UI en français, code en anglais** (variables, commentaires, docstrings).
- **`app/lexicon.ts` est la source unique des libellés français.** Aucun libellé UI en dur
dans un composant s'il existe (ou devrait exister) dans le lexicon. Ton : tutoyer la
personne dans ses gestes ; actes collectifs à l'infinitif ; le sujet grammatical est
toujours la personne ou le collectif — jamais la formule ni le système.
- **Test anti-lexique** (`tests/lexicon.spec.ts`) : la liste `FORBIDDEN_UI_TERMS` interdit
certains mots dans toute chaîne visible (lexicon + templates `.vue`), insensible à la casse
et aux accents. « Triage », « verdict », « déléguer »… peuvent exister comme identifiants de
code, jamais à l'écran.
- **CSS** : borderless (profondeur via background + box-shadow), palettes par classes
`.mood-*` sur `<html>`, composants natifs (`<input>`, `<select>`, `<button>` custom).
- **Composants** : `pathPrefix: false` — noms de fichiers uniques et parlants, auto-import.
- **Interface en français, code en anglais** (identifiants, commentaires, messages de log).
- **Trilingue dès le départ** : toute chaîne affichée vit dans `web/src/lib/i18n/fr.ts`
(la source), puis `en.ts` et `es.ts`. Le test de parité refuse une clé manquante ou un
paramètre `{x}` qui diffère. Le texte voté est le français ; les traductions sont éditoriales.
- **Ton** : tutoyer la personne ; le sujet est la personne ou le collectif, jamais le système.
Mots interdits à l'écran (`FORBIDDEN_UI_TERMS`) : « triage », « verdict », « déléguer »…
- **Moteur pur** : `crates/ld-engine` n'a ni I/O, ni base, ni réseau. Toute règle de vote,
d'édition ou de composition s'y écrit et s'y teste d'abord.
- **Accès** : le serveur ne décide jamais seul qui voit quoi — il demande à `trustin-access`.
- **Données runtime** (`data/`, volume `lpk-libredecision-app_data`) : jamais écrasées, jamais
réinitialisées ; une migration ajoute, elle ne réécrit pas.
- **Design** : sans bordures (profondeur par fond et ombre), arrondis 16 / 12 / 20 / 14 px,
deux ambiances (pierre, nuit), Literata pour les textes, Nunito pour l'interface.
- **Commits** : en français, une intention par commit, `git add` avec chemins explicites.
## Invariants doctrinaux (non négociables)
## Signaler
- **Jamais de score sur une personne.** Pas de note, pas de réputation, pas d'agrégat
individuel. `Person.attributes` ne sont jamais affichés à autrui ni agrégés ; l'historique
de re-vote n'est visible que de son auteur.
- **Jamais de contenu adopté sans geste humain.** Le moteur constate des seuils sur des
contenus formulés par des humains ; la cristallisation paramétrique, la clôture d'un
dossier découpé, le départage d'une égalité sont des gestes datés — jamais des automatismes.
- **Le repli universel est le consentement** : tout collectif a un protocole Consentement
(`protocolByRange.consent`), tout repli de résolution y mène.
- **Dérogation asymétrique** : alourdir libre (1 tap journalisé) ; alléger = note obligatoire
+ fenêtre d'objection.
- **Budget 400 lignes maximum par page** (`app/pages/`). Au-delà, extraire des composants.
- **Moteurs purs** : `app/engine/` ne dépend de rien (ni Nuxt, ni Pinia, ni DOM), s'importe
uniquement via le barrel `~/engine`, et chaque module a ses tests vitest. Les stores
s'écrivent avec des imports explicites (pas d'auto-import Nuxt) pour rester testables sous
vitest pur.
## Ajouter une modalité de vote
1. `app/types/domain.ts` — étendre `VoteMethod` et les types de session/vote nécessaires.
2. `app/engine/` — le calcul dans un module pur + ses tests (`tests/engine/`). Exporter via
`engine/index.ts`.
3. `app/engine/state.ts` — la règle de clôture (automatique à `closesAt`, ou geste humain).
4. `app/lexicon.ts` — les libellés (et vérifier l'anti-lexique).
5. `app/components/votes/VoteX.vue` — l'écran de la modalité, branché dans
`pages/decisions/[id]/vote.vue`.
6. Seeds et gabarits — un protocole nommé qui la référence (le modifier = amender le Pacte).
## Ajouter un gabarit de collectif
Dans `app/data/templates.ts` : une entrée `TEMPLATE_CARDS` + le montage dans
`buildTemplateBundle()`. Invariants : protocole Consentement au minimum, clause A1
(« Notre finalité »), préambule Autonomie–Équilibre–Liaison proposé et modifiable, cercles
typés `kind`. Un bundle seed passe par le même `importBundle()` que l'import utilisateur.
## Tests
```bash
cd frontend && npm test
```
- `tests/engine/` — les moteurs purs (seuils, triage, états, médiane, impact…).
- `tests/stores/` — la façade decisions sous Pinia nu.
- `tests/seeds/` — tests de non-perte : le bundle Ğ1 doit conserver 33+59 clauses, décomptes
et provenance exacts ; l'Atelier du Canal doit couvrir tous les états.
- `tests/lexicon.spec.ts` — l'anti-lexique.
Le compteur de référence est **342 tests verts**. Toute PR qui en casse un est refusée ;
toute fonctionnalité de moteur arrive avec les siens.
## Build
`npm run build` doit rester vert (zéro erreur, zéro warning nouveau) avant chaque commit.
Vérifier mobile + desktop (zéro régression CSS).
**« Faire un retour »** (dans le menu de gauche, sur chaque page) envoie une note datée avec la
page, la langue et la taille de l'écran ; elles se lisent dans trustinWallet (`/acces`).
Pour un bug reproductible, une issue sur la forge avec les étapes.
+35
View File
@@ -0,0 +1,35 @@
# HANDOFF — libreDecision
> Dernière mise à jour : 2026-10-21, session orch du chantier 1.
## Où on en est
**Chantier 1 — Acte d'engagement Certification** : construit, boucle verte, prêt à livrer.
- v3 Rust + Svelte à la racine du dépôt ; la maquette v2 intacte dans `maquette/`.
- Boucle : Rust (fmt, clippy `-D warnings`, 54 tests dont 13 scénarios d'API et la
publication vers un dépôt nu) · interface (svelte-check 0 avertissement, 9 vitest) ·
e2e 11 / 11 (C1–C10, M1). L'image Docker rejoue tout et a été essayée localement :
santé, interface, volume non-root, clé de publication, lien admin initial (0600, absent
des journaux), publication immédiate au démarrage, redémarrage sans commit superflu.
- Déploiement : `.woodpecker/maquette.yml` (la maquette part sur
maquette.decision.librodrome.org) puis `.woodpecker/app.yml` (le produit à la racine).
## Ce qui attend Yvv
1. **Envoyer la demande à poka** : `docs/chantiers/acte-certification/DEMANDE-CONNECT.md`.
2. **Créer le dépôt des textes** sur git.duniter.org : `Yvv/libredecision-textes`, **public**,
vide ; y ajouter la **clé de déploiement** du serveur, **en écriture** (sa partie publique
s'affiche dans trustinWallet, `/acces`, section Publication). Le serveur fait lui-même le
premier envoi.
3. **Ouvrir trustinWallet** avec le lien administrateur initial (remis hors conversation),
puis créer l'**invitation de Chiara** (groupes Monnaie libre + groupe d'essai).
4. Donner à Chiara un compte **git.open.us.org** collaboratrice de `yvv/decision`.
5. Décider : la **licence** du code ; le texte F1.2 du seed (annonce C = 0 et 30 jours).
## Pour reprendre
- Lire `docs/chantiers/acte-certification/CHANTIER.md` puis `CONTROLES.md`.
- Les retours de Chiara arrivent dans `/acces` → Retours : chacun est un contrôle à repasser.
- Quand les identifiants connect arrivent : `DEMANDE-CONNECT.md` § « À la réponse ».
- Chantier suivant (après la mise en production et les retours) : Acte d'engagement forgeron.
+74 -73
View File
@@ -1,11 +1,10 @@
# libreDecision
Boîte à outils de la première démocratie — décider ensemble, du trio à la population.
Rendre possible la décision collective — en fonction du contexte, processus et modalités,
boîte à outils.
libreDecision fait de la décision un geste quotidien : chaque question tient en une phrase,
l'outil propose le chemin le plus court — jamais plus lourd que l'enjeu — et tout ce qui est
décidé reste révisable, mesuré, consigné. Le même outil sert un trio d'amis, une association,
une coopérative ou la toile de confiance Ğ1 (marque blanche par construction).
En ligne : **https://decision.librodrome.org** (le produit, v3) · la maquette v2, conservée pour
présenter le projet : https://maquette.decision.librodrome.org (`maquette/`).
## Manifeste
@@ -24,90 +23,92 @@ une coopérative ou la toile de confiance Ğ1 (marque blanche par construction).
> 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.
## Les 6 routes de décision
## Les quatre outils
Le triage (moteur pur, toujours expliqué en une phrase, toujours contournable) oriente chaque
décision vers l'une de six routes :
| Outil | Ce qu'il fait | État |
|---|---|---|
| **Décisions** | funnel assisté pour choisir les modalités, journal des décisions | à venir |
| **Docs de référence** | acte d'engagement, licence, charte, constitution… sous vote permanent | **actif** |
| **Mandats** | création et révocation de tout mandat | à venir |
| **Protocoles** | workflows sans organe statutaire, rôles tournants, automatisations | à venir |
| Route | Geste |
|---|---|
| **solo** | « Personne d'autre n'est concerné — décide. » 2 gestes, moins de 10 secondes, trace facultative |
| **record** | « Déjà tranché — je le consigne. » L'observatoire en un geste, adopté immédiatement |
| **mandate** | « Ton mandat couvre — décide, c'est tracé. » Trace obligatoire + fenêtre d'objection 48 h |
| **transmit** | « Le mandat d'un autre couvre — transmets-lui. » (ou conteste le mandat) |
| **advice** | « Réversible et petit cercle — demande leur avis, puis décide. » 72 h |
| **collective** | « Nous décidons. » Formulation, liste des concernés arrêtée, session de vote |
Le produit avance **un chantier à la fois**, chacun livré complet et en production avant le
suivant — feuille de route : [`docs/chantiers/README.md`](docs/chantiers/README.md).
Chantier en cours : **Acte d'engagement Certification** (Monnaie libre), en binôme Yvv + Chiara.
Dérogation asymétrique : alourdir est libre (1 tap journalisé) ; alléger exige une note motivée
et une fenêtre d'objection.
## Comment marche un doc de référence
## Les 5 modalités
- On arrive sur **le texte en vigueur**, seul, lisible. Trois gestes contextuels : consulter le
vote, voir les propositions, proposer une modification.
- Chaque article est sous **vote permanent** : une proposition qui atteint son seuil (formule
inertielle Ğ1, plancher C de votants « pour ») **remonte en vigueur** et remplace la version
en place — si elle tient une marge plus large. Les textes de départ sont des *seeds a minima*,
à zéro vote.
- **Corriger n'est pas voter** : une coquille se corrige sans vote ; un changement de fond
ouvre une nouvelle révision, dont les votes antérieurs sont à reconfirmer.
- Deux propositions qui **se contredisent** ne passent pas ensemble : un signalement bloque la
seconde tant qu'il n'est pas levé.
- L'**inertie** de chaque article se vote aussi (par défaut : un cran au-dessus).
- Tout ce qui entre en vigueur est **publié automatiquement** dans un dépôt git
(fr/en/es + JSON) lu par les applications clientes.
Chaque protocole est nommé et référencé par une clause du Pacte — le modifier, c'est amender
le Pacte.
1. **Consentement** — « Ça me va / J'objecte » : zéro objection maintenue = adopté. Le repli
universel : tout collectif en a un (invariant).
2. **Nuancé 6 niveaux** — de « Pas du tout » à « Tout à fait », jamais « contre » ;
histogramme visible, commentaire obligatoire sur les niveaux bas.
3. **Paramétrique** — décider au curseur : faisceau des positions, médiane vivante, carte
« Pour moi » ; la cristallisation finale est un geste humain daté du garant, jamais un
automatisme.
4. **Élection** — réservée aux mandats et aux problématiques d'échelle (6 méthodes de
nomination, vote blanc, jamais de départage par l'outil).
5. **Binaire inertiel** — le seuil `C + B^W + (M + (1−M)(1−(T/W)^G))·max(0, T−C)`, héritage
Ğ1 remis à sa place : l'outil de dernier recours des très grands corps. L'Atelier des
formules (`/textes/formules`) reste son école.
## Local-first
- **Aucun serveur requis** : tout l'état vit dans le navigateur (IndexedDB via idb-keyval,
une clé par collectif).
- **Export / import** de bundles JSON — la fédération, c'est l'échange de bundles, jamais une
table.
- **Seeds** livrés et importés par le même chemin que l'import utilisateur :
- `duniter-g1.bundle.json` — la toile Ğ1 : 33 clauses Certification + 59 clauses Forgeron
(textes votés intacts, provenance et décomptes conservés), protocoles, 3 mandats ;
- `atelier-du-canal.bundle.json` — association fictive de 12 membres, le test permanent de
marque blanche : décisions à tous les états, paramétrique vivant, épreuve du réel due.
- **7 gabarits** de collectif à la création (`/creer`) : page blanche, informel, association,
coopérative, commune, monnaie libre, institution symétrique.
Le détail, les décisions et leurs raisons : [`docs/chantiers/acte-certification/CHANTIER.md`](docs/chantiers/acte-certification/CHANTIER.md)
et [`docs/adr/`](docs/adr/).
## Stack
- **Frontend** : Nuxt 4 (SPA, SSR off) + Vue 3 + TypeScript + Nuxt UI v3 + Pinia + UnoCSS ;
npm ; port **3002**
- **Moteurs purs** : `frontend/app/engine/` — triage, machine à états, seuils, médiane
paramétrique, nuancé, périmètres — zéro dépendance Nuxt, testés vitest (**342 tests**)
- **Backend v1** (FastAPI + SQLAlchemy async) : conservé dans `backend/` pour la future
synchronisation multi-appareils — non requis pour utiliser ou développer le frontend
- **Serveur** : Rust (edition 2024, toolchain 1.99 épinglée) — axum 0.8, sqlx/SQLite, tokio.
Trois crates : `ld-engine` (moteur pur : seuils, édition, assistant, composition),
`trustin-access` (règles d'accès de trustinWallet), `ld-server` (API + publication + service
de l'interface).
- **Interface** : SvelteKit 2 / Svelte 5 (runes), SPA statique servie par le serveur ;
trilingue fr · en · es dès le départ (le français fait foi).
- **Déploiement** : une seule image Docker, Woodpecker → sonic (fabio), volume de données nommé.
## Démarrage
## Démarrer
```bash
cd frontend
npm install
npm run dev # http://localhost:3002
npm test # vitest — 342 tests
npm run build # build de production
export PATH=$HOME/.cargo/bin:$PATH
cd web && npm ci && npm run build && cd .. # l'interface compilée
LD_DEV_LOGIN=1 cargo run -p ld-server # API + interface sur :8002
cd web && npm run dev # interface à chaud sur :3002 (proxy /api → :8002)
```
## Structure v2
`LD_DEV_LOGIN=1` ouvre une connexion de développement (sans clé, membre de tous les groupes) —
jamais en production. Toutes les variables : [`.env.example`](.env.example).
## Vérifier (la boucle)
```bash
cargo fmt --all --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace
cd web && npm run check && npm test && npm run build && npm run e2e
```
Les contrôles d'usage (C1–C10, M1) sont décrits dans
[`docs/chantiers/acte-certification/CONTROLES.md`](docs/chantiers/acte-certification/CONTROLES.md) ;
les tests de Chiara en sont la version humaine.
## Structure
```
frontend/app/
engine/ # moteurs purs (barrel index.ts — seul point d'import)
data/ # persistence.ts (IndexedDB), templates.ts (7 gabarits), seeds/*.bundle.json
stores/ # collective.ts (le tenant) + decisions.ts (façade d'actions)
composables/ # useMood, useFeed, useSearch
components/ # par domaine : chemin/ decisions/ votes/ texts/ mandates/ feed/ onboarding/ common/
pages/ # 14 routes : / decider decisions(+id, vote, observatoire)
# textes(+slug, formules) mandats(+id, nouveau) creer donnees
lexicon.ts # source unique des libellés FR + test anti-lexique
types/domain.ts # le noyau du modèle
docs/dev/BLUEPRINT-V2.md # la référence produit
crates/
ld-engine/ # moteur pur, sans I/O : threshold, edit, assist, compose
trustin-access/ # qui voit, qui ouvre, qui gère les accès
ld-server/ # axum : auth Ğ1 + invitations, docs, propositions, votes, adoption, publication
migrations/ # schéma SQLite (données runtime : jamais réécrites)
tests/api.rs # scénarios bout en bout de l'API
seeds/ # documents de départ (JSON trilingue), instanciés une seule fois par groupe
web/
src/lib/ # api, session, i18n (fr source, en, es), composants
src/routes/ # accueil, groupes, lecteur du texte, trustinWallet (/acces)…
e2e/ # Playwright : C1–C10, M1
docker/ # app.Dockerfile + app-compose.yml (sonic)
.woodpecker/ # app.yml (le produit) · maquette.yml (la maquette)
docs/ # chantiers/, adr/, dev/ (blueprint v2), recherche/
maquette/ # la v2 Nuxt + FastAPI, telle quelle
```
## Licence
Logiciel libre. Le fichier LICENSE sera posé avant la première publication.
Logiciel libre — la licence reste à choisir (question ouverte à Yvv) ; le fichier LICENSE sera
posé avant toute publication du code.
+37
View File
@@ -0,0 +1,37 @@
# libreDecision v3 on sonic — decision.librodrome.org.
# Runtime data lives in the named volume `data` (SQLite, the server's clone of
# the texts repository, its deploy key): never `down -v`, never recreated.
# Settings that are not secret are here; secrets, if any, in /opt/libredecision-app/secrets.env.
name: lpk-libredecision-app
services:
app:
image: libredecision-app:latest
container_name: lpk-libredecision-app
restart: always
env_file:
- path: /opt/libredecision-app/secrets.env
required: false
environment:
LD_PUBLIC_URL: https://${APP_DOMAIN:-decision.librodrome.org}
LD_ADMIN_G1_IDS: ${LD_ADMIN_G1_IDS:-idty:8352}
LD_BOOTSTRAP_ADMIN: "1"
LD_PUBLISH_REMOTE: ${LD_PUBLISH_REMOTE:-git@git.duniter.org:Yvv/libredecision-textes.git}
LD_PUBLISH_BRANCH: main
RUST_LOG: info,sqlx=warn,tower_http=warn
volumes:
- data:/data
labels:
# Registrator → Consul → Fabio.
- SERVICE_8002_NAME=lpk-libredecision-app-8002
- SERVICE_8002_TAGS=urlprefix-${APP_DOMAIN:-decision.librodrome.org}/*
- SERVICE_8002_CHECK_TCP=true
networks:
- sonic
volumes:
data:
networks:
sonic:
external: true
+56
View File
@@ -0,0 +1,56 @@
# syntax=docker/dockerfile:1
# libreDecision v3 — one image: the Rust server serves the API and the compiled
# Svelte interface. Every gate of the loop runs inside the build: a red check
# means no image.
# --- Interface -----------------------------------------------------------------
FROM node:22-bookworm-slim AS web
WORKDIR /src/web
COPY web/package.json web/package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY web/ ./
ARG LD_BUILD=dev
ENV LD_BUILD=${LD_BUILD}
RUN npx svelte-kit sync \
&& npx svelte-check --tsconfig ./tsconfig.json --fail-on-warnings \
&& npx vitest run \
&& npx vite build
# --- Server --------------------------------------------------------------------
FROM rust:1.99-slim-bookworm AS server
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY rust-toolchain.toml ./
RUN rustup show active-toolchain || rustup toolchain install
COPY Cargo.toml Cargo.lock rustfmt.toml ./
COPY crates crates
COPY seeds seeds
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target \
cargo fmt --all --check \
&& cargo clippy --workspace --all-targets --locked -- -D warnings \
&& cargo test --workspace --locked \
&& cargo build --release --locked -p ld-server \
&& cp target/release/ld-server /usr/local/bin/ld-server
# --- Runtime -------------------------------------------------------------------
FROM debian:bookworm-slim AS production
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git openssh-client tini \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 --create-home --home-dir /home/app app \
&& mkdir -p /data && chown app:app /data
COPY --from=server /usr/local/bin/ld-server /usr/local/bin/ld-server
COPY --from=web /src/web/build /app/web
COPY seeds /app/seeds
ARG LD_BUILD=dev
ENV LD_BUILD=${LD_BUILD} \
LD_BIND=0.0.0.0:8002 \
LD_DATA_DIR=/data \
LD_WEB_DIR=/app/web \
LD_SEED_DIR=/app/seeds
USER app
EXPOSE 8002
VOLUME /data
ENTRYPOINT ["tini", "--"]
CMD ["ld-server"]
+19
View File
@@ -0,0 +1,19 @@
# 0001 — Rust + Svelte, une image, SQLite
**Contexte.** La v2 (Nuxt local-first + FastAPI) a servi de maquette. Yvv veut « un dev rust -
svelte pour la longue durée, la performance, la modularité et la compilation de haute qualité ».
Le premier chantier a besoin d'un serveur (votes partagés, publication), pas d'un stockage
navigateur.
**Décision.**
- Serveur en Rust : `ld-engine` (règles pures, sans I/O), `trustin-access` (accès),
`ld-server` (axum + sqlx). Toolchain épinglée, `clippy -D warnings`, `unsafe` interdit.
- Interface SvelteKit / Svelte 5 en SPA statique, compilée et servie par le même binaire.
- SQLite (WAL) dans un volume nommé : une instance, des milliers de votants, aucune base à
administrer. Migrations additives seulement.
- Une seule image ; sa construction rejoue toute la boucle.
- La maquette v2 reste dans `maquette/`, intacte, déployée à part.
**Coût.** Deux langages dans le dépôt ; une instance unique (pas de réplication). Si la
synchronisation entre instances devient nécessaire, elle passera par le dépôt des textes et une
API, pas par la base.
+20
View File
@@ -0,0 +1,20 @@
# 0002 — Vote permanent : marge du texte en place et plancher C
**Contexte.** Chaque article est sous vote permanent ; « dès qu'une proposition atteint le score,
elle remonte en vigueur ». La formule inertielle Ğ1 donne un seuil faible quand peu de gens
votent : sans garde-fou, une seule voix ferait passer un texte. Sans règle de départage, deux
versions qui atteignent leur seuil s'échangeraient la place à chaque vote.
**Décision.**
- Plancher **C = nombre minimal de votants « pour »** (Monnaie libre : 50 ; essai : 1), dans
la formule (`R = C + …`) — choix d'Yvv.
- Une proposition entre en vigueur si elle atteint son seuil **et** tient une marge
(pour − requis) strictement plus large que la version en place. La version déplacée redevient
une proposition avec ses votes : rien n'est perdu, tout peut revenir.
- Les textes de départ sont des *seeds a minima* (0 vote, marge −C) : visibles, non adoptés.
- Changer l'inertie d'un article est une proposition votée à l'inertie du cran au-dessus.
- Le balayage tourne après chaque geste et toutes les 5 minutes, sous un verrou global.
**Coût.** Une version en place très soutenue est difficile à déloger — c'est voulu. La taille
de la toile (W) bouge : un texte peut passer ou repasser sans nouveau vote ; chaque entrée en
vigueur est datée et chiffrée dans `adoptions`.
+15
View File
@@ -0,0 +1,15 @@
# 0003 — Corriger n'est pas voter
**Contexte.** « Ça ne peut pas devenir un système qui nécessite un vote pour corriger une
coquille. » Mais une correction ne doit pas pouvoir changer le sens d'un texte voté.
**Décision.** Le moteur classe chaque modification (`ld-engine/src/edit.rs`) :
- **coquille** si le squelette (sans accents, casse, ponctuation, espaces) est identique, ou si
au plus 2 mots diffèrent de 2 lettres au plus, sans chiffre ni mot de négation (ne, pas,
jamais, personne…) ; elle s'applique sans vote, garde les votes et les traductions ;
- **de fond** sinon : nouvelle révision d'une proposition ouverte (par son auteur), votes
antérieurs « à reconfirmer », signalements levés ; refusée sur un texte en vigueur.
L'éditeur montre le verdict avant d'enregistrer. Les brouillons s'éditent librement.
**Coût.** Le classement est prudent : certaines vraies coquilles (« persone » → « personne »)
passent pour du fond et demandent une proposition. On préfère ce sens d'erreur.
+14
View File
@@ -0,0 +1,14 @@
# 0004 — Contradictions : signalées par les personnes, bloquantes
**Contexte.** « Il faut que 2 unités modulaires ne puissent pas passer en vigueur si elles se
contredisent. » La contradiction est une question de sens : aucune règle ne la détecte sans
se tromper, et l'IA est écartée pour l'instant.
**Décision.** Tout membre signale une contradiction entre deux propositions, avec un motif.
Tant que le signalement tient, celle qui n'est pas en vigueur ne peut pas y entrer. Il se lève
par son auteur ou un administrateur, et tombe si la proposition signalée change sur le fond.
L'assistant, lui, prévient (sans bloquer) quand un texte porte plusieurs décisions ou ressemble
à une proposition existante.
**Coût.** Un signalement abusif bloque une proposition jusqu'à sa levée. Les signalements sont
publics, motivés, et l'administration peut les lever.
+17
View File
@@ -0,0 +1,17 @@
# 0005 — Publication par file d'envoi vers un dépôt git
**Contexte.** « Le tout est automatiquement poussé sur le dépôt git exploité par les applis
clientes. » Le réseau ou la forge peuvent être indisponibles ; un vote ne doit jamais échouer
pour autant.
**Décision.** Chaque entrée en vigueur (ou correction de coquille, ou traduction d'un texte en
vigueur) ajoute une ligne à une file d'envoi en base ; au démarrage, chaque document publié y
est remis une fois (rien ne se perd sur un redémarrage, un document inchangé ne fait pas de
commit : sa date est celle du dernier changement, jamais « maintenant »). Un travailleur rend `fr.md`, `en.md`,
`es.md` et `document.json` dans le clone du serveur, commit, pousse avec une clé de déploiement
générée par le serveur (sa partie publique s'affiche dans trustinWallet), et réessaie avec un
délai croissant. Dépôt : `git.duniter.org/Yvv/libredecision-textes`, un dossier par groupe et
par document. Le français fait foi ; les traductions sont marquées éditoriales.
**Coût.** La publication est asynchrone (quelques secondes, plus en cas de panne) ; l'état
de la file se lit dans trustinWallet.
@@ -0,0 +1,20 @@
# 0006 — Accès : connect.monnaie-libre.fr, invitations, trustinWallet
**Contexte.** « Le même accès que pour le forum Monnaie libre », « toute personne avec une clé
Duniter peut se loger et tout faire — sauf la gestion des accès », qui revient à trustinWallet
(nouveau nom de trustWallet, déjà pris).
**Décision.**
- Connexion Ğ1 : libreDecision est client OAuth2 de connect.monnaie-libre.fr (comme le forum).
Le compte est la clé d'identité (`idty:N`) ; on garde le nom affiché et le statut de membre.
- En attendant l'inscription du client : liens d'invitation à usage unique (14 jours), le
secret dans le fragment de l'URL (jamais dans les journaux du serveur).
- Sessions : cookie HttpOnly, SameSite=Lax, Secure en https, 30 jours, jeton haché en base ;
en-tête anti-CSRF sur toute écriture.
- Les règles (qui voit, qui ouvre, qui administre) vivent dans `trustin-access`, futur cœur du
service trustinWallet ; le serveur ne décide jamais seul.
- Premier administrateur : un lien d'invitation écrit au premier démarrage dans le volume de
données (0600), jamais dans les journaux.
**Coût.** Tant que connect n'est pas branché, chaque testeuse a besoin d'un lien. Quand
trustinWallet deviendra un service, `trustin-access` deviendra son client.
+13
View File
@@ -0,0 +1,13 @@
# Décisions d'architecture
Une décision durable par fichier : le contexte, la décision, ce qu'elle coûte. On n'efface
pas une décision : on en écrit une nouvelle qui la remplace, et on le note dans l'ancienne.
| # | Décision | Date |
|---|---|---|
| [0001](0001-rust-svelte.md) | Rust + Svelte, une image, SQLite | 2026-10-21 |
| [0002](0002-vote-permanent.md) | Vote permanent : marge du texte en place et plancher C | 2026-10-21 |
| [0003](0003-corriger-n-est-pas-voter.md) | Corriger n'est pas voter | 2026-10-21 |
| [0004](0004-contradictions-signalees.md) | Contradictions : signalées par les personnes, bloquantes | 2026-10-21 |
| [0005](0005-publication-git.md) | Publication par file d'envoi vers un dépôt git | 2026-10-21 |
| [0006](0006-acces-connect-trustinwallet.md) | Accès : connect.monnaie-libre.fr, invitations, trustinWallet | 2026-10-21 |
+20
View File
@@ -0,0 +1,20 @@
# Les chantiers
Un chantier à la fois, **livré complet et en production** avant d'ouvrir le suivant. Chaque
chantier a sa fiche (`CHANTIER.md` : ce qu'on construit et pourquoi), ses contrôles d'usage
(`CONTROLES.md`) et sa branche commune `chantier/<nom>`.
| # | Chantier | Outil | Ce qu'il ajoute | État |
|---|---|---|---|---|
| 1 | [Acte d'engagement Certification](acte-certification/CHANTIER.md) | Docs de référence | le texte sous vote permanent, l'édition distinguée du vote, les contradictions, l'inertie votée, la publication git, l'accès Ğ1 + trustinWallet, le trilingue | **en cours** — binôme Yvv · Chiara |
| 2 | Acte d'engagement forgeron | Docs de référence | la **qualification d'une compétence** (au-delà de l'engagement personnel) | à ouvrir après la mise en production du 1 |
| 3 | Runtime Upgrade | Protocoles | un **protocole** : décision on-chain d'une mise à jour du runtime Duniter | après le 2 |
Les outils Décisions et Mandats restent grisés tant qu'aucun chantier ne les ouvre.
## Ce qu'un chantier doit laisser derrière lui
- la boucle verte (Rust, interface, e2e) et l'image déployée sur decision.librodrome.org ;
- les contrôles d'usage passés par Chiara (ses retours valent contrôles) ;
- la fiche à jour : décisions prises, questions ouvertes, ce qui reste ;
- les décisions d'architecture durables consignées dans `docs/adr/`.
@@ -0,0 +1,154 @@
# Chantier 1 — Acte d'engagement Certification
> Binôme : **Yvv** (porteur) · **Chiara** (testeuse et codeuse — ses tests d'usage valent
> contrôles). Branche commune : `chantier/acte-certification`. Ouvert le 2026-10-21.
> Production : https://decision.librodrome.org
## Le besoin, en une phrase
La communauté Monnaie libre tient son Acte d'engagement Certification comme un **texte vivant** :
lisible d'abord, modifiable par article, chaque modification passant par un vote dont le seuil
suit la formule inertielle de la Ğ1 — sans qu'une coquille demande un vote, sans que deux
articles contradictoires soient en vigueur ensemble, et avec une publication automatique que
les applications clientes (Cesium, Ğecko…) peuvent suivre.
## Ce que voit la personne
1. **Accueil sobre** : « Rendre possible la décision collective » / « en fonction du contexte,
processus et modalités, boîte à outils », les quatre outils en une ligne chacun, puis la
connexion. Le menu complet à gauche : Décisions · Docs de référence · Mandats · Protocoles
(seul Docs de référence est actif).
2. **Connexion Ğ1** — la même que le forum Monnaie libre (connect.monnaie-libre.fr). En attendant
l'inscription du client OAuth2, des **liens d'invitation** à usage unique.
3. **Groupes** : avec une clé Ğ1, on voit Monnaie libre, Axiom, Librodrome et le Groupe local
Vallée Drôme Crestois — pas l'Atelier du canal. Un groupe et son contenu ne s'ouvrent qu'à
ses membres ; Monnaie libre s'ouvre à tout membre de la toile de confiance, les autres
groupes sur accord dans trustinWallet.
4. **Le texte en vigueur**, seul, en grand, typographie de lecture (Literata) : au départ le
Préambule et les deux engagements fondamentaux (Unicité du compte, Réciprocité), marqués
*seed a minima · 0 vote*. Au survol (ou toujours visibles sur mobile), trois gestes discrets :
**Le vote** (en cours, inertie, formule, annexes) · **Les propositions** · **Proposer**. Ils
ouvrent un panneau latéral (une feuille sur mobile) dont l'état vit dans l'URL.
5. **En dessous**, les propositions en cours — les 30 autres articles du texte source, à zéro
vote, inertie standard — rangées par section (introduction, fondamental, technique,
conclusion, annexes, formule, inertie, ordonnancement), les variantes signalées.
6. **Deux aperçus** : « si les bien cotées passent » et « si tout passe ».
7. **Trois langues** : fr · en · es, pour l'interface comme pour le texte. Le français fait foi ;
une traduction devenue ancienne (le français a changé depuis) est signalée.
## Les règles
### Le seuil — formule inertielle Ğ1
```
R = C + B^W + (M + (1 − M)·(1 − (T/W)^G)) · max(0, T − C)
```
W = électorat (Monnaie libre : taille de la toile, lue sur la chaîne toutes les 6 h ; un groupe
géré : ses membres), T = votes exprimés, M = majorité, G = gradient, B = 0,1, **C = plancher de
votants « pour »** (Monnaie libre : **50** ; groupe d'essai : 1). Adoptée si « pour » ≥ ⌈R⌉.
| Inertie | M | G |
|---|---|---|
| basse | 50 % | 0,1 |
| standard (au départ, partout) | 50 % | 0,2 |
| haute | 60 % | 0,4 |
| maximale | 66 % | 0,6 |
Moteur : `crates/ld-engine/src/threshold.rs` (testé : 7224 / 120 votants → 94 voix requises).
### Le vote permanent
- Toute version d'un article (proposition ou texte en vigueur) se vote à tout moment.
- Une proposition **remonte en vigueur** dès qu'elle atteint son seuil **et** tient une marge
(pour − requis) plus large que la version en place. La version déplacée redevient une
proposition, avec ses votes. Un *seed a minima* a pour marge −C : la première proposition qui
atteint son seuil le remplace.
- Retirer un article est une proposition comme une autre (« retirer cet article »).
- Le balayage tourne à chaque vote et toutes les 5 minutes (la taille de la toile bouge).
### L'inertie se vote aussi
Chaque article porte son inertie. La changer est une proposition, **votée elle-même à
l'inertie du cran au-dessus** (« inertie + ») : alourdir ou alléger demande plus que modifier.
### Corriger n'est pas voter
| Geste | Qui | Effet sur les votes |
|---|---|---|
| Corriger une **coquille** (accents, casse, ponctuation, espaces ; ou jusqu'à 2 mots à 2 lettres près) | tout membre sur un texte de départ ou en vigueur ; l'auteur sur sa proposition | aucun — les votes restent, les traductions restent à jour |
| Modifier **le fond** d'une proposition | son auteur | nouvelle révision : les votes antérieurs sont « à reconfirmer », les signalements tombent |
| Modifier le fond d'un texte **en vigueur** ou d'un texte de départ | personne | refusé : on propose une nouvelle version |
Le classement est prudent : un chiffre ou un mot de négation (ne, pas, jamais, personne…)
touché rend la modification « de fond ». Moteur : `crates/ld-engine/src/edit.rs`.
### Deux articles qui se contredisent
Tout membre peut **signaler une contradiction** entre deux propositions, motif à l'appui. Tant
que le signalement tient, celle des deux qui n'est pas en vigueur **ne peut pas entrer en
vigueur**. Il se lève par son auteur (ou un administrateur), ou tombe quand la proposition
signalée est révisée sur le fond.
### Une décision par proposition
L'éditeur a un **assistant à règles** (pas d'IA pour l'instant ; plus tard, une IA locale, sans
abonnement) : il repère un texte qui porte plusieurs décisions et propose de le **découper en
brouillons**, signale une proposition très proche d'une existante, et un texte long. Moteur :
`crates/ld-engine/src/assist.rs`.
### Brouillon → proposition
On écrit en brouillon (libre, invisible des autres), on soumet ; la proposition est alors
ouverte au vote. Son auteur peut la retirer.
### Publication
Chaque entrée en vigueur (et la publication initiale) passe par une file d'envoi : le serveur
écrit `fr.md`, `en.md`, `es.md` (front matter : document, langue, source, mise à jour ; les
traductions marquées éditoriales) et `document.json`, commit et pousse sur
`git.duniter.org/Yvv/libredecision-textes`, chemin `monnaie-libre/acte-engagement-certification/`.
Échec → nouvel essai avec délai croissant. La clé de publication est générée par le serveur ;
sa partie publique s'affiche dans trustinWallet, à ajouter comme clé de déploiement (écriture)
du dépôt. Le groupe d'essai ne publie jamais.
### Accès — trustinWallet
Tout le monde connecté peut tout faire dans les groupes qu'il ouvre (lire, proposer, voter,
corriger, signaler) **sauf la gestion des accès**, réservée aux administrateurs de trustinWallet
(`/acces`) : membres des groupes, invitations, administrateurs, retours, clé de publication.
Le nom trustinWallet remplace trustWallet (déjà pris). Règles : `crates/trustin-access`.
## Le groupe d'essai
Caché, plancher C = 1, sans publication : on y éprouve tout (adoption, coquilles, découpage)
sans toucher au texte de la communauté. Chiara et Yvv en sont membres.
## Décisions prises (2026-10-21, avec Yvv)
| Question | Décision |
|---|---|
| Stack | Rust (serveur, moteur) + Svelte (interface), pour la durée, la performance, la modularité |
| Domaine | le produit à la racine `decision.librodrome.org` ; la maquette sur `maquette.decision.librodrome.org` |
| Connexion | client OAuth2 de connect.monnaie-libre.fr (demande à poka) ; liens d'invitation en attendant |
| Dépôt des textes | `git.duniter.org/Yvv/libredecision-textes` (compte d'Yvv) |
| Garde-fou du vote permanent | nombre minimal de votants « pour » (C) |
| Langues | fr, en, es ; le français est voté, les traductions sont éditoriales |
| Chiara | testeuse et codeuse |
| Assistant | à règles d'abord ; IA locale plus tard, sans dépendance d'abonnement |
Les raisons longues sont dans `docs/adr/`.
## Questions ouvertes (à Yvv)
- **Licence** du code — non choisie.
- **Texte « Paramètres par défaut » du seed** (F1.2) : il annonce C = 0 et une durée de 30 jours,
alors que le vote est permanent avec C = 50. À reformuler par proposition (c'est leur rôle),
ou à corriger dans le seed avant la première publication si c'est une erreur de transcription.
- Anglais : l'interface est en anglais américain, le seed en anglais britannique.
## Ce qui reste après la livraison
- L'inscription du client OAuth2 (poka) : `DEMANDE-CONNECT.md`, puis les deux secrets dans la CI.
- Les retours de Chiara → contrôles → corrections, sur la branche du chantier.
- Ensuite : chantier 2, l'Acte d'engagement forgeron (qualification d'une compétence).
@@ -0,0 +1,42 @@
# Contrôles d'usage — Acte d'engagement Certification
Chaque contrôle existe en deux versions : **automatique** (Playwright, `web/e2e/`, rejoué par
`npm run e2e`) et **humaine** (Chiara, sur la production ou en local). Une remarque de Chiara
sur un contrôle vaut échec du contrôle tant qu'elle n'est pas traitée.
Où tester : les contrôles qui écrivent (C5, C6, C7) se font dans le **groupe d'essai**
(caché, plancher C = 1, jamais publié) — jamais sur le texte de la communauté.
| # | Contrôle | Geste humain | Attendu |
|---|---|---|---|
| C1 | L'accueil | ouvrir `/` sans être connectée | « Rendre possible la décision collective », la phrase d'outils, les quatre sections ; le menu à gauche avec les quatre outils, trois grisés ; « Se connecter avec sa clé Ğ1 » |
| C2 | Les groupes | se connecter, ouvrir « Groupes » | Monnaie libre, Axiom, Librodrome, Groupe local Vallée Drôme Crestois ; **pas** l'Atelier du canal |
| C3 | Le texte en vigueur | Monnaie libre → Docs de référence → Acte d'engagement Certification | trois articles seulement (Préambule, Unicité du compte, Réciprocité), chacun « seed a minima · 0 vote » ; en dessous, « Propositions en cours » (32 lignes) |
| C4 | Gestes contextuels | survoler le Préambule → « Le vote » | un panneau : seuil 50 voix pour, il en manque 50 ; « La formule du seuil » se déplie ; l'URL garde le panneau ; Échap le ferme |
| C5 | Entrée en vigueur | groupe d'essai : à deux, voter « Pour » sur « Les deux garanties réciproques » | au second vote, « … est entré en vigueur » ; le texte en vigueur compte quatre articles |
| C6 | Coquille sans vote | groupe d'essai : Préambule → « Le vote » → « Corriger une coquille », remplacer « latransmission » par « la transmission » | « Coquille corrigée, sans toucher aux votes. » ; un changement de sens est refusé : « Ce changement touche au sens : propose-le comme modification. » |
| C7 | Une décision par proposition | groupe d'essai : « Proposer un nouvel article », deux paragraphes « Je m'engage à… » | l'assistant : « Ce texte semble porter 2 décisions » ; « Découper en 2 brouillons » ; « Tes brouillons » apparaît |
| C8 | Trois langues | choisir EN puis ES pour le texte | « Certification Commitment Act », « Acta de compromiso de Certificación » ; l'interface suit |
| C9 | Les aperçus | « Si tout passe », puis « Si les bien cotées passent » | 33 articles avec la mention d'aperçu, puis 3 |
| C10 | trustinWallet | ouvrir `/acces` sans être administratrice | « La gestion des accès est réservée aux administrateurs de trustinWallet. » (côté admin : créer un lien d'invitation) |
| M1 | Mobile | téléphone (ou fenêtre étroite) | menu en tiroir, les trois gestes visibles sans survol, le panneau s'ouvre en feuille |
## Au-delà de la liste
Ce que l'automate ne voit pas et que Chiara regarde :
- **la lecture** : le texte est-il beau, aéré, sans bruit ? lit-on un article sans effort ?
- **la fatigue** : un geste de trop, une étape inutile, un mot qui arrête ;
- **le vocabulaire** : un libellé qui parle du système au lieu de parler de toi ou du groupe ;
- **les deux ambiances** (pierre / nuit) et le clavier seul (Tab, Entrée, Échap) ;
- **les langues** : une traduction maladroite, une chaîne restée en français.
Chaque remarque passe par « Faire un retour » (menu de gauche) : elle arrive datée, avec la
page, la langue et la taille de l'écran. Les retours se traitent dans trustinWallet
(Vu → Traité) et, s'ils changent une règle, dans `CHANTIER.md`.
## Journal des passages
| Date | Qui | Où | Contrôles | Résultat |
|---|---|---|---|---|
| 2026-10-21 | automate | local, serveur jetable :8092 | C1–C10, M1 | 11 / 11 verts |
@@ -0,0 +1,42 @@
# Demande à poka — client OAuth2 sur connect.monnaie-libre.fr
Brouillon prêt à envoyer (par Yvv, en message privé à poka sur forum.monnaie-libre.fr ou par
le canal habituel). Rien de secret dedans ; la réponse, elle, contiendra un secret.
---
Bonjour poka,
On ouvre libreDecision en production sur **https://decision.librodrome.org** : la communauté
y tient l'**Acte d'engagement Certification** comme un texte vivant, voté article par article
(formule inertielle Ğ1). On voudrait la même connexion que le forum : connect.monnaie-libre.fr.
Peux-tu inscrire un client OAuth2 ?
- **Nom affiché** : libreDecision
- **Site** : https://decision.librodrome.org
- **URI de redirection** : `https://decision.librodrome.org/api/auth/g1/callback`
- **Flux** : authorization code (`/oauth/authorize` → `/oauth/token` en basic auth →
`/oauth/userinfo`), sans scope particulier
- **Ce qu'on lit de `/oauth/userinfo`** : `id` (idty:N), `username`, `address`, `member` —
rien d'autre n'est stocké ; seuls les membres de la toile ouvrent le groupe Monnaie libre.
Il nous faudrait en retour le `client_id` et le `client_secret` (le secret par un canal privé,
pas sur le forum).
Merci !
Yvv
---
## À la réponse
1. Ne pas coller le secret dans une conversation : protocole `/tmp/pwd.txt` (l'assistant crée
et ouvre le fichier, Yvv colle).
2. Ajouter dans Woodpecker (dépôt LPK/decision) deux secrets : `connect_client_id`,
`connect_client_secret`.
3. **Ensuite seulement** (un `from_secret` absent fait refuser tout le pipeline) : dans
`.woodpecker/app.yml`, étape `deploy`, écrire `/opt/libredecision-app/secrets.env` avec
`LD_CONNECT_CLIENT_ID` et `LD_CONNECT_CLIENT_SECRET` (chmod 600) — le compose le lit déjà.
4. Contrôle : « Se connecter avec sa clé Ğ1 » mène à connect.monnaie-libre.fr et revient
connecté ; un membre de la toile ouvre Monnaie libre.
@@ -0,0 +1,72 @@
# Bienvenue sur le chantier — Chiara
Tu es **testeuse et codeuse** sur ce chantier : tes tests d'usage sont nos contrôles (une
remarque de toi = un contrôle à repasser), et tu peux corriger ou construire toi-même.
## 1. Ce qu'on construit
Lis d'abord `CHANTIER.md` (10 minutes) : le texte vivant, le vote permanent, coquille ≠ vote,
les contradictions, l'inertie votée, la publication. Puis `CONTROLES.md` : ce que tu vérifies.
## 2. Tes accès (préparés par Yvv)
| Quoi | Pour quoi | Qui le fait |
|---|---|---|
| Un **lien d'invitation** à decision.librodrome.org (groupes Monnaie libre + groupe d'essai) | tester en production avant la connexion Ğ1 | Yvv, dans trustinWallet (`/acces`) |
| Ta **clé Ğ1** | une fois connect.monnaie-libre.fr branché, tu te connectes comme sur le forum | — |
| Un compte sur **git.open.us.org**, collaboratrice de `yvv/decision` | pousser tes branches, ouvrir des demandes de fusion | Yvv |
Le lien d'invitation s'ouvre **une seule fois** et expire en 14 jours ; il ouvre ta session sur
ce navigateur pour 30 jours.
## 3. Tester sans rien installer
1. Ouvre ton lien d'invitation → tu arrives connectée.
2. Les contrôles qui écrivent (voter, corriger, découper) se font dans le **groupe d'essai** :
il est caché, une seule voix « pour » suffit à faire passer une proposition, et rien n'en
sort. Le texte de Monnaie libre, lui, est le vrai : lis-le, ne vote que pour de vrai.
3. Passe les contrôles C1–C10 et M1 (sur ton téléphone aussi), puis tout ce qui te gêne.
4. Chaque remarque : **« Faire un retour »** dans le menu de gauche. Écris ce que tu as fait, ce
que tu attendais, ce qui s'est passé. La page, la langue et la taille de l'écran sont jointes.
## 4. Coder en local
Prérequis : git, Node 22, Rust (rustup — la version 1.99 s'installe toute seule grâce à
`rust-toolchain.toml`).
```bash
git clone gitea@git.open.us.org:yvv/decision.git libreDecision && cd libreDecision
git switch chantier/acte-certification
curl --proto '=https' -sSf https://sh.rustup.rs | sh -s -- -y # si Rust n'est pas installé
cd web && npm ci && npm run build && cd ..
LD_DEV_LOGIN=1 cargo run -p ld-server # http://127.0.0.1:8002 — API + interface
# dans un second terminal, pour l'interface à chaud :
cd web && npm run dev # http://127.0.0.1:3002
```
`LD_DEV_LOGIN=1` affiche une connexion « développement » : tu tapes un nom, tu es membre de
tous les groupes (y compris l'essai). La base locale vit dans `data/` — supprime ce dossier pour
repartir du texte de départ.
Où sont les choses :
- les **règles** (seuil, coquille, découpage, aperçus) : `crates/ld-engine/` — pur, testé ;
- les **accès** : `crates/trustin-access/` ;
- l'**API** : `crates/ld-server/src/` (une route = une fonction ; scénarios dans `tests/api.rs`) ;
- l'**interface** : `web/src/routes/` (pages) et `web/src/lib/components/` ;
- les **textes affichés** : `web/src/lib/i18n/fr.ts` (puis `en.ts`, `es.ts`) — jamais en dur ;
- le **texte de départ** : `seeds/acte-engagement-certification.json`.
## 5. Ta boucle avant de pousser
```bash
cargo fmt --all --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace
cd web && npm run check && npm test && npm run build && npm run e2e
```
Tout vert, puis : `git switch -c chiara/<sujet>`, commits, `git push origin chiara/<sujet>`, et une
demande de fusion vers `chantier/acte-certification`. Le détail : `CONTRIBUTING.md`.
## 6. Si tu bloques
Écris à Yvv, ou laisse un retour dans l'outil en commençant par « BLOQUÉE : ».
+49
View File
@@ -0,0 +1,49 @@
# PostgreSQL
POSTGRES_DB=libredecision
POSTGRES_USER=libredecision
POSTGRES_PASSWORD=change-me-in-production
DATABASE_URL=postgresql+asyncpg://libredecision:change-me-in-production@localhost:5432/libredecision
# Backend
SECRET_KEY=change-me-in-production-with-a-real-secret-key
DEBUG=true
# Environment: development, staging, production
ENVIRONMENT=development
# Logging: DEBUG, INFO, WARNING, ERROR
LOG_LEVEL=INFO
# CORS
CORS_ORIGINS=["http://localhost:3002"]
# Database connection pool
DATABASE_POOL_SIZE=20
DATABASE_MAX_OVERFLOW=10
# Auth / Sessions
SESSION_TTL_HOURS=24
CHALLENGE_EXPIRE_SECONDS=300
# Rate limiting (requests per minute)
RATE_LIMIT_DEFAULT=60
RATE_LIMIT_AUTH=10
RATE_LIMIT_VOTE=30
# Duniter V2 RPC
DUNITER_RPC_URL=wss://gdev.p2p.legal/ws
DUNITER_RPC_TIMEOUT_SECONDS=10
# Blockchain cache TTL (seconds)
BLOCKCHAIN_CACHE_TTL_SECONDS=3600
# IPFS
IPFS_API_URL=http://localhost:5001
IPFS_GATEWAY_URL=http://localhost:8080
IPFS_TIMEOUT_SECONDS=30
# Frontend
NUXT_PUBLIC_API_BASE=http://localhost:8002/api/v1
# Docker / Production
DOMAIN=decision.librodrome.org
+118
View File
@@ -0,0 +1,118 @@
> **Maquette v2** — conservée telle quelle pour présenter le projet, en ligne sur
> https://maquette.decision.librodrome.org. Le produit (v3, Rust + Svelte) est à la racine du
> dépôt : voir `../README.md`. Les chemins ci-dessous sont relatifs à `maquette/` ; la référence
> produit de la maquette reste `../docs/dev/BLUEPRINT-V2.md`.
# libreDecision
Boîte à outils de la première démocratie — décider ensemble, du trio à la population.
libreDecision fait de la décision un geste quotidien : chaque question tient en une phrase,
l'outil propose le chemin le plus court — jamais plus lourd que l'enjeu — et tout ce qui est
décidé reste révisable, mesuré, consigné. Le même outil sert un trio d'amis, une association,
une coopérative ou la toile de confiance Ğ1 (marque blanche par construction).
## 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.
## Les 6 routes de décision
Le triage (moteur pur, toujours expliqué en une phrase, toujours contournable) oriente chaque
décision vers l'une de six routes :
| Route | Geste |
|---|---|
| **solo** | « Personne d'autre n'est concerné — décide. » 2 gestes, moins de 10 secondes, trace facultative |
| **record** | « Déjà tranché — je le consigne. » L'observatoire en un geste, adopté immédiatement |
| **mandate** | « Ton mandat couvre — décide, c'est tracé. » Trace obligatoire + fenêtre d'objection 48 h |
| **transmit** | « Le mandat d'un autre couvre — transmets-lui. » (ou conteste le mandat) |
| **advice** | « Réversible et petit cercle — demande leur avis, puis décide. » 72 h |
| **collective** | « Nous décidons. » Formulation, liste des concernés arrêtée, session de vote |
Dérogation asymétrique : alourdir est libre (1 tap journalisé) ; alléger exige une note motivée
et une fenêtre d'objection.
## Les 5 modalités
Chaque protocole est nommé et référencé par une clause du Pacte — le modifier, c'est amender
le Pacte.
1. **Consentement** — « Ça me va / J'objecte » : zéro objection maintenue = adopté. Le repli
universel : tout collectif en a un (invariant).
2. **Nuancé 6 niveaux** — de « Pas du tout » à « Tout à fait », jamais « contre » ;
histogramme visible, commentaire obligatoire sur les niveaux bas.
3. **Paramétrique** — décider au curseur : faisceau des positions, médiane vivante, carte
« Pour moi » ; la cristallisation finale est un geste humain daté du garant, jamais un
automatisme.
4. **Élection** — réservée aux mandats et aux problématiques d'échelle (6 méthodes de
nomination, vote blanc, jamais de départage par l'outil).
5. **Binaire inertiel** — le seuil `C + B^W + (M + (1−M)(1−(T/W)^G))·max(0, T−C)`, héritage
Ğ1 remis à sa place : l'outil de dernier recours des très grands corps. L'Atelier des
formules (`/textes/formules`) reste son école.
## Local-first
- **Aucun serveur requis** : tout l'état vit dans le navigateur (IndexedDB via idb-keyval,
une clé par collectif).
- **Export / import** de bundles JSON — la fédération, c'est l'échange de bundles, jamais une
table.
- **Seeds** livrés et importés par le même chemin que l'import utilisateur :
- `duniter-g1.bundle.json` — la toile Ğ1 : 33 clauses Certification + 59 clauses Forgeron
(textes votés intacts, provenance et décomptes conservés), protocoles, 3 mandats ;
- `atelier-du-canal.bundle.json` — association fictive de 12 membres, le test permanent de
marque blanche : décisions à tous les états, paramétrique vivant, épreuve du réel due.
- **7 gabarits** de collectif à la création (`/creer`) : page blanche, informel, association,
coopérative, commune, monnaie libre, institution symétrique.
## Stack
- **Frontend** : Nuxt 4 (SPA, SSR off) + Vue 3 + TypeScript + Nuxt UI v3 + Pinia + UnoCSS ;
npm ; port **3002**
- **Moteurs purs** : `frontend/app/engine/` — triage, machine à états, seuils, médiane
paramétrique, nuancé, périmètres — zéro dépendance Nuxt, testés vitest (**342 tests**)
- **Backend v1** (FastAPI + SQLAlchemy async) : conservé dans `backend/` pour la future
synchronisation multi-appareils — non requis pour utiliser ou développer le frontend
## Démarrage
```bash
cd frontend
npm install
npm run dev # http://localhost:3002
npm test # vitest — 342 tests
npm run build # build de production
```
## Structure v2
```
frontend/app/
engine/ # moteurs purs (barrel index.ts — seul point d'import)
data/ # persistence.ts (IndexedDB), templates.ts (7 gabarits), seeds/*.bundle.json
stores/ # collective.ts (le tenant) + decisions.ts (façade d'actions)
composables/ # useMood, useFeed, useSearch
components/ # par domaine : chemin/ decisions/ votes/ texts/ mandates/ feed/ onboarding/ common/
pages/ # 14 routes : / decider decisions(+id, vote, observatoire)
# textes(+slug, formules) mandats(+id, nouveau) creer donnees
lexicon.ts # source unique des libellés FR + test anti-lexique
types/domain.ts # le noyau du modèle
docs/dev/BLUEPRINT-V2.md # la référence produit
```
## Licence
Logiciel libre. Le fichier LICENSE sera posé avant la première publication.