npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@esfaenza/flow-builder

v20.3.21

Published

Editor visuale di flow: componente standalone Angular, con tutto il traffico verso il backend dietro un'unica interfaccia.

Downloads

2,285

Readme

flow-builder

Editor visuale per i flow descritti in FRONTEND.md: un componente standalone Angular che disegna il grafo, edita ogni tipo di elemento, valida, versiona e prova un flow.

Requisiti: Angular ≥ 18 (sviluppato e verificato su 19.2), TypeScript.


Indice

  1. Integrazione in tre passi
  2. L'interfaccia verso il backend
  3. Cosa fa il builder
  4. Architettura
  5. Dipendenze esterne
  6. Tema e personalizzazione
  7. Le trappole della §13, una per una
  8. Limiti noti

Integrazione in tre passi

1. Fornire l'implementazione dell'interfaccia

FlowBuilderApi e' l'unica porta verso il backend: tutto il traffico di dati passa da lì, e nessun componente della libreria conosce HTTP.

import { FlowBuilderApi, HttpFlowBuilderApi, FLOW_BUILDER_HTTP_CONFIG } from 'flow-builder';

bootstrapApplication(App, {
  providers: [
    provideHttpClient(),
    { provide: FLOW_BUILDER_HTTP_CONFIG, useValue: { baseUrl: '/api' } },
    { provide: FlowBuilderApi, useClass: HttpFlowBuilderApi },
  ],
});

HttpFlowBuilderApi usa le rotte proposte dalla §6. Se le vostre differiscono, ci sono due strade e nessuna delle due tocca l'editor:

// 1. estendere e sovrascrivere i soli metodi con rotta diversa
class NostraApi extends HttpFlowBuilderApi {
  override loadFlow(flowName: string, version?: number) { /* … */ }
}

// 2. implementare `FlowBuilderApi` da zero: e' una classe astratta, quindi il compilatore
//    elenca esattamente i metodi che mancano

I metodi con implementazione di default rifiutano con MissingService: un ambiente che non espone parseFlow, exportFlow o le primitive di esecuzione resta usabile, con il comando corrispondente degradato invece di rotto.

2. Includere i due fogli di stile

// angular.json → architect.build.options.styles
"styles": [
  "node_modules/@foblex/flow/styles/default.scss",
  "node_modules/flow-builder/styles/flow-builder.css",
  "src/styles.css"
]

Servono entrambi. flow-builder.css contiene le variabili del tema, i controlli di form e gli stili degli archi: i path SVG delle connessioni sono creati a runtime e non portano l'attributo di scope di Angular, quindi un CSS incapsulato non li raggiunge.

3. Montare il componente

<fb-flow-builder
  [flowName]="'ApprovazioneOrdine'"
  [version]="null"
  [author]="utenteCorrente"
  (saved)="onSaved($event)"
  (activated)="onActivated($event)"
/>

| Input | Significato | |---|---| | flowName | il flow da aprire; null → si parte da un flow nuovo | | version | la versione da aprire; assente → l'attiva se c'e', altrimenti l'ultima (§6.1) | | author | registrato sulla versione e mostrato nell'elenco | | defaultProcessType | processType iniziale di un flow nuovo | | inspectorMode | 'dialog' (predefinito) apre il dettaglio dell'elemento in una finestra sopra il canvas; 'panel' lo tiene nel pannello laterale |

| Output | Quando | |---|---| | saved | dopo un salvataggio riuscito, con FlowSaveResult. Lo emette anche «Duplica», e in quel caso flowName e' quello della copia: la sessione si e' spostata su di essa, quindi l'host che tiene il nome da parte deve adottarlo | | activated | dopo un'attivazione riuscita |

Il componente vuole un'altezza: <fb-flow-builder style="height: 100vh">, oppure un contenitore flex. Gli store sono provider del componente, quindi due builder sulla stessa pagina editano due flow indipendenti.


L'interfaccia verso il backend

Metodi raggruppati come nella specifica. Gli astratti sono obbligatori; i concreti sono opzionali e rifiutano con MissingService se non sovrascritti.

| §6.1 Lettura | | |---|---| | listFlows(query?) | astratto | | listVersions(flowName) | astratto | | loadFlow(flowName, version?) | astratto | | exportFlow(flowName, query?) | opzionale | | parseFlow(definition) | opzionale — serve all'import di un file |

| §6.2 Scrittura | | |---|---| | createFlow(request) | astratto | | saveFlow(flowName, request) | astratto | | createVersion(flowName, request?) | astratto | | activateVersion(flowName, version) | astratto | | deactivateFlow(flowName) | astratto | | cloneFlow, deleteVersion, deleteFlow | opzionali — senza cloneFlow «Duplica» resta disponibile e ripiega su createFlow |

| §6.3 Validazione | | |---|---| | validateDefinition(definition) | astratto — e' il pannello dei problemi | | validateVersion(flowName, version) | opzionale | | validateFormula(request) | opzionale — verifica una espressione mentre si digita, ed e' cio' che alimenta <fb-formula-editor>. Unica primitiva di analisi con un involucro: il documento sta sotto flow, insieme a expression, usage e expectedDataType (§6.3, §6.4). I rilievi portano position/length, che la validazione dell'intero documento non ha. Tre esiti da tenere distinti e nessuno dei tre e' «formula valida»: rifiuto MissingService (primitiva assente), isVerified: false (primitiva sì, motore non configurato), isValid (verificata davvero) |

