L’Invariante
Una mutazione di business o committa in modo atomico — transizione FSM, scrittura data-link, riga di audit, evento outbox CDC — oppure non esiste. Non c’è stato parziale: un constraint fallito fa rollback della transazione e lascia solo l’evidenza in audit. Lo stesso Idempotency-Key restituisce sempre lo stesso esito.
Il Contratto
POST /api/v1/actions/:trigger sull’edge è l’unica superficie di mutazione. Ogni richiesta attraversa la stessa pipeline fail-closed, in ordine:
auth → idempotenza → RBAC → constraint → FSM → data-link → audit → commit
Un rifiuto a qualsiasi stadio interrompe la richiesta — nulla a valle viene eseguito.
Il Flusso
- auth — PIN operatore o device key risolvono tenant e ruoli dalla sessione autenticata, mai dai campi payload.
- idempotenza — l’header
Idempotency-Keyviene deduplicato; un replay restituisce la risposta originale. - RBAC — le permission
rbac_enginedel manifest mappano ruoli → trigger ammessi;OPERATOREnon può lanciareQC_PASS. - constraint — le
pre_commit_ruleslegate al trigger vengono valutate; una violazione risponde422 CONSTRAINT_VIOLATED. - FSM — il trigger deve corrispondere a una transizione
from → todichiarata per lo stato corrente dell’aggregato. - data-link — la query manifest del trigger viene eseguita nella stessa transazione.
- audit + commit — riga di audit ed evento
ax_cdc_outboxvengono scritti col commit; il sync drena in seguito.
Esempio di Payload
# Un operatore qualità chiude un collaudo — il constraint engine verifica la misura
curl -X POST http://edge:8291/api/v1/actions/QC_PASS \
-H "Authorization: Bearer $EDGE_TOKEN" \
-H "Idempotency-Key: qc-L0043-001" \
-H 'content-type: application/json' \
-d '{
"payload": {
"piece_id": "PZ-9044",
"misura": 0.018
}
}'
# → 200 { committed: true, to_state: "CONFORME" }
#
# La stessa chiamata con "misura": 0.026 viola `tolleranza_collaudo`
# → 422 { error: "CONSTRAINT_VIOLATED", rule: "tolleranza_collaudo" }
# → nessun commit, nessuna riga di business — la catena di audit conserva il tentativo
I Fallimenti Noti
| Sintomo | Causa |
|---|---|
401 |
Sessione mancante/scaduta — rifare login via /api/v1/auth/login |
403 |
Il ruolo non ha il trigger in rbac_engine.permissions — è un diniego, non un bug |
409 |
Transizione FSM non dichiarata dallo stato corrente (es. QC_PASS prima di START_MEASURE) |
422 CONSTRAINT_VIOLATED |
Una pre_commit_rule è fallita — il payload è fuori dall’inviluppo dichiarato |
| Un retry ha successo in silenzio | Lo stesso Idempotency-Key ha restituito la risposta originale in cache — by design |