0027. Dead-letter visibile nella DAG (nodo sintetico + edge d'errore)
- Stato: Accettata
- Data: 2026-07-22
- Decisori: Team BI/ETL
Contesto
Le pipeline a grafo isolano gli errori con dead-letter per nodo (ADR-0016): un deadLetter?: Sink<DeadLetter> su un nodo transform/join instrada i record scartati senza abortire la run (status partial). Ma questa gestione errori era invisibile nella UI del DAG: il deadLetter è una proprietà del nodo (src/core/graph.ts), non un nodo/edge del GraphSpec, e toPlanSpec lo scartava — nessun PlanNode/PlanEdge lo rappresentava. L'utente non vedeva, guardando il diagramma, che una pipeline gestisce gli errori né dove.
Decisione
toPlanSpec proietta il deadLetter di ogni nodo in topologia sintetica (deadLetterProjection): un PlanNode con stepType: "dead-letter" per ogni sink di scarto (dedup per name) e un PlanEdge con kind: "dead-letter" da ogni nodo che lo alimenta. Questi nodi/edge non esistono nel GraphSpec: sono una vista per la UI.
Contratto additivo (src/core/graph.ts):
PlanStepType = GraphNodeKind | "dead-letter"— nuovo tipo di step, usato daPlanNode.stepType.PlanEdge.kind?: "dead-letter"— assente sugli edge normali del flusso dati.
Il nodo dead-letter ha columns: []: porta l'envelope DeadLetter (record + causa), non lo schema del record — coerente con ADR-0026 ("non si inventano colonne dove non esistono"). Le pipeline che vogliono mostrare il branch collegano il dead-letter anche in getSpec() (non solo in run()): vedi orders-enriched-daily.
Lato UI (noeva-etl-ui, submodule indipendente, sync manuale via src/lib/types.ts): ETLStepType e lo schema Zod di plan-loader accettano "dead-letter"; il nodo è reso in destructive (icona PackageX) e l'edge tratteggiato (strokeDasharray) in destructive.
Alternative scartate
- Rendere il dead-letter un vero nodo/edge del
GraphSpec(non sintetico) — scartato: cambierebbe la semantica dell'esecuzione e le validazioni divalidateGraph(un sink con un edge in ingresso da un nodo che non è "a valle" nel senso del flusso dati). Il dead-letter è un canale laterale, non un ramo del flusso; proiettarlo solo intoPlanSpeclo mantiene tale. - Mostrare i conteggi reject live sul nodo (dal run report:
rejected/rejectedReasons) invece di un branch topologico — feature complementare, non alternativa: riguarda la vista run, non la topologia. Qui si mostra che il branch esiste; quanti record ci finiscono è un'altra iterazione. - Un flag booleano
hasDeadLettersul nodo invece di un nodo+edge dedicati — scartato: non si vedrebbe dove va lo scarto (il sink di destinazione, il suo descriptor), e non si potrebbe stilare un edge d'errore distinto.
Conseguenze
- Pro: la gestione errori diventa visibile nel diagramma senza cambiare l'esecuzione; cambiamento additivo (nuovo
stepType/kind, retrocompatibile); riusa il descriptor del sink di scarto (ADR-0020) per dire dove finiscono gli scarti. - Contro: piccola duplicazione di contratto cross-repo (il
"dead-letter"va aggiunto sia innoeva-etlsia innoeva-etl-ui, che restano submodule separati); mitigata da test su entrambi i lati e dalla nota di sync intypes.ts. - Una pipeline che ha il
deadLettersolo inrun()e non ingetSpec()non mostra il branch: per vederlo va collegato anche nella topologia statica (fatto perorders-enriched-daily).
Riferimenti
src/core/graph.ts(PlanStepType,PlanEdge.kind,toPlanSpec,deadLetterProjection),src/pipelines/orders-enriched-daily/run.ts(getSpeccon dead-letter).- UI:
noeva-etl-ui/src/lib/types.ts(ETLStepType,dagPlanToFlow),noeva-etl-ui/src/lib/plan-loader.ts,noeva-etl-ui/src/lib/step-visuals.ts,noeva-etl-ui/src/components/dag/DatasetNode.tsx. - Correlati: ADR-0016 (dead-letter per nodo), ADR-0020 (descriptor), ADR-0026 (colonne).
- Piano di implementazione: docs/plans/2026-07-22-dag-dead-letter-branch.md.