| §6.4 Dizionari e cataloghi | | |---|---| | getDictionaries(processType?) | astratto — processType filtra le globali (§4.1) | | getReferences(definition) / getWritableReferences(definition) | astratti — prendono il documento nudo e restituiscono tutti i riferimenti: nel body non c'e' nessun filtro di tipo, e la compatibilita' col campo la decide l'editor (§6.4, §13.17) | | getConnectorTargets(definition, excluding?) | astratto | | getOutline(definition) | astratto | | listObjects(), listFields(object, usage?) | astratti | | listActionTypes / listActions / listActionParameters | astratti | | listScripts / listScriptParameters | astratti | | listForms(kind?) / listFormParameters | astratti — un catalogo, due ruoli (§5.1, §5.2): le schermate intere di uno Screen (formName) e i componenti di uno screen dinamico (extensionName) stanno nello stesso elenco e si distinguono per formKind. kind vale Form, Component o assente per tutto; una voce senza formKind vale Any e resta proponibile in entrambi i posti, perche' un catalogo che il ruolo non lo dichiara non deve vietare tutto. Usare l'uno al posto dell'altro e' FORM_KIND_MISMATCH, che e' un errore diverso da «il nome non esiste». I parametri si chiedono per nome, uguali per i due ruoli | | listEnumTypes, listEvents, listSubflowCandidates | astratti | | describeObject, listFieldValues | opzionali | | listEnumValues(enumType) | opzionale — i valori di un tipo di enumerazione, cioe' i nomi scrivibili in enumValue (§4.2, §4.6). listEnumTypes e' l'altra meta' e non basta: quella dice quali tipi esistono, questa quali valori ha un tipo. enumType e' esattamente l'objectType della risorsa, del parametro o del membro di una classe (§4.7.1): la risposta e' del tipo, non del punto di uso, e la cache e' per nome del tipo. Lista vuota = "non lo so" — il valore si scrive a mano, e un tipo sconosciuto risponde vuoto, non 404 | | listStructures, describeStructure(className) | opzionali — le classi e i loro membri per le risorse Structure (§4.7). listStructures e' l'elenco delle classi senza membri; describeStructure restituisce la descrizione di una classe (className, label, members), non un array nudo (§4.7.1). Senza, le classi non sono verificabili e il nome si scrive a mano: "non lo so", non "non esiste". I tre esiti sono distinti: members popolato = elenco autorevole, members: [] = membri non dichiarati (si digita, nessuna accusa), rifiuto StructureNotFound = classe non registrata (si segnala la classe) |

| §6.5 Esecuzione | | |---|---| | startInterview, respondToScreen, resumeInterview, inspectInterview, abandonInterview | opzionali — senza, il pannello «Prova» segnala che la funzione non c'e' | | completeStageStep(request) | opzionale — conclude uno step di orchestrazione (§5.14) |

Errori. Ogni metodo, in caso di rifiuto, deve fallire con un FlowApiError categorizzato (§10). L'editor si comporta in base alla category, mai leggendo il messaggio: NotEditable fa comparire «Nuova versione» al posto di «Salva», VersionConflict apre il banner con «ricarica» / «salva come nuova versione», ValidationFailed apre il pannello dei problemi con il payload.

throw new FlowApiError('VersionConflict', 'Ha salvato mrossi.', undefined, 'mrossi');

Un'implementazione completa e commentata sta in projects/demo/src/app/backend/in-memory-flow-builder-api.ts: copre tutte le primitive, il ciclo di vita, la concorrenza ottimistica e un esecutore finto, senza una sola richiesta di rete.


Cosa fa il builder

Canvas. Node draggabili con l'ingresso sul bordo superiore e un'uscita per ramo sul bordo inferiore, in fila e nell'ordine del modello, secondo la mappa della §3.5: connector, faultConnector, timeoutConnector, rules[].connector, nextValueConnector / noMoreValuesConnector, waitEvents[].connector, scheduledPaths[].connector. Archi colorati per kind (Fault rosso, Rule viola ed etichettato, Default tratteggiato, LoopNext verde), isGoTo reso come salto, pan/zoom, minimap, selezione, creazione e riassegnazione di archi trascinando, drag-and-drop dalla palette, comando «Riordina» che ricalcola e scrive le coordinate nel documento. I node non raggiungibili e i rami dichiarati senza destinazione sono marcati sul canvas.

Il lato da cui gli archi entrano ed escono e' dichiarato (fConnectorConnectableSide: TOP sull'ingresso, BOTTOM sulle uscite), non calcolato: lasciandolo calcolare a @foblex/flow, un ramo che risale — il ritorno di un Loop, per esempio — uscirebbe di fianco e il grafo perderebbe la lettura dall'alto verso il basso. La larghezza del node cresce a scaglioni col numero di rami, perche' le etichette delle uscite stanno in fila: arriva da una classe (.fb-node--outlets-N) e non da uno stile inline, perche' @foblex/flow riscrive l'attributo style del node a ogni spostamento e uno [style.width] verrebbe cancellato. La tabella delle larghezze e' la stessa che il comando «Riordina» passa a dagre: se divergessero, i node larghi resterebbero sovrapposti.

Copia, incolla, duplica. Ctrl+C copia gli elementi selezionati — la selezione del canvas puo' contenerne piu' d'uno (rettangolo, Ctrl+A) — Ctrl+V li incolla, Ctrl+D li duplica in loco; gli stessi comandi stanno nella top bar («⧉» e «⎘») e sul node selezionato («⧉»). I tasti ascoltano sull'host del builder e si ignorano quando il fuoco e' in un campo di testo, dove Ctrl+C deve copiare il testo.

Cosa viaggia con il blocco: la configurazione intera dell'elemento, i collegamenti interni al blocco (quelli verso l'esterno no — sarebbero CONNECTOR_TARGET_UNKNOWN) e le risorse referenziate, che e' cio' che rende il gesto utile su un elemento complesso. Gli appunti stanno in localStorage, quindi valgono anche fra flow diversi e fra schede: non e' la clipboard di sistema, che per essere letta chiede un permesso che in un iframe puo' non arrivare mai.

L'incolla passa da una conferma perche' non e' mai una copia identica, e le tre cose che cambiano le decide il contratto:

  • i nomi già usati vengono rinominati, e non solo quello dell'elemento: uno spazio di nomi unico (§3.3) comprende gli step di orchestrazione, i campi di uno screen dinamico e le screen action, e duplicare uno screen dinamico senza rinominare i campi produceva un NAME_DUPLICATED per campo;
  • i riferimenti interni al blocco seguono i nomi nuovi (§13.8: nessuna primitiva lo fa), condizioni, visibilityRule, trigger e output automatici compresi;
  • le risorse che nella destinazione non esistono si creano (con una casella per non farlo), quelle omonime si riusano, e cio' che resta irrisolvibile — un elemento rimasto nel flow di origine — viene elencato invece di sparire in un REFERENCE_UNKNOWN da scoprire dopo.

Tutto l'incolla e' un solo passo di annulla.

