Commit 8b31cd95 authored by Kourser's avatar Kourser
Browse files

Socle Kastell : noyau de persistance, boîte noire, administration d'instance

Première pierre du cockpit de gestion de crise. Le lot L0 pose le socle et le
lot L1 le noyau de persistance — la pièce qui ne se rattrape pas après coup.

Noyau de persistance (L1)
  Toute mutation de l'état d'une crise passe par enregistrer(), qui inscrit un
  événement puis projette, dans la même transaction. La garantie n'est pas
  déclarative : PostgreSQL attribue la séquence, l'horodatage et l'empreinte
  chaînée, et refuse par déclencheur toute modification, suppression ou
  troncature de la table des événements — y compris au propriétaire de la base.

  La main courante est une vue sur le journal, pas une table : consigner, c'est
  écrire un événement d'un type particulier. Le code ne peut pas diverger.

  etatALaSeq() reconstitue par rejeu l'état d'une crise à n'importe quel
  instant. Chaque module ajouté devra fournir sa lecture à date : c'est un
  critère de recette, pas une intention.

  L'export transporte la forme canonique de chaque charge telle que Postgres
  l'a hachée, pour qu'un vérificateur tiers contrôle la chaîne sans
  réimplémenter la normalisation jsonb ni exécuter notre code.

Administration de l'instance (L0)
  Un compte root administre organisations, comptes et quotas, mais ne lit pas
  le contenu des crises au titre de son statut. Pour cela il ouvre un accès
  exceptionnel : motif circonstancié obligatoire, borné à huit heures par une
  contrainte du schéma, journalisé au nom de l'organisation concernée. Le
  journal d'administration a les mêmes garanties que le journal de crise —
  les actes du root sont ceux qu'il ne faut pas pouvoir effacer.

Socle et déploiement (L0)
  Monorepo TypeScript, migrations appliquées au démarrage sous verrou,
  configuration validée au lancement avec refus des valeurs d'exemple en
  production. Dockerfile unique multi-étapes, image de 164 Mo en utilisateur
  non privilégié et système de fichiers en lecture seule ; composition
  mono-hôte avec sondes de vivacité et de disponibilité.

Vérification
  `pnpm verif` applique les migrations, joue une crise complète et contrôle 31
  garanties rattachées une à une aux exigences du cahier des charges : refus de
  mutation du journal, séquence sans trou, détection d'une charge altérée,
  d'une troncature et d'une réorganisation, reconstitution d'un état passé,
  rectification sans effacement, bornes du bris de glace.

Pas d'ORM : le schéma porte les garanties, les masquer les rendrait
négociables. Les migrations SQL font autorité.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parents
Loading
Loading
Loading
Loading

.dockerignore

0 → 100644
+15 −0
Original line number Diff line number Diff line
# Le contexte de construction ne doit contenir que des sources.
# Sans cette exclusion, les node_modules de l'hôte (binaires macOS) écrasent
# ceux installés dans l'image et la construction échoue.
node_modules
**/node_modules
dist
**/dist
.env
.env.*
!.env.example
.git
.gitignore
*.log
.DS_Store
cahier-des-charges.html

.env.example

0 → 100644
+29 −0
Original line number Diff line number Diff line
# ── Kastell de crise — configuration ────────────────────────────────────────
# Copier en .env puis remplacer TOUTES les valeurs marquées CHANGEZ-MOI.
# Le serveur refuse de démarrer si une valeur par défaut subsiste (EX-26, EX-27).

# Identité de l'instance
KASTELL_URL_PUBLIQUE=http://localhost:8080
KASTELL_PORT=8080
KASTELL_ENV=developpement

# Base de données
POSTGRES_USER=kastell
POSTGRES_PASSWORD=CHANGEZ-MOI-mot-de-passe-base
POSTGRES_DB=kastell
# URL vue depuis la machine hôte (outillage : pnpm migrate, pnpm verif).
# Dans la composition, l'application reconstruit la sienne avec l'hôte "postgres".
DATABASE_URL=postgres://kastell:CHANGEZ-MOI-mot-de-passe-base@localhost:5432/kastell

# Cache et files
REDIS_URL=redis://localhost:6379

# Stockage objet (S3)
S3_ENDPOINT=http://localhost:9000
S3_REGION=eu-west-1
S3_BUCKET=kastell
S3_ACCES=kastell
S3_SECRET=CHANGEZ-MOI-secret-stockage

# Secrets applicatifs — générer avec: openssl rand -hex 32
SECRET_SESSION=CHANGEZ-MOI-secret-session-64-caracteres-hexadecimaux

.gitignore

0 → 100644
+9 −0
Original line number Diff line number Diff line
node_modules/
dist/
.env
.env.local
*.log
.DS_Store
coverage/
.pnpm-store/
*.tsbuildinfo

README.md

0 → 100644
+125 −0
Original line number Diff line number Diff line
# Kastell

Cockpit de gestion de crise d'entreprise. Application web qui prend le relais du système d'information quand on ne peut
plus lui faire confiance. Elle prépare la crise à froid, la conduit à chaud, et
l'enregistre intégralement — de manière à pouvoir la rembobiner.

**Spécification :** [cahier-des-charges.html](cahier-des-charges.html)

---

## État d'avancement

