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
+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`.