Dettaglio dell'elemento. Con inspectorMode="dialog" (predefinito) il form si apre in una finestra sopra il canvas: doppio click sul node, bottone «✎» sul node, comando «Dettaglio» nella top bar, click su un rilievo nel pannello dei problemi, o creazione di un elemento dalla palette. Il click singolo seleziona soltanto: un click capita anche solo per spostare un node. La dialog non ha «Annulla» — le modifiche entrano nel documento mentre si digita, esattamente come nel pannello, e tornare indietro e' compito dell'annulla dell'editor. Vive dentro il componente e non in fondo al body: la libreria e' innestabile in una pagina qualsiasi.

Palette. Costruita da elementTypes: nessun tipo e' cablato, e i due non supportati (Step, Experiment) sono nascosti tramite isSupported. L'etichetta del ramo di fault arriva dalla mappa delle uscite invece di essere «ramo di errore» per tutti: su uno stage di orchestrazione quel ramo e' lo step rifiutato, non un guasto.

La stessa mappa distingue i rami di recupero da errore (isFaultRecovery: Record*, ActionCall, ScriptCall, Wait) dagli altri, perche' lasciarne uno vuoto non significa «il percorso finisce qui» ma «se questo fallisce l'interview termina Failed e le modifiche vengono annullate» (§3.5). L'editor delle uscite lo dice come promemoria — non col colore d'avviso, perche' rinunciare al recupero e' la scelta piu' comune — e il backend lo segnala con FAULT_CONNECTOR_MISSING, che e' un Warning e non blocca niente. Lo stage di orchestrazione ne resta fuori: la §7 non lo comprende.

Un tipo con un discriminatore compare come piu' voci, una per valore: il Collection Processor e' «Ordina» e «Filtra», non «ordina o filtra». Sono due gesti diversi — un Sort vuole sortOptions, un Filter vuole condizioni (§5.6) — e sceglierli dalla palette evita di sceglierli dopo, dentro il form. L'elemento nasce con il discriminatore già scritto, con la label della variante (da cui viene anche il nome tecnico) e con il glifo della variante, lo stesso che avra' sul canvas. core/element-variants.ts tiene la mappa tipo → campo del discriminatore; i valori restano nel dizionario del backend (collectionProcessorTypes), e senza quel dizionario il tipo torna a essere una voce sola.

Inspector. Un form per ogni tipo supportato: Start, Screen, Screen dinamico, Assignment, Decision, Loop, Collection Processor, Get / Create / Update / Delete / Rollback Records, Action, Script, Subflow, Wait, Custom Error, Transform, Orchestrated Stage. Più l'intestazione comune con la rinomina, che riscrive tutti i riferimenti all'elemento.

Screen dinamico (§5.2). È un compositore, non un form, e per questo la dialog e' larga il doppio di com'era: tre colonne. A sinistra la palette (due schede — i componenti, e i campi di un'entita' che portano tipo ed etichetta dallo schema), al centro la schermata disegnata (si clicca il campo da configurare, lo si trascina dove deve stare, la larghezza in dodicesimi si vede), a destra le proprieta' di cio' che e' selezionato — o della schermata, se non c'e' selezione, che e' anche cio' che si ottiene cliccando l'anteprima fuori dai campi. L'anteprima e' inerte di proposito: i controlli non ricevono il puntatore, perche' un input che accetta testo in un editor di metadati fa credere di star compilando la schermata. Quali proprieta' mostrare lo decide il dizionario (screenFieldTypes e i suoi flag storesValue, isCollection, acceptsChoices, isContainer, requiresDataType), non il codice: sono gli stessi flag che applica il runtime, e reimplementarli porta a proporre configurazioni che il motore rifiuta.

Tre cose che il modello non mostra e il form dice:

  • un campo che raccoglie un valore e' una risorsa del flow, referenziabile per nome ovunque e di sola lettura — il suo nome vive nello spazio dei nomi comune, e rinominarlo riscrive i riferimenti (FlowDocumentStore.renameScreenField, come per i node: nessuna primitiva del backend lo fa);
  • le regole di visibilita' si rivalutano sui valori appena inviati, i campi nascosti vengono azzerati e solo i visibili vengono validati;
  • choiceReferences accetta solo Choice e DynamicChoiceSet, e gli output automatici di un ComponentInstance sono esclusivi con i suoi outputParameters.

Il riordino e l'annidamento si fanno trascinando (@angular/cdk/drag-drop, senza drop list: il bersaglio si calcola dal DOM) oppure con i comandi «su / giu' / porta fuori».

Screen action (§5.2). Un campo che l'utente compila puo' innescare un'action i cui risultati finiscono negli altri campi della stessa schermata: e' il caso «scrivi il codice fiscale e nome e cognome compaiono da soli», e senza di essa lo si farebbe spezzando lo screen in due con un elemento Action in mezzo. Si modellano nelle proprieta' della schermata (cliccare l'anteprima fuori dai campi), in due elenchi che il contratto tiene separati — actions e triggers — perche' piu' trigger possono invocare la stessa action. Identificazione e parametri sono quelli di un elemento Action, ACTION_UNKNOWN e PARAMETER_UNKNOWN compresi.

Le tre cose che riguardano l'editor, e non il runtime:

  • il picker della destinazione e' allargato qui e solo qui. L'assignToReference di un output accetta anche i campi di questa schermata, che ovunque altrove sono di sola lettura: POST /flows/references/writable continua a escluderli — e giustamente, un Assignment che ci scrive resta TARGET_NOT_WRITABLE. L'allargamento e' quindi un parametro del chiamante (<fb-reference-picker [extraReferences]>), non un comportamento del picker;
  • il nome dell'action sta nello spazio dei nomi del flow (§3.3): con l'output automatico e' la radice di Cerca.nome, quindi un'omonimia con una variabile — o con una Structure chiamata come lei — renderebbe ambiguo quel riferimento. usedNames lo comprende, e il form lo dice con NAME_DUPLICATED come per il nome di uno step;
  • un'action senza output e' un avviso (SCREEN_ACTION_WITHOUT_OUTPUTS): viene invocata e il suo risultato si perde. L'output automatico non e' la cura per tutti i casi — rende il risultato leggibile come Cerca.nome ma non scrive in nessun campo: e' la forma per una condizione, non per precompilare;
  • un trigger senza campo e senza initBehavior non scattera' mai (SCREEN_TRIGGER_WITHOUT_CAUSE), e l'unico initBehavior ammesso e' runOnLoad — il runOnRevisit della specifica Salesforce e' SCREEN_TRIGGER_INIT_BEHAVIOR_UNKNOWN, e vale la pena saperlo perche' chi arriva da quella specifica lo scrive per abitudine.

