Architettura — panoramica
Modello mentale
La piattaforma è un framework ETL code-first. Ogni processo è un grafo (DAG) di nodi tipati, eseguito su stream (AsyncIterable) per scalare su dataset grandi. Il grafo è l'unica unità eseguibile: il DSL lineare è stato rimosso (ADR-0036).
flowchart LR
src1["source"] --> v1["transform<br/>validate"]
src2["source"] --> v2["transform<br/>validate"]
v1 -.dead-letter.-> dl["sink di scarto"]
v1 --> join["join<br/>(unico fan-in)"]
v2 --> join
join --> guard["try-catch"]
guard --> map["transform / lookup"]
map --> sinkA["sink A"]
map --> sinkB["sink B<br/>(fan-out)"]
guard -.catch.-> ramo["ramo di gestione errori"]
Il grafo è pull-based: la domanda parte dai sink e risale verso le source, quindi è il ritmo di scrittura a regolare quello di lettura. Il fan-in passa solo da join; il fan-out è libero. La forma del grafo è validata a runtime su .build() (ADR-0016).
Componenti del core (src/core/)
| File | Responsabilità |
|---|---|
connector.ts | Interfacce Source<T> / Sink<T>, descriptor per la UI, connettori in-memory per i test |
transform.ts | Interfaccia Transform<I,O> + helper mapTransform / filterTransform / validate |
graph-builder.ts | DSL graph(name)…build() → GraphPipeline |
graph.ts | Tipi dei nodi, validazione della topologia, proiezione in PlanSpec per etl plan/UI |
graph-runner.ts | Esecuzione del grafo: streaming, dead-letter e scope try/catch, GraphRunReport |
join.ts · lookup.ts · tee.ts | Join a chiave, lookup SQL in sola lettura, duplicazione dello stream sul fan-out |
context.ts | RunContext: env, params, logger, metriche, signal |
observability.ts | Logger JSON strutturato + contatori di metriche |
errors.ts | Gerarchia EtlError per distinguere errori di dominio da bug |
progress.ts · lineage.ts | Report di avanzamento per nodo, identità dei record e lineage derivedFrom (opt-in) |
schema-columns.ts · plan-column.ts | Colonne dei nodi ricavate dagli schemi Zod, per la UI del DAG |
Gestione degli errori (dentro una run)
Tre percorsi, dal più locale al più drastico:
- Dead-letter per nodo — su
transform,lookupejoin: il record che sollevaEtlErroresce su un sink dedicato e lo stream continua. Per le trasformazioni stateless. - Nodo
try-catch— protegge i nodi a valle della sua portaout, sink compresi. Di defaultper-record: ogni errore va sul ramocatch, che è fatto di nodi veri (ADR-0029, ADR-0031). - Fail-fast — il default: l'errore aborta la run. L'unica scelta corretta per le trasformazioni stateful (aggregazioni).
Un errore che non è un EtlError aborta sempre la run: un bug va visto, non scartato.
Modalità di invocazione (del servizio)
Riguarda come una pipeline viene lanciata, non come processa i record. Il nucleo di esecuzione è lo stesso e stateless in tutti i casi: cambia solo chi la fa partire e dove finisce il GraphRunReport.
- Batch (
etl run, CLI): un container/processo = una esecuzione, report su stdout — vedi ADR-0005, ADR-0006. Chi la lancia in locale trova le run concluse conetl runs list(ADR-0039). - Daemon (
etl worker,src/worker/): processo long-running che fa polling della tabellaprocessesdi Noeva vianoeva-server-api, rivendica i job filtrati perworker_type+workspace_idcon CAS atomico (server-side) e dispatcha la pipeline indicata dafunctional registry — vedi ADR-0019, che supera ADR-0017. Una singola istanza può servire N workspace avviando un poller per workspace (ADR-0021). Il report finisce nel job Noeva, con l'avanzamento per nodo durante la run (ADR-0018). - Server + daemon (
etl serve,src/server/): il default dell'immagine Docker. Nello stesso processo espone via HTTP la topologia dei grafi e gli eventi delle run pernoeva-etl-ui, e avvia i worker (ADR-0024, ADR-0028, ADR-0040). Mai insieme a unetl workerseparato nello stesso ambiente.
La UI (noeva-etl-ui, submodule) è un'app desktop Tauri che consuma etl serve e noeva-server-api (ADR-0025).
Layout del repository
src/
core/ # framework (non contiene logica di dominio)
connectors/ # connettori riusabili verso sistemi reali (DB, API, file, Noeva)
transforms/ # trasformazioni riusabili di dominio
pipelines/<nome>/ # una cartella per pipeline: schema, definizione, run, test
pipelines/registry.ts # PIPELINES: ciò che CLI, worker e server possono lanciare
worker/ # daemon: poller su `processes` via noeva-server-api (claim, dispatch, loop)
server/ # `etl serve`: HTTP dei grafi e delle run
runs/ # storico locale delle run — mai importato da core/
tests/
core/ # test del framework
integration/ # test con sistemi reali (separati dalla CI rapida)
docs/ # convenzioni, ADR, runbook, schede pipeline
.claude/ # governance dell'agente: settings, comandi, skill
noeva-etl-ui/ # submodule: app desktop TauriPrincipi architetturali
- Il core non conosce il dominio. Niente regole di business in
src/core. - I confini sono tipati. Schema-contratto Zod su ingresso e uscita di ogni pipeline.
- Tutto è iniettabile. Sorgenti/sink concreti entrano dai parametri → testabilità e portabilità tra ambienti.
- L'IO sta nei nodi che lo dichiarano —
source,sink,lookup— mai dentro unaTransform, che resta pura. - Osservabilità non opzionale. Ogni run produce un
GraphRunReport: totali, contatori per nodo, durata, metriche — mai payload né segreti. - Le decisioni si registrano. Cambi strutturali → ADR.

