# ia_spe.md — Concevoir une application fwtozza (guide destiné à une IA)

Ce document s'adresse à une **IA à qui l'on demande de coder une application** avec le
framework fwtozza. L'humain te fournira une **description des interfaces et des
fonctionnalités** ; tu produiras en retour **les catalogues** (contrats de messages) et
**les composants** (objets standard), puis, si demandé, le câblage (`index.html`,
`server.js`).

Le modèle fwtozza lui-même est décrit normativement dans `fwtozza.md` — lis-le. Ce
guide-ci ne le répète pas : il ajoute ce qui **n'y ressort pas assez**, et qui fait la
différence entre une conception juste et une conception naïve : **le choix de la
granularité** — ne pas multiplier les composants ni les messages.

---

## 0. Ce qu'on te donne, ce que tu rends

**Entrée** (rédigée par l'humain) : les écrans/zones d'interface, et les fonctionnalités
attendues (données manipulées, actions, règles d'accès). Si une information te manque
pour trancher une frontière (une donnée est-elle partagée ? une action est-elle réservée
à un rôle ?), **demande-la** avant de coder.

**Sortie** (que tu proposes, dans cet ordre) :

1. **Le découpage** : la liste des composants (client et serveur) avec, pour chacun, sa
   responsabilité en une phrase, et pourquoi cette frontière (fonction / déploiement).
2. **Les catalogues** : les contrats de messages (`oneOf` regroupé, `examples` /
   `counterexamples`).
3. **Les composants** : les objets standard (`emits` / `receives` / `handleMessage`).
4. Le câblage si demandé.

Avant de rendre, **compte tes composants et tes messages**. S'ils sont nombreux au regard
de la taille de l'application, tu as probablement sur-découpé : relis la §2.

---

## 1. Rappel express du modèle (le détail est dans `fwtozza.md`)

- **Message** = enveloppe à clé unique `{ nom_du_message: { …payload… } }`. Le payload est
  de la **donnée JSON pure**, chaque champ **déclaré au catalogue**.
- **Contrat** = une entrée de catalogue, décrite par `versions` → `revisions` → `fields`.
  Vocabulaire des champs : `type`, `required`, `pattern`, `enum`, `default`, `fields`
  (sous-objet), `items` (éléments de liste), et **`oneOf`** au niveau d'une révision
  (exactement un champ présent parmi N ; ses membres restent **optionnels et sans
  `default`**).
- **Composant** = objet standard : des tableaux `emits` / `receives` (contrats déclarés),
  une méthode `handleMessage(message)`, une inscription `broker.register(this, emits,
  receives)`, un `destroy()`. **Le composant ne connaît que le broker** — jamais les
  autres composants.
- **Broker** = livraison FIFO à épuisement, validation stricte (payload hors contrat jeté
  et signalé).
- **Client ↔ serveur** = le **même** modèle, relié par un **canal**. Deux clés d'enveloppe
  réservées : **`id`** (cibler une ou plusieurs sessions ; sans `id`, diffusion à tous) et
  **`session`** (état de session, propagé **par référence** côté serveur).
- **Auth** = un composant serveur écrit l'identité dans `session.data.user` au login, et
  pose la grille d'admission `session.allowMessage(name, payload)` au `connected` (via
  `connection_event`). Le canal appelle cette grille sur chaque entrant ; message refusé =
  jeté.

---

## 2. Doctrine de granularité — le cœur de ce guide

fwtozza porte son intérêt dans la **modularité des fonctionnalités** et leur
**dissociation client/serveur**, pas dans l'émiettement de l'interface. Quatre règles.

### 2.1 Un composant = une fonction cohérente — jamais une action, ni un widget

Découpe par **fonction et/ou déploiement**, pas par morceau d'écran ni par action.

- **Un composant qui gère un agrégat de 1 à 3 tables associées** vaut mieux qu'un composant
  par action et par table. `clients` + ses `contacts` (une table maîtresse et ses tables
  liées, toujours lues/écrites ensemble) = **un** composant serveur, pas six
  (`lireClient`, `creerClient`, `lireContacts`…).
- **À l'intérieur d'un composant, programme normalement** : POO classique ou React,
  collections d'objets ordinaires. fwtozza ne régit que les **frontières entre
  composants**, jamais leur mécanique interne. Une liste et sa zone de saisie, une table et
  son formulaire, vivent dans **le même** composant.

**Heuristique de frontière** — deux morceaux méritent d'être deux composants seulement si
tu peux répondre « oui » à l'une de ces questions :

- pourraient-ils être **déployés séparément** (l'un au client, l'autre au serveur ; l'un
  dans un autre processus) ?
- l'un est-il **réutilisable / affichable indépendamment** de l'autre ?
- relèvent-ils de **cycles de vie ou de responsabilités franchement différents** (ex.
  `clients` vs `factures` : mêmes écrans, mais durées de vie, règles et volumétrie
  distinctes → deux agrégats, deux composants) ?

Sinon : **un seul composant.**

### 2.2 Un message = une capacité — regroupe les opérations avec `oneOf`

Préfère **un** message d'opérations à une pléiade de messages-verbes.

```javascript
// BIEN — un contrat, une souscription, une évolution commune :
gerer_clients: {
	versions: { 1: { revisions: { 0: {
		oneOf: ["read", "create", "update", "delete"], // exactement un membre présent
		fields: {                                       // membres oneOf : optionnels, sans default
			read:   { type: "object", fields: { filtre: { type: "object", required: true, fields: {
				nom: { type: "string" },
			} } } },
			create: { type: "object", fields: { client: { type: "object", required: true, fields: {
				nom:   { type: "string", required: true },
				email: { type: "string", required: true, pattern: "^[^@]+@[^@]+$" },
			} } } },
			update: { type: "object", fields: {
				id:    { type: "number", required: true },
				patch: { type: "object", required: true },
			} },
			delete: { type: "object", fields: { id: { type: "number", required: true } } },
		},
	} } } },
	examples: { 1: { 0: [
		{ read:   { filtre: {} } },
		{ create: { client: { nom: "Acme", email: "a@b.co" } } },
		{ delete: { id: 42 } },
	] } },
	counterexamples: { 1: { 0: [
		{ reason: "aucun membre oneOf",  payload: {} },
		{ reason: "deux membres oneOf",  payload: { read: { filtre: {} }, delete: { id: 42 } } },
	] } },
},
```

À fuir : `lire_clients`, `creer_client`, `maj_client`, `suppr_client`… — une nuée de
contrats à déclarer, versionner et souscrire séparément.

On désigne un couple (message, membre) par la notation **`gerer_clients.read`**,
`gerer_clients.update`, etc.

**Pourquoi regrouper :** moins de contrats à maintenir ; une seule souscription ; les
opérations d'une même capacité **évoluent ensemble** (une révision les couvre toutes) ; et
surtout, la grille d'admission `allowMessage` peut les **autoriser au grain de
l'opération** (§2.3).

**Quand ne PAS regrouper** (pour ne pas sur-fusionner) : sépare en messages distincts si
les opérations ont des **abonnés différents**, des **cycles de vie disjoints**, ou passent
une **frontière de déploiement différente**. Exemple : `gerer_clients` (client → serveur,
requête) et `clients_result` (serveur → client, réponse) sont **deux** messages — sens de
circulation opposé, abonnés opposés.

### 2.3 L'authentification se branche sur cette granularité via `allowMessage`

La grille reçoit **le nom du message et son payload** — elle voit donc quel membre `oneOf`
est présent et peut autoriser **par sous-opération**, avec `this === session` et l'identité
dans `session.data.user` :

```javascript
// posé par le composant d'auth sur connection_event 'connected' :
session.allowMessage = (name, payload) => {
	const user = session.data.user;
	if (!user) return name === "login";               // avant login : seul le login passe
	if (name === "gerer_clients") {
		if (payload.delete) return user.role === "admin"; // suppression réservée aux admins
		return true;                                       // read / create / update : tout connecté
	}
	return true;
};
```

C'est exactement ce que le regroupement `oneOf` rend possible : **une** autorisation, au
grain fin, sans multiplier les messages.

### 2.4 Les résultats aussi : un message de retour, ciblé — pas un par opération

Un composant serveur répond en publiant **un** message de résultat **ciblé sur la session
demandeuse** par la clé d'enveloppe `id` :

```javascript
this.broker.publish(this, { clients_result: { ok: true, rows }, id: message.id });
```

Un seul contrat `clients_result` (au besoin lui-même en `oneOf` si les formes de retour
diffèrent vraiment), pas un `resultat_lecture`, `resultat_creation`, etc.

### 2.5 Tableau anti-patterns

| Réflexe naïf | Conception fwtozza |
|---|---|
| un composant par action sur chaque table | **un composant par agrégat** de 1–3 tables liées |
| micro-composants d'UI (liste, champ, bouton) reliés par le broker | **un composant** par zone fonctionnelle ; UI interne libre (POO/React) |
| `lire_x`, `creer_x`, `maj_x`, `suppr_x` … | **un** message `gerer_x` avec `oneOf: [read, create, update, delete]` |
| un message de résultat par opération | **un** `x_result`, ciblé par `id` |
| autoriser en éparpillant la logique dans chaque composant | **`allowMessage`** central, au grain du membre `oneOf` |
| reconnaître un composant par un nom d'instance passé en payload | composants **uniques** ; corréler un échange par un `ref` **de message** ; cibler un client par `id` de session |

---

## 3. Procédure quand tu reçois une spécification

1. **Inventorie** les données (tables) et les fonctionnalités (actions, écrans, règles
   d'accès).
2. **Regroupe les données en agrégats** de 1 à 3 tables liées. Un agrégat = **un composant
   serveur**.
3. Pour chaque agrégat, définis **un** message d'opérations `gerer_<agrégat>` avec un
   `oneOf` couvrant ses actions, et **un** message de résultat `<agrégat>_result` (ciblé
   par `id`).
4. **Trace les frontières client/serveur** : ce qui doit être relocalisable ou déployé à
   part devient un composant + un message de transit (déclaré partagé, autorisé au canal).
5. **Côté client**, un composant par **zone fonctionnelle** cohérente (une vue et ses
   contrôles ensemble), pas par widget.
6. **Pose l'authentification** : un composant serveur abonné à `login` écrit
   `session.data.user`, et sur `connection_event` `connected` installe `session.allowMessage`
   (au minimum un *deny-sauf-login* synchrone, resserré ensuite). Gate au grain `oneOf`.
7. **Écris les catalogues** (contrats, `oneOf`, `examples` / `counterexamples` qui prouvent
   chaque forme et rejettent les mauvaises), **puis** les composants.
8. **Contrôle final** : chaque message a un émetteur **et** au moins un abonné ; aucun couple
   de composants ne se distingue par un nom d'instance ; le nombre de messages et de
   composants reste **proportionné** à l'application (sinon, re-regroupe).

---

## 4. Exemple travaillé — mini-CRM

**Spécification fournie (extrait de ce qu'écrirait l'humain).** Une application interne :
un écran « Clients » (liste + fiche avec les contacts du client) et un écran « Factures ».
On peut lire/créer/modifier/supprimer un client et ses contacts ; lire/créer une facture.
La suppression d'un client est réservée aux administrateurs. Connexion par login.

**Découpage proposé.**

- `AuthBoard` (serveur) — login → `session.data.user` ; pose `allowMessage`.
- `ClientsBoard` (serveur) — agrégat **clients + contacts** (tables liées, toujours
  manipulées ensemble → un seul composant).
- `FacturesBoard` (serveur) — agrégat **factures** (cycle de vie distinct → composant à
  part).
- `ClientsPanel` (client) — la zone « Clients » entière : liste, fiche, contacts,
  formulaires. Un composant, mécanique interne libre.
- `FacturesPanel` (client) — la zone « Factures ».
- `LoginPanel` (client) — la connexion.

Six composants pour deux agrégats métier + l'auth : c'est proportionné. La tentation naïve
(un composant par action × par table) en aurait produit une quinzaine, tous couplés au
broker pour rien.

**Catalogue partagé (extrait).** Une capacité = un message `oneOf` ; un résultat ciblé.

```javascript
export const SHARED = {

	login: {
		versions: { 1: { revisions: { 0: { fields: {
			user: { type: "string", required: true },
			pass: { type: "string", required: true },
		} } } } },
		examples:        { 1: { 0: [ { user: "ana", pass: "x" } ] } },
		counterexamples: { 1: { 0: [ { reason: "pass manquant", payload: { user: "ana" } } ] } },
	},

	gerer_clients: { /* oneOf: [read, create, update, delete] — cf. §2.2 */ },
	clients_result: {
		versions: { 1: { revisions: { 0: { fields: {
			ok:   { type: "boolean", required: true },
			rows: { type: "array", items: { type: "object" } }, // optionnel
		} } } } },
		examples: { 1: { 0: [ { ok: true, rows: [] }, { ok: false } ] } },
	},

	gerer_factures: { /* oneOf: [read, create] */ },
	factures_result: { /* comme clients_result */ },

};
```

**Composant serveur `ClientsBoard` (esquisse).** Il traite les quatre opérations d'un même
message et répond, ciblé, à la session demandeuse. L'autorisation est déjà faite en amont
par `allowMessage` — le canal n'aura pas livré ici un membre `oneOf` interdit.

```javascript
handleMessage(message) {
	if (!message.gerer_clients) return;
	const op = message.gerer_clients;
	let rows;
	if (op.read)   rows = this.#read(op.read.filtre);
	if (op.create) rows = this.#create(op.create.client);
	if (op.update) rows = this.#update(op.update.id, op.update.patch);
	if (op.delete) rows = this.#delete(op.delete.id);
	this.broker.publish(this, { clients_result: { ok: true, rows }, id: message.id });
}
```

**Composant serveur `AuthBoard` (esquisse).** Login → identité en session ; `connected` →
grille d'admission au grain `oneOf` (cf. §2.3).

```javascript
handleMessage(message) {
	if (message.login) {
		const user = this.#authenticate(message.login);
		if (user) message.session.data.user = user;
		this.broker.publish(this, { login_result: { granted: !!user }, id: message.id });
	}
	// un composant d'auth par canal : on filtre sur le canal d'où vient l'événement
	if (message.connection_event?.event === "connected" && message.connection_event.channel === this.canal) {
		const session = message.session;
		session.allowMessage = (name, payload) => {
			const user = session.data.user;
			if (!user) return name === "login";
			if (name === "gerer_clients" && payload.delete) return user.role === "admin";
			return true;
		};
	}
}
```

**Composant client `ClientsPanel` (esquisse).** Toute la zone « Clients » dans un seul
composant : il émet la capacité, reçoit le résultat, gère son affichage en interne.

```javascript
emits    = [{ name: "gerer_clients", version: 1 }];
receives = [{ name: "clients_result", version: 1 }];

// un clic « supprimer » dans la fiche :
this.broker.publish(this, { gerer_clients: { delete: { id } } });

handleMessage(message) {
	if (message.clients_result) this.#rafraichir(message.clients_result.rows);
}
```

---

## 5. Récapitulatif à te répéter avant de rendre

- **Découpe par fonction/déploiement**, pas par écran ni par action.
- **Un agrégat de 1–3 tables liées = un composant.** L'UI d'une zone = un composant.
- **Une capacité = un message `oneOf`**, pas une pléiade de verbes. Un résultat ciblé par
  `id`.
- **`allowMessage` autorise au grain du membre `oneOf`** ; l'identité vit dans
  `session.data.user`.
- **Composants uniques** : jamais de nom d'instance en payload ; corréler par `ref`, cibler
  par `id`.
- **Compte** composants et messages : trop nombreux ⇒ re-regroupe.
