0036. Rimozione del DSL lineare: il grafo è l'unico modo di costruire un processo
- Stato: Accettata
- Data: 2026-07-28
- Decisori: Team BI/ETL
Contesto
ADR-0016 ha introdotto il DSL a grafo (graph().source().transform().join().sink().build()) accanto al DSL lineare (pipeline().from().pipe().to()), non al suo posto: il lineare copriva già i casi a singolo source/sink e riscriverlo non era la priorità.
Da allora il lineare ha smesso di essere un modo alternativo di eseguire ed è diventato una façade su quello a grafo: Pipeline.run() costruiva un GraphSpec a tre nodi (source → chain → sink, dove chain collassava l'intera sequenza di transform in un unico nodo) e lo eseguiva con runGraph. Un solo motore, due superfici pubbliche.
Il costo di tenerle entrambe:
- Due tipi di report.
RunReporteGraphRunReportcon la stessa semantica ma campi diversi, e una unionRunReport | GraphRunReportpropagata suRunnablePipeline, CLI e worker. Ogni consumatore doveva restare sul sottoinsieme comune. - Un grafo finto in UI. Una pipeline lineare mostrava sempre tre nodi
source/chain/sink, indipendentemente da cosa facesse davvero: il nodochainnon dice niente né all'operatore né a chi debugga una run. - Due punti di verità da documentare.
writeReportsi configurava per nodo nel builder a grafo e viaRunOptions.writeReport: { source, chain, sink }nel lineare; ilcatchera un'opzione dirun()da una parte e un nodo dall'altra. - Un solo processo lo usava ancora (
langfuse-sessions-daily). Le restanti 38 occorrenze erano in 14 file di test, dove serviva solo come harness comodo.
Decisione
Il DSL lineare viene rimosso. src/core/pipeline.ts è cancellato per intero: pipeline(), PipelineBuilder, Pipeline, RunOptions, PipelineWriteReportOptions, RunReport, RunStatus, RunError, buildFailedReport, REPORT_SCHEMA_VERSION.
Conseguenze dirette della rimozione:
langfuse-sessions-dailyè migrato agraph()— quattro stadi come quattro nodi (sessions → raw-contract → aggregate-daily → fact-contract → sink) invece di un unicochain. Il suogetSpec()statico usa gli stessi id, e un test verifica che topologia dichiarata ed eseguita non divergano.RunnablePipeline.runritornaPromise<GraphRunReport>, non più una union.buildFailedGraphReportrimpiazzabuildFailedReportper CLI e worker: senza di esso una run abortita non avrebbe più prodotto un report JSON strutturato.GraphRunErrorguadagna il campostage, che aveva soloRunError.GraphRunReportguadagna gli aggregati top-levelextracted,written,rejected,rejectedReasons, con una semantica dichiarata:extractedsomma i soli nodi source. È ciò che rende il numero non ambiguo su un fan-out, dove lo stesso record attraversa più rami a valle.writtensomma i nodi sink. Su un fan-out lo stesso record scritto su due sink conta due volte, perché due scritture sono davvero avvenute.
runGraphscrive i contatori standard suctx.metricse li include nel report insieme a quelli custom degli stage. Prima lo faceva soloPipeline.run(): senza questo, rimuovendo il lineare, unctx.metrics.increment("api_calls")dentro una transform sarebbe sparito dal report — perdendo un punto di estensione documentato in run-report.md.- I test che usavano il lineare come harness passano a
graphOf()(tests/helpers/graph-of.ts), che conserva la forma fluente.from().pipe().to()ma costruisce un grafo vero, con un nodo per transform.
Alternative considerate
- Deprecare
pipeline()per un ciclo di release invece di rimuoverlo — scartata: terrebbe in vita esattamente ciò che l'operazione vuole eliminare (due superfici, due report), e non risulta alcun consumatore esterno del pacchetto oltre a questo repo. La rimozione è secca, con bump a 2.0.0. - Rimuovere gli aggregati top-level dal report invece di calcolarli sommando i
NodeReport— scartata:docs/conventions/run-report.mddocumenta il contratto CLIetl run … | jq '.status, .written', e romperlo per un refactor interno sarebbe un costo scaricato sui consumatori senza contropartita. Il costo del mantenerli è dover dichiarare la semantica sul fan-out, non subirla. - Tenere l'harness dei test come
graph()esplicito, senzagraphOf()— scartata per la dimensione del diff: 38 call-site in 14 file, dove la topologia non è il soggetto del test (codec, storage, integrazione) e riscriverla a mano aggiungeva rumore senza aggiungere copertura. Dove invece la topologia è il soggetto (tests/core/,src/core/), i test usanograph()esplicito. - Far collassare
graphOf()le transform in un unico nodochain, per preservare al byte i report dei test migrati — scartata: replicherebbe nell'harness proprio la finzione che stiamo eliminando dal prodotto. Il prezzo è che i report dei test migrati hanno più nodi di prima; è una differenza attesa e verificata.
Conseguenze
- Positive:
- Un solo modo di costruire un processo, un solo tipo di report, un solo posto dove configurare
writeReporte la gestione errori. - La topologia mostrata in UI è sempre quella reale: niente più nodo
chainopaco. GraphRunReportè ora un superset di ciò che offrivaRunReport— nessun consumatore perde informazione.
- Un solo modo di costruire un processo, un solo tipo di report, un solo posto dove configurare
- Negative / costi:
- Breaking change sulla public API del pacchetto
@gruppo4d/noeva-etl(2.0.0): chi importassepipeline()dall'esterno deve passare agraph(). - Le pipeline a catena singola sono ora leggermente più verbose: cinque chiamate al builder invece di
.from().pipe().to(). È il prezzo di avere id di nodo espliciti — che è anche ciò che rende la UI leggibile. graphOf()vive intests/, quindi i test colocati insrc/lo importano per path relativo (../../../../tests/helpers/graph-of.js). Alternativa scartata: metterlo insrc/, dove finirebbe nel bundle pubblicato e nel calcolo coverage.
- Breaking change sulla public API del pacchetto
- Cosa diventa più difficile da cambiare: la semantica di
writtensul fan-out è ora un contratto documentato. Cambiarla (es. deduplicare i record scritti su più sink) sarebbe un breaking sul report, non un dettaglio interno.
Riferimenti
- Issue #85, piano 2026-07-28-gh85-remove-linear-dsl-and-graph-parallelism.md.
- ADR-0016 (introduzione del DSL a grafo), ADR-0018 (
writeReporte progress per nodo). src/core/graph-builder.ts,src/core/graph-runner.ts,tests/helpers/graph-of.ts.