Le action si aprono una alla volta: i cataloghi delle action e dei loro parametri dipendono da actionType/actionName, e tenerne N in volo sarebbe N volte la corsa fra risposte che l'inspector dell'Action risolve scartando quelle superate.

Sullo stage di orchestrazione il form dice le tre cose che il modello non mostra: gli step non sono una sequenza (le frecce riordinano l'esame, non l'esecuzione), ingresso e uscita non sono simmetriche (ingresso falso = step saltato, uscita falsa = stage in stallo), e faultConnector e' il ramo dello step rifiutato, non un ramo di guasto — senza, un rifiuto fa fallire l'interview. Una condizione che referenzia l'output di un altro step viene segnalata con la via d'uscita: scrivere quel risultato in una variabile.

Risorse. I sette tipi, con i vincoli visibili: dataType obbligatorio, objectType per Object / Enum / Structure, scale solo sui numerici, costanti che non possono referenziare risorse, stageOrder distinti.

Classi (Structure). L'ottavo tipo di dato non e' un doppione di Object (§4.7): di un'istanza di classe il flow legge e scrive i membri. Cambia quindi il form: la classe si scoglie con <fb-structure-picker> e la sua assenza e' un errore — senza classe il runtime non ha nulla da istanziare — mentre il campo «valore iniziale» non c'e' affatto, perche' non esiste un structureValue e la variabile parte con un'istanza vuota. I membri si scelgono con <fb-structure-member-picker>, e nelle destinazioni si propongono solo quelli scrivibili: un membro calcolato si legge in una condizione ma assegnarlo e' TARGET_NOT_WRITABLE. Lo stesso vale dentro <fb-reference-picker>, dove Richiesta.Ragione si propone e si verifica (STRUCTURE_MEMBER_UNKNOWN). Un Transform con target Structure compone l'istanza intera in un elemento solo: lì outputFieldApiName e' il nome di un membro, e il form lo dice invece di far indovinare (§5.13).

Percorsi annidati. I percorsi con il punto non sono piu' un buco nero: la validazione li segue, e quindi li segue l'editor. core/path-navigation.ts naviga la catena una tappa alla volta — objectType di un membro Structure porta la classe annidata, quello di un membro Object porta un'entita' dello schema, e da lì si continua con i campi (§4.7.1) — ed e' lo stesso codice per i membri di una classe e per i percorsi di relazione di un record (Cliente.Citta, §4.4). Tre conseguenze visibili: i pickers propongono il livello in cui si sta scrivendo e hanno un «›» per scendere di uno; un refuso in mezzo (Richiesta.Clente.Nome) e' segnalato subito, con l'elenco di cio' che esiste in quella tappa; e dove la catena si interrompe — un membro senza objectType, una classe annidata non descritta, una collection — il rilievo e' l'avviso PATH_NOT_VERIFIABLE con il motivo, che vuol dire "non lo so" e non impedisce di scrivere. Restano fuori i percorsi radicati sul risultato automatico di un elemento (Leggi_Contatti.Owner.Name): la validazione non li verifica, e l'editor non accusa cio' che il backend non verifica.

Il filtro di tipo di un campo (§6.4) lo applica l'editor: la primitiva prende il documento nudo e restituisce tutti i riferimenti, così la stessa risposta serve tutte le tendine di una schermata (§13.17). La regola sta in core/reference-filter.util.ts, ed e' la stessa per le radici e per i segmenti di un percorso. Il filtro guarda il tipo della radice, e quindi non basta: in un parametro String una variabile Structure non e' compatibile, ma Richiesta.Ragione sì. Per questo <fb-reference-picker> tiene l'elenco completo accanto a quello filtrato, e ne ricava due cose: i contenitori compaiono come righe in cui si entra — cliccarle apre il percorso invece di scegliere un valore del tipo sbagliato — e le radici si riconoscono tutte, così un Leggi_Ordine.Numero già scritto in un campo String non viene accusato di non esistere. Nelle proposte non entra comunque nessun valore del tipo sbagliato: i segmenti li filtra la stessa regola, tenendo solo quelli da cui si scende. In un campo di destinazione le due liste sono due richieste — getWritableReferences per le proposte, getReferences per le radici — perche' la scrivibilita' e' del percorso e non della radice: un $User.Anagrafica.Citta scritto a mano e' TARGET_NOT_WRITABLE, non un nome che non esiste.

C'e' un posto in cui alle proposte si aggiunge qualcosa che la primitiva non restituisce, e extraReferences esiste solo per quello: l'output di una screen action, che si assegna a un campo della schermata che si sta modificando (§5.2). Non e' una scorciatoia per riferimenti che il backend non conosce — quei campi restano esclusi da references/writable, e altrove assegnarli e' TARGET_NOT_WRITABLE: e' l'unico punto in cui la regola del contratto e' diversa, e per questo l'elenco lo passa il chiamante.

Le globali si navigano come le risorse. Un percorso dichiarato dall'host puo' portare un contenitore, e allora objectType dice di che tipo: $User.Anagrafica.Citta si propone e si verifica come Richiesta.Ragione. La regola da non sbagliare e' che vince il prefisso dichiarato piu' lungo — non tutto cio' che segue lo scope — perche' altrimenti un percorso giusto viene segnalato come rotto (§4.1). Dove il catalogo dichiara il contenitore ma non il tipo il rilievo e' PATH_NOT_VERIFIABLE, e la correzione sta nel catalogo dell'host; navigare uno scalare ($User.Email.Dominio) e' invece GLOBAL_UNKNOWN. Sulle destinazioni non cambia niente da sapere: le globali sono di sola lettura, membri compresi — le assegnabili sono $Flow.CurrentStage, $Flow.ActiveStages e i campi di $Record — e il picker lo dice con TARGET_NOT_WRITABLE invece di far sembrare il nome inesistente (§5.2).

Su una destinazione la colonna che decide non e' la stessa nei due mondi: un membro porta il proprio isWritable, un campo no. Fuori da un Create o da un Update il contesto non c'e' — un Assignment su Cliente.Codice non sa se quel record verra' creato o aggiornato — quindi lì basta isCreateable oppure isUpdateable, e un campo che non e' ne' l'uno ne' l'altro e' TARGET_NOT_WRITABLE in qualunque contesto (§5.8).