| Lot | Contenu | État |
|-----|---------|------|
| **L0** | Socle, conteneurisation, compte root, migrations, sondes | 🟡 en cours — Docker, migrations et modèle root faits, authentification à venir |
| **L1** | Noyau de persistance, main courante, rembobinage | 🟡 en cours — noyau et lecture à date faits, filtres et export PDF à venir |
| L2 | Dossier de préparation (annuaire, tiers, fiches réflexes) | ⚪ |
| L3+ | Décisions, chat, GED, visio, tableau blanc, mobilisation | ⚪ |

Ce qui fonctionne aujourd'hui : le journal inviolable, le chaînage d'empreintes,
le vérificateur indépendant, la reconstitution d'un état passé par rejeu, la
main courante avec rectification sans effacement, le compte root avec son
journal d'administration et le bris de glace, et la pile Docker complète.

---

## Démarrage

Prérequis : Docker, Node ≥ 22, pnpm.

```bash
cp .env.example .env
# renseigner les valeurs CHANGEZ-MOI : openssl rand -hex 32
pnpm install
pnpm up          # postgres, redis, minio, application
```

L'application écoute sur `http://localhost:8080` (`/sante`, `/pret`).

Pour travailler sur le code sans reconstruire l'image à chaque fois :

```bash
docker compose -f docker/compose.yaml --env-file .env up -d postgres redis minio
pnpm dev
```

### Vérifier l'installation et les garanties

```bash
pnpm verif
```

Ce contrôle applique les migrations, joue une crise complète, puis vérifie une
à une les garanties de la boîte noire : refus de modification et de suppression
d'un événement, séquence sans trou, chaîne d'empreintes valide, détection d'une
falsification par un vérificateur qui n'utilise pas la base, et reconstitution
d'un état passé. Chaque ligne renvoie à l'exigence du cahier des charges qu'elle
démontre.

---

## Structure

```
apps/api/src/
  noyau/          le noyau de persistance — à lire en premier
    enregistrer.ts   l'unique chemin d'écriture vers l'état d'une crise
    projections.ts   traduction d'un événement en état interrogeable
    rejeu.ts         lecture à date : le moteur du rembobinage
    integrite.ts     chaînage, export, vérificateur indépendant
    administration.ts  compte root, journal d'administration, bris de glace
  domaine/        opérations métier, qui n'écrivent que via le noyau
  db/migrations/  le schéma fait autorité, y compris sur les garanties
packages/shared/  catalogue des événements, partagé avec le web et le mobile
docker/           Dockerfile unique et composition mono-hôte
```

## La règle à ne jamais enfreindre

Toute mutation de l'état d'une crise passe par `enregistrer()`. Sans exception.

La contrainte n'est pas déclarative : la séquence, l'horodatage et l'empreinte
sont posés par PostgreSQL, et la table `evenement` refuse par déclencheur toute
modification ou suppression — y compris au propriétaire de la base. Une écriture
qui contournerait `enregistrer()` produirait un état invisible du rembobinage.

C'est le seul défaut de ce produit qui ne se rattrape pas après coup.

**Corollaire pour tout lot à venir :** un module doit fournir sa lecture à une
séquence donnée (`etatALaSeq`) en même temps que sa lecture au temps présent.
C'est un critère de recette, pas une intention.

## Administration de l'instance

Un compte **root** administre l'instance : organisations, comptes, quotas,
santé, intégrité. Il **ne lit pas** le contenu des crises au titre de son
statut. Pour cela il ouvre un accès exceptionnel — motif circonstancié
obligatoire, borné à huit heures par une contrainte du schéma, journalisé au
nom de l'organisation concernée et notifié à ses propriétaires.

Le journal d'administration a les mêmes garanties que le journal de crise :
ajout seul, séquence sans trou, chaînage d'empreintes. Les actes du root sont
précisément ceux qu'il ne faut pas pouvoir effacer — y compris par le root.

Voir §7 du cahier des charges.

## Choix techniques notables

- **Pas d'ORM.** Le schéma porte les garanties (déclencheurs, révocations,
  contraintes) ; les masquer derrière une abstraction les rendrait négociables.
- **La main courante est une vue**, pas une table. Consigner, c'est écrire un
  événement d'un type particulier — le code ne peut pas diverger du journal.
- **L'export transporte la forme canonique** de chaque charge telle que
  PostgreSQL l'a hachée, pour qu'un tiers vérifie la chaîne sans réimplémenter
  la normalisation `jsonb`.
- **Le rejeu revalide** chaque charge à travers le schéma de son type : un
  journal se relit des années après son écriture, et une évolution du catalogue
  doit produire une erreur franche plutôt qu'une projection silencieusement
  fausse.

## Licence

À confirmer — **AGPL-3.0** proposée (voir §12 du cahier des charges). Le fichier
`LICENSE` sera ajouté une fois la décision prise ; le dépôt n'est pas encore
public.

apps/api/package.json

0 → 100644
+23 −0
Original line number Diff line number Diff line
{
  "name": "@kastell/api",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx --env-file=../../.env watch src/index.ts",
    "migrate": "tsx --env-file=../../.env src/db/migrate.ts",
    "verif": "tsx --env-file=../../.env src/verif.ts",
    "build": "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --banner:js=\"import{createRequire}from'module';const require=createRequire(import.meta.url);\""
  },
  "dependencies": {
    "@kastell/shared": "workspace:*",
    "fastify": "^5.2.0",
    "postgres": "^3.4.5",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@types/node": "^22.10.2",
    "esbuild": "^0.24.2",
    "tsx": "^4.19.2"
  }
}
Loading