L’Invariante
Esiste un solo path di scrittura ed è questo. POST /api/v1/actions/:trigger sul nodo edge è l’unico modo in cui un fatto di business entra nel sistema — dalla dashboard operatore, da un’integrazione macchina o da qualsiasi client futuro. Se stai scrivendo qualcosa che bypassa questo endpoint, stai costruendo il bug che questa architettura esiste per prevenire.
Il Contratto
POST /api/v1/actions/:trigger
Authorization: Bearer <sessione operatore>
X-Idempotency-Key: <uuid per gesto logico> # stessa chiave = stesso gesto, retry sicuro
Content-Type: application/json
{ "aggregateId": "<uuid opzionale>",
"payload": { /* solo campi dichiarati dal trigger — il contesto viene dalla sessione */ } }
type ActionResult =
| { status: 200, body: object } // applicata (row, to_state, …)
| { status: 400, code: 'ACTION_FAILED' } // constraint/validazione — fail-closed
| { status: 401, code: 'AUTH_REQUIRED' } // sessione mancante/scaduta
| { status: 403, code: 'ACTION_FAILED' } // diniego RBAC
| { status: 409, code: 'ACTION_FAILED' } // conflitto transizione FSM
| { status: 409, code: 'IDEMPOTENCY_IN_PROGRESS' } // stessa chiave in elaborazione — riprova
| { status: 429, code: 'RATE_LIMITED' } // rispetta Retry-After
| { status: 503, code: 'NO_ENGINE' }; // nessun manifest caricato sul nodo
Regole del payload: payload è l’input del trigger — tenant_id, operator_id, device_code vengono dalla sessione ($context), mai dal body; le espressioni lo leggono come $payload.*. Non esiste un campo version di optimistic-lock: l’ordinamento è garantito dalla FSM — un trigger fuori stato viene rifiutato con 409.
Il Flusso
client → trigger + payload + idempotency-key
→ edge: sessione → RBAC → constraint → FSM → data-link → txn(write+audit+outbox)
→ 200: fatto committato, entry outbox già durevole
retry su caduta rete: stessa X-Idempotency-Key → stesso esito, nessun fatto duplicato
Esempio di Payload
# QC_PASS — chiude il collaudo; il constraint engine verifica la misura
curl -X POST http://edge:8291/api/v1/actions/QC_PASS \
-H "Authorization: Bearer $TOKEN" -H "X-Idempotency-Key: $(uuidgen)" \
-H 'content-type: application/json' \
-d '{"payload":{"piece_id":"PZ-001","misura":0.018}}'
# misura 0.026 → la regola `tolleranza_collaudo` rifiuta: fail-closed, niente commit.
// Node — il gesto intero, retry-safe
const r = await fetch(`${edge}/api/v1/actions/QC_PASS`, {
method: 'POST',
headers: { authorization: `Bearer ${token}`,
'x-idempotency-key': gestureKey, // RIUSA la chiave sul retry
'content-type': 'application/json' },
body: JSON.stringify({ payload }) });
if (r.status === 409) { /* conflitto FSM — l'aggregato è in un altro stato */ }
if (r.status === 400 || r.status === 403) { /* leggi j.error — constraint o RBAC */ }
I Fallimenti Noti
| Codice | Significato → azione correttiva |
|---|---|
409 ACTION_FAILED |
Transizione FSM non ammessa dallo stato corrente → ispeziona le transitions del manifest |
403 ACTION_FAILED |
Il ruolo non ha il trigger → rbac_engine.permissions del manifest |
400 ACTION_FAILED |
Una pre_commit_rule o validazione è fallita → leggi error, risolvi il blocco prima |
409 IDEMPOTENCY_IN_PROGRESS |
Stessa chiave ancora in volo → attendi e riprova con la stessa chiave |
| Fatti duplicati dopo retry | Hai cambiato la X-Idempotency-Key al retry — stesso gesto, stessa chiave |
401 AUTH_REQUIRED a metà sessione |
Rifai login; non ritentare lo stesso token morto in loop |