Dei tre esiti di describeStructure il picker mostra tre messaggi diversi, perche' portano a gesti opposti: con l'elenco autorevole un nome fuori elenco e' un errore; con members: [] la classe e' valida ma i membri non sono dichiarati e il nome si digita, senza accuse; con un 404 (StructureNotFound) e' la classe a essere sbagliata, e il messaggio manda a correggere quella invece del membro. L'intestazione dell'elenco usa la label della classe, che e' la ragione per cui la risposta ha un involucro invece di essere un array.

Oggetti e campi. Ogni punto che chiede un'entita' o un suo campo usa lo stesso controllo: <fb-object-picker> e <fb-field-picker>, una casella con autocomplete che filtra il catalogo mentre si scrive. Un <select> andava bene con dieci entita', non con trecento; e dove il catalogo non arriva il controllo resta lo stesso, con un avviso, invece di degradare in un campo di testo cieco. La scrittura libera e' voluta — il catalogo dell'host puo' essere incompleto — ma un nome fuori catalogo viene segnalato subito, prima che il backend risponda OBJECT_UNKNOWN o FIELD_UNKNOWN. I percorsi di relazione (Cliente.Citta) si navigano: l'elenco diventa quello dell'oggetto referenziato e l'ultimo segmento si verifica con l'uso richiesto (§4.4). Il campo si chiede sempre con l'uso giusto (filterable, sortable, updateable, createable), che decide sia l'elenco sia il testo dell'avviso: "non filtrabile", "non aggiornabile" e "non valorizzabile alla creazione" sono errori diversi. In particolare createable e updateable non sono sinonimi (§5.8): un Create propone i primi, un Update i secondi, e il validatore usa il codice giusto per l'elemento (FIELD_NOT_CREATEABLE / FIELD_NOT_UPDATEABLE). Per lo stesso motivo l'editor non da' per scontato che Id sia filtrabile: su una chiave composta porta la forma canonica della chiave e non e' filtrabile, quindi l'editor dice di filtrare per le colonne della chiave invece di far cercare un campo che nell'elenco non c'e'. Restano <select> i dizionari chiusi — tipi di dato, operatori, enumerazioni — dove non c'e' niente da scrivere a mano.

Valori di enum. Un Enum ha due meta' e servono entrambe: l'objectType dice quale tipo — e quello resta un <select>, perche' l'elenco dei tipi e' un dizionario chiuso — mentre i valori di quel tipo arrivano da listEnumValues(objectType) e sono cio' che finisce in enumValue (§4.2, §4.6). Senza la seconda chiamata all'utente resta una casella di testo su un insieme chiuso, che e' il modo piu' facile di scrivere un valore che il runtime non riconoscera'. Li propone <fb-enum-value-picker>, che il <fb-value-editor> monta al posto della casella di testo appena il tipo della destinazione e' Enum: vale quindi in una condizione, in un filtro, nel valore iniziale di una risorsa, in un parametro di action o di form, in un Assignment e in una Map di un Transform. Cio' che si scrive e' il nome: numericValue viene mostrato accanto al valore — aiuta a riconoscerlo in dati legacy — ma un numero in enumValue sarebbe un altro tipo (§13.18). Elenco vuoto resta "non lo so" — primitiva non esposta, tipo senza valori dichiarati, objectType ancora da scegliere — e allora il valore si digita; con l'elenco popolato un valore fuori elenco e' un avviso, perche' nessun codice della §7 lo blocca ed e' l'editor ad accorgersene prima del runtime.

Formule. L'espressione va al motore di regole e la sua sintassi resta del motore: la libreria non ha nessun parser. Cio' che ha e' il controllo, validateFormula (POST /flows/validate-formula, §6.3), chiesto con debounce mentre si digita: risponde lo stesso motore, e i suoi rilievi portano la posizione del tratto sbagliato. Li mostra <fb-formula-editor>, che monta la casella e sotto l'elenco dei rilievi, ognuno con un bottone che seleziona quel tratto nella casella — senza il salto, position resterebbe un numero da contare a mano. Lo stesso componente offre i nomi citabili come autocompletamento mentre si scrive un identificatore: la fonte e' POST /flows/references, la stessa del reference picker, output automatici compresi; frecce per scorrere, Invio o clic per inserire. Elenco non disponibile = nessun suggerimento e nessuna accusa — la formula si scrive lo stesso. Lo usano tutti i punti in cui si scrive un'espressione: la risorsa Formula, la modalita' Formula di una condizione, la filterFormula di un filtro, la regola di validazione di un campo di screen dinamico e il formulaExpression dentro un valore. A ogni punto corrispondono un usage e un expectedDataTypeBoolean per condizioni e validation rule, il tipo dichiarato per una risorsa — e mandarli e' cio' che permette al motore di dire «qui serve un booleano» invece di accettare un'espressione che fallira' davanti all'utente. Il documento si manda in lavorazione: una formula cita le variabili appena aggiunte. Tre esiti restano distinti e nessuno dei tre e' «formula valida»: primitiva assente (MissingService, silenzio), isVerified: false (motore non configurato: si dice che non e' stata controllata), isValid (verificata davvero, ed e' l'unico caso con la spunta verde) — una spunta su un'espressione mai controllata e' peggio di nessuna spunta.

Un campo a null e' assente. Il valore ha un campo per tipo e va valorizzato uno e un solo campo (§4.2); la §2 dice che cio' che non c'e' si omette, ma un backend che serializza tutte le proprieta' — System.Text.Json senza IgnoreNullValues lo fa di default — manda {"enumValue": "InIstruttoria", "formulaExpression": null, …}: un solo valore e sette caselle piene di niente. Riconoscere il campo con !== undefined faceva vincere il primo letto, cioe' formulaExpression: l'editor apriva la formula (vuota) e il valore, pur presente nel documento, spariva dalla vista — e il conteggio dei campi accusava VALUE_AMBIGUOUS. La regola sta in core/flow-value.util.ts (isValued, valuedFieldOf, valuedFieldsOf) e la usano l'editor del valore e quello delle condizioni: null e undefined significano la stessa cosa, mentre booleanValue: false, numberValue: 0 e stringValue: '' restano valori.

