L’Invariante
Un nuovo verticale non modifica mai un motore esistente. Entra nella piattaforma come hanno fatto tutti i motori: un bounded context, fatti in ingresso via kernel, proiezioni in uscita dallo stream eventi. Se il tuo verticale richiede di cambiare un motore esistente, il design è sbagliato — correggi il contratto, non l’accoppiamento.
Il Contratto
Cosa deve produrre/consumare un verticale per essere “nativo”:
interface VerticalContract {
manifest: { // registrato in ax_manifests
triggers: Trigger[]; // azioni INSERT/UPDATE + letture LIST_*
roles: Record<string, string[]>; // ruolo → permessi trigger
migrations: string[]; // SQL solo per le proprie tabelle biz_*
};
factsIn: 'POST /api/v1/actions/:trigger'; // sempre — mai SQL diretto
factsOut: 'cp_events | biz_* LIST_*'; // leggi lo stream, o letture locali
}
Regole dure: le nuove tabelle sono biz_<verticale>_* e private del runtime; gli eventi seguono il Contratto Zero.
Il Flusso
1. DEFINISCI IL CONTRATTO
Schema eventi secondo il Contratto Zero: append-only, immutabili,
niente update-in-place. Decidi i confini dell'aggregato
(cos'è "il lotto" del tuo verticale?).
2. REGISTRA IL MANIFEST
trigger + ruoli + migrazioni → ax_manifests → publish → pull → verify.
Il runtime fa hot-swap; nessun redeploy di alcun motore.
3. COLLEGA PRODUCER / CONSUMER
Producer: chiama /api/v1/actions/:trigger (edge) — dashboard,
integrazioni macchina, plugin usano lo stesso endpoint.
Consumer: legge le proiezioni cp_events — mai richiama il producer.
4. VALIDA CON LA TEST SUITE
La suite node:test del repo (path trigger/FSM/RBAC) + test di contratto
sui payload. Suite verde ⇒ il verticale è nativamente compatibile con
dashboard, catena di audit e canale sync — non serve altro.
Esempio di Payload
// Un nuovo verticale: "energia". Un nuovo tipo di fatto, zero modifiche ai motori.
// trigger manifest RECORD_ENERGY → biz_energy_samples (tabella propria!)
POST /api/v1/actions/RECORD_ENERGY
{ "payload": { "sample_seq": 9081, "kwh": 1.42, "power_factor": 0.93,
"lot_id": "<lotto-aperto-legato>" } }
// → audit + outbox + cp_events gratis; portale e admin lo vedono già.
I Fallimenti Noti
| Errore | Conseguenza → fix |
|---|---|
| Il verticale scrive la tabella di un altro motore | Isolamento rotto → sposta la scrittura dietro un trigger azione tuo |
| SQL diretto “temporaneo” per velocità | I fatti bypassano audit/outbox → violazione del contratto, rifiuta la PR |
| Righe di “stato” mutabili nella tua tabella | Redesign: lo stato = l’ultimo evento; archivia eventi, leggi l’ultimo |
| Il verticale serve dato di un altro motore | Leggi cp_events/proiezioni — se manca, pubblica un fatto più ricco a monte |