# 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.

## 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

## 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

## 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)
```

## Données runtime

- **postgres-data** / **ipfs-data** : volumes Docker du backend v1 — jamais écrasés
- `.env` racine : secrets backend — jamais commité

## Commandes

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

# 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
```

## 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)