Perche' l'editor conosca il tipo della destinazione serve saperlo, e i percorsi non fanno eccezione: POST /flows/references elenca le radici e non i percorsi, quindi su Richiesta.Stato la corrispondenza esatta del nome non trova niente. Il tipo si ricava allora navigando la catena — core/reference-path innesta l'elenco dei riferimenti su core/path-navigation: trova il prefisso dichiarato piu' lungo (§4.1), naviga il resto un segmento alla volta e prende il tipo dell'ultimo segmento. Lo usano l'Assignment (su getWritableReferences), le condizioni e i filtri sui record — dove la radice e' l'entita', quindi Cliente.Citta si risolve come il campo che e'. In un Transform il tipo viene dal membro della classe di destinazione, che l'inspector già descrive.

Dove il tipo non si sa — catena interrotta, catalogo assente, segmento inesistente, percorso radicato sull'output automatico di un elemento (che il backend non verifica, §7) — il campo resta generico invece di indovinare: un elenco chiuso di valori sbagliati e' peggio di una casella di testo.

Lo stesso controllo, nella variante <fb-name-picker>, vale per ogni altro nome che il flow prende da fuori: tipo di action e action, script, form, i nomi dei loro parametri di ingresso e di uscita, il flow di uno step in background e dei suoi evaluation flow, il flow invocato da un Subflow e le variabili di input e output del flow invocato. Qui l'elenco arriva da chi ospita il picker — il catalogo già caricato per altri motivi, listSubflowCandidates, le variabili della definizione caricata — perche' dipende dal contesto: le action dipendono dal tipo scelto, i parametri dall'action, le variabili dal subflow.

Un elenco vuoto resta "non lo so": il campo si scrive a mano e non si accusa nessun nome. Con l'elenco popolato, un nome che non c'e' e' segnalato, e con la gravita' giusta: un'action, un form, uno script o un parametro inesistenti sono errori che bloccano l'attivazione (ACTION_UNKNOWN, FORM_UNKNOWN, PARAMETER_UNKNOWN) e si mostrano in rosso; un flow senza versione attiva e' un avviso. Prima questi campi erano <select> con un'opzione sintetica «(non nel catalogo)» per non perdere il valore, oppure un datalist che accettava qualunque cosa in silenzio: il primo non si poteva cercare, il secondo non diceva niente.

Problemi. Pannello con filtro per gravita', navigazione all'elemento con un clic, validazione su pausa di digitazione (debounce 400 ms) e l'avvertenza che l'assenza di rilievi non e' una garanzia di correttezza.

Versioni. Elenco con stato, «Nuova versione» al posto di «Salva» sulle versioni chiuse, attivazione bloccata finche' ci sono errori, eliminazione dietro una conferma esplicita che spiega il rischio per le esecuzioni sospese.

Duplica. Il flow salvato sotto un altro nome, alla versione 1 (§6.2): il comando chiede nome tecnico e nome visibile della copia, e l'editor continua sulla copia — restare sull'originale farebbe finire il salvataggio successivo nel flow sbagliato. Due strade, e la differenza conta: su un documento pulito si usa cloneFlow, che copia lato backend; con modifiche non salvate la copia si crea con createFlow mandando il documento in mano, perche' cloneFlow copierebbe la versione salvata e le modifiche appena scritte spariscono senza dirlo. La stessa strada e' il ripiego dove cloneFlow non c'e' (e' opzionale). Un nome già usato lo rifiuta il backend con AlreadyExists, e il modulo resta aperto sul nome da correggere.

Prova. Interview con form generato dalle variabili di input, form generico per gli screen (valori in ingresso più i campi da restituire), canGoBack / canFinish / canPause come verita' runtime, traccia navigabile e tabella delle risorse — con l'avviso in chiaro che la traccia riporta anche i dati personali. Su un'orchestrazione sospesa distingue le due attese (isWaitingForEvent / isWaitingForStageStep) e consente di concludere uno step al posto dell'assegnatario, rifiuto compreso: la risposta puo' portare una interviewKey nuova, e il pannello sostituisce quella vecchia invece di conservarla.

Import/export. Export JSON indentato; import via POST /flows/parse, che normalizza il documento o lo rifiuta con InvalidDefinition.


Architettura

src/lib/
  model/          i tipi: documento Flow, dizionari, payload API, errori categorizzati
  api/            FlowBuilderApi (astratta = token DI) + HttpFlowBuilderApi
  core/
    element-outlets.ts       la mappa "tipo di elemento → uscite" (§3.5), in un posto solo
    flow-document.store.ts   il documento come fonte di verita': flatten, CRUD, undo/redo
    flow-dictionary.store.ts cache dei dizionari e memoizzazione dei cataloghi
    flow-validation.store.ts debounce, indicizzazione dei rilievi per elemento e campo
    formula-validation.service.ts la verifica di una singola formula, e la memoria di
                                  "questo ambiente la primitiva non ce l'ha"
    flow-editor-session.ts   versioni, salvataggio, concorrenza ottimistica
    flow-layout.service.ts   auto-layout con dagre
    flow-clipboard.ts        copia/incolla: l'elenco unico dei campi-riferimento, la riscrittura
                             dei nomi, il piano dell'incolla (che nomi cambiano, che risorse
                             mancano, cosa resta irrisolto)
    flow-clipboard.service.ts gli appunti condivisi fra istanze e schede (localStorage)
    flow-name.util.ts        nomi: regexp, namespace unico, slug dalla label
    condition-logic.util.ts  riscrittura di conditionLogic su cancella / riordina
    condition-types.util.ts  quali tipi si confrontano, e i numeri in cultura invariante
    stage-step.util.ts       gli step di uno stage: nomi, output non condizionabili
    screen-field.util.ts     l'albero dei campi di uno screen dinamico: percorsi, nomi, mutazioni
  ui/
    flow-builder.component.ts  il componente da montare
    canvas/ palette/ inspector/ resources/ problems/ versions/ debug/ shared/
styles/
  flow-builder.css   variabili del tema, controlli di form, stili degli archi

Due scelte di progetto che vale la pena conoscere.

Il documento e' la fonte di verita' unica. Canvas, inspector e pannelli sono funzioni del FlowDefinition in memoria, e ogni gesto e' una mutazione del documento. Le primitive del backend non vengono mai chiamate dallo store, così l'editing resta sincrono e reattivo anche mentre una validazione e' in volo.

Local-first per il disegno, backend per la verita'. Archi e raggiungibilita' sono calcolati anche in locale, perche' trascinare un arco deve riflettersi subito; POST /flows/outline viene richiamato con debounce e, quando arriva, il suo isReachable sovrascrive il calcolo locale. Chiedere l'outline a ogni movimento del mouse renderebbe il canvas dipendente dalla latenza.


Dipendenze esterne

Oltre ad Angular:

| Pacchetto | Perche' | |---|---| | @foblex/flow (+ @foblex/platform, @foblex/mediator, @foblex/2d, @foblex/utils) | rendering del grafo, pan/zoom, gesti su node e connessioni. Usata in classic mode: la libreria disegna e riconosce i gesti, lo stato resta nostro | | dagre | auto-layout gerarchico del comando «Riordina» | | @angular/cdk | il solo drag-drop, per l'albero dei campi dello screen dinamico. Nessun componente Material: i controlli restano input nativi |

Nessun design system: i controlli sono input nativi con binding espliciti e CSS custom, così il builder si integra nel tema dell'app ospite senza imporre il proprio.


Tema e personalizzazione

Le variabili si sovrascrivono sul contenitore:

.mio-contenitore {
  --fb-accent: #7048c4;
  --fb-surface: #ffffff;
  --fb-border: #dcdfe4;
  --fb-error: #b3261e;
  --fb-edge-fault: #b3261e;
}

Elenco completo in styles/flow-builder.css. Per la variante scura basta data-fb-theme="dark" su un antenato.

La dialog di modifica ha una misura sola per ogni tipo di elemento — l'inspector dentro cambia, la finestra no, altrimenti i comandi si spostano sotto il mouse a ogni apertura — e le due misure sono token: --fb-dialog-width (1400px) e --fb-dialog-height (860px), entrambe limitate allo spazio disponibile. La larghezza e' quella che serve al compositore dello screen dinamico, che e' a due pannelli.

Nota sugli archi: il colore si imposta valorizzando le custom properties del tema di @foblex/flow (--ff-connection-color, --ff-marker-color) invece di sovrascrivere le sue regole, perche' i selettori hanno la stessa specificita' e chi vince dipenderebbe dall'ordine con cui l'app ospite carica i fogli di stile. Lo stesso vale per il resto del disegno che fa la libreria: flow-builder.css mappa i propri colori sui token --ff-* (sfondo del canvas, pattern, minimappa, spessore degli archi) e ne annulla due — le maniglie di riassegnazione (--ff-connection-drag-handle-*), che disegnate formavano una macchia sopra la punta della freccia. Il colore dell'uscita di un ramo passa da --ff-connector-connected-color, unico modo di sopravvivere alla regola di foblex per i connettori collegati, che ha specificita' (0,4,1) e non si batte con una classe.

L'aspetto dei node segue l'esempio call-center di @foblex/flow: card con badge dell'icona colorato per categoria, titolo, sottotitolo e uscite sul bordo inferiore. I glifi sono caratteri Unicode in core/element-icons.ts, condivisi fra palette e canvas: nessun asset da distribuire e nessuna dipendenza da un font di icone dell'applicazione ospite.


Le trappole della §13, una per una

| # | Trappola | Dove e' gestita | |---|---|---| | 1 | Operatori unari: rightValue e' l'esito atteso, non il termine di confronto | ConditionEditorComponent: per gli operatori con isUnary il campo «confronta con» non esiste; c'e' un selettore «è / NON è» e la frase «questa condizione e' vera quando: Esito NON è vuoto». Il flag arriva dal dizionario, non da una lista cablata | | 2 | assignNextValueToReference di un Collection Processor non e' la destinazione | CollectionProcessorInspectorComponent: il campo si chiama «Elemento in esame», e un callout dice che il risultato e' l'output automatico dell'elemento | | 3 | Gli output automatici sono riferimenti validi anche se non dichiarati | ReferencePickerComponent chiede l'elenco a POST /flows/references, che li include; non ricostruisce nulla lato client | | 4 | Cancellare una condizione richiede di riscrivere conditionLogic | condition-logic.util.ts: removeCondition e moveCondition rimappano gli indici 1-based; un termine rimasto orfano diventa ? e viene segnalato | | 5 | Node e risorse condividono un unico spazio di nomi | FlowDocumentStore.usedNames unisce cinque insiemi — node, risorse, step di orchestrazione, campi di screen dinamico e screen action, cioe' tutto cio' il cui nome e' la radice di un riferimento (§3.3) — e checkFlowName confronta case-insensitive; pannello risorse e inspector usano lo stesso controllo | | 6 | processType non e' decorativo | Banner in cima all'editor per AutoLaunched + screen, Orchestration senza stage, Screen senza screen; la palette segnala lo Screen incompatibile. Il processType viaggia anche a getDictionaries, perche' decide quali globali esistono | | 7 | Le regole di una Decision sono ordinate | DecisionInspectorComponent: regole numerate, frecce di riordino, callout «valutate dall'alto verso il basso, si ferma alla prima vera» | | 8 | Rinominare non aggiorna i riferimenti | FlowDocumentStore.renameNode cammina il documento e riscrive ogni campo di riferimento, incluso il caso navigato (Vecchio.CampoNuovo.Campo); l'inspector dice quante occorrenze verranno toccate | | 9 | Step ed Experiment non sono supportati; OrchestratedStage lo e' | I due non supportati sono filtrati dalla palette via isSupported e, se arrivano da un import, l'inspector li mostra in sola lettura spiegando perche'. Lo stage ha il suo form: step non presentati come sequenza, e la condizione che referenzia l'output di un altro step segnalata con la via d'uscita | | 10 | Un Delete senza filtri e' un errore, un Update senza filtri un avviso | RecordWriteInspectorComponent: severita' diverse, e l'update di massa chiede una conferma esplicita | | 11 | Non cablare enum e operatori | Ogni tendina legge da getDictionaries(); le mappe locali intervengono solo come fallback quando il dizionario non e' disponibile | | 12 | Il token di continuazione e' opaco | DebugPanelComponent lo rimanda tale e quale, non lo legge e non lo mette in nessun URL | | 13 | Testo e numero non si confrontano (CONDITION_TYPE_MISMATCH) | ConditionEditorComponent conosce il tipo del lato sinistro da POST /flows/references: filtra gli operatori applicabili, guida il campo del letterale del secondo operando e segnala la coppia vietata. La regola sta in core/condition-types.util.ts, che salta gli operatori con semantica propria (Contains, In, …). I numeri si scrivono in cultura invariante: 1234,50 viene tradotto in 1234.50, non troncato | | 14 | None non e' un operatore, e' un "da completare" | Condizione evidenziata come incompleta, con il conteggio in testa alla sezione e la frase «la bozza si salva, l'attivazione no»; il validatore della demo lo emette come errore | | 15 | Un valore data senza Z significa ora locale | ValueEditorComponent ha un selettore «Ora locale / UTC (Z)», dice che cambiare fuso riscrive l'orario e non lo converte, e toglie il suffisso solo per il controllo datetime-local, che con il fuso resterebbe vuoto | | 16 | Structure e Object non sono intercambiabili | StructurePickerComponent per la classe (fuori catalogo = errore, STRUCTURE_TYPE_UNKNOWN) e StructureMemberPickerComponent per i membri, filtrati su isWritable dove serve una destinazione. Il pannello risorse mostra la classe come obbligatoria con severita' di errore e non mostra il valore iniziale, perche' un structureValue non esiste; ReferencePickerComponent propone Variabile.Membro e verifica il percorso fino in fondo con core/path-navigation.ts, con PATH_NOT_VERIFIABLE dove la catena si interrompe; il Transform con target classe chiede un membro invece di un campo. FlowDictionaryStore.isStructure legge il flag dal dizionario, non dal nome del tipo. La risposta dei membri e' la descrizione della classe ({className, label, members}), e i suoi tre esiti restano distinti fino alla UI: FlowCatalogStore.describeStructure li traduce in declared / undeclared / unknown-class / unknown-catalog, e solo declared autorizza a dire che un membro non esiste (§4.7.1) | | 17 | Due body diversi per due famiglie di primitive | HttpFlowBuilderApi: le primitive che analizzano il documento — validate, parse, references, references/writable, connector-targets, outline — lo mandano nudo alla radice; l'involucro {definition, flowName, …} resta alle sole primitive di scrittura della §6.2. Sbagliare involucro non da' nessun errore: il backend legge un flow vuoto e risponde 200. Per lo stesso motivo getReferences/getWritableReferences prendono la FlowDefinition e non un oggetto query: non c'e' nessun campo di filtro nel body, e il filtro di tipo lo applica l'editor con core/reference-filter.util.ts | | 18 | Gli enum si scrivono come stringhe | Il modello e' fatto di unioni di stringhe letterali (FlowDataType, FlowVersionStatus, …) e non di enum numerici: JSON.stringify non ha modo di produrre un ordinale. Conta perche' "status": 3 non e' Draft ma InvalidDraft e "dataType": 0 e' String, non "non specificato" — nessun errore lo segnalerebbe. Chi scrive un'altra implementazione di FlowBuilderApi non traduca i valori in numeri | | 19 | Un campo di screen dinamico e' una risorsa, ma di sola lettura | Lo restituisce POST /flows/references con kind: "ScreenField", e il reference picker lo propone in un gruppo suo; nei selettori di destinazione non compare, perche' references/writable lo esclude. L'unica eccezione e' l'assignToReference di una screen action, dove il campo di quella schermata e' una destinazione e l'elenco lo passa il chiamante (extraReferences). Il nome entra in FlowDocumentStore.usedNames, quindi un campo omonimo di una variabile e' NAME_DUPLICATED, e renameScreenField riscrive i riferimenti come renameNode | | 20 | La validazione di uno screen dinamico la fa il server e non fa avanzare il flow | Riguarda il runtime, non l'editor: EXECUTION_FRONTEND.md §5.5 e §6.2, e lato client flow-execution (isValidationRetry, indexValidationErrors). Qui l'editor fa la sua parte a monte: validationRule si scrive con <fb-formula-editor>, che la fa verificare mentre si digita (§6.3) | | 21 | Un campo nascosto viene azzerato all'invio | L'inspector lo dice dove si scrive la visibilityRule: la regola si rivaluta sui valori appena inviati e il campo che sparisce perde il valore. È l'unica cosa che chi disegna il flow non puo' dedurre dal metadata, e per questo sta nel form e non solo in questa tabella |

Altre regole del contratto rispettate: liste vuote omesse invece di scritte come [] (§2), filterLogic mostrato solo dove il modello lo prevede (§4.4), fullName / status mai usati per rinominare o attivare (§2), un solo campo valorizzato in FlowElementReferenceOrValue con segnalazione e normalizzazione dei documenti ambigui (§4.2), Transform.connector e start trattati come array per fedelta' storica (§3.5), salvataggio possibile anche con errori e attivazione bloccata dagli errori (§8).


Limiti noti

  • Le uscite indicizzate cambiano chiave se si riordinano le regole: la chiave di un connettore contiene l'indice (rule:0), quindi riordinare le regole di una Decision riassegna gli id dei connettori sul canvas. Le destinazioni seguono la regola, perche' vivono dentro rules[i].connector, ma un arco selezionato perde la selezione.
  • relatedRecords di Get Records e' esposto in sola lettura con un avviso: e' modellato ma non tradotto in query.
  • La verifica delle formule e' del sistema ospite: senza un motore configurato l'editor non ha nulla da dire sulla sintassi, e lo dice — non finge una conferma. La libreria non prova a supplire con un parser proprio: una grammatica indovinata segnalerebbe come sbagliate le espressioni che il motore vero accetta.
  • Il compositore dello screen dinamico non usa le drop list del CDK: annidate sono ambigue — il rettangolo di una sezione contiene quello delle sue colonne, e a ricevere il rilascio e' sempre la lista registrata per prima, cioe' la radice. Il bersaglio si calcola dal DOM (elementFromPoint per il contenitore piu' interno, i rettangoli dei fratelli per la posizione).
  • L'esecutore della demo non valuta le condizioni: prende il primo ramo disponibile e lo scrive nella traccia. È un mock per esercitare il pannello, non il motore.
  • Nessun test automatico: la libreria e' stata verificata a mano sull'app demo (canvas, inspector, salvataggio, attivazione, interview completa). Casi su condition-logic.util, flow-name.util, element-outlets e FlowDocumentStore sarebbero il primo investimento sensato.