@hostwebhook/node-types 1.84.0 → 1.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,16 +39,21 @@ export declare function isCalendlyOperation(value: unknown): value is CalendlyOp
39
39
  */
40
40
  export declare const CALENDLY_ITERABLE_OPERATIONS: ReadonlyArray<CalendlyOperation>;
41
41
  /**
42
- * Tipos de parámetro. Los seis primeros son los de Apify. Los dos nuevos:
42
+ * Tipos de parámetro. Los seis primeros son los de Apify. Los tres nuevos:
43
43
  *
44
- * - `datetime`: una fecha con hora, ISO 8601, en la zona horaria del
45
- * usuario; el dashboard la pinta con el selector de Calendar y admite
46
- * plantilla (`{{payload.startTime}}`).
44
+ * - `datetime`: una fecha con hora de PARED (`2026-09-20T10:00:00`, sin
45
+ * desfase), que el nodo sitúa en la zona del parámetro `timezone` de esa
46
+ * operación —o en la del workspace si no se eligió—, igual que hace el
47
+ * nodo de Google Calendar. Con desfase (`Z`, `+02:00`) manda el desfase.
48
+ * El dashboard la pinta con el mismo calendario que Calendar y admite
49
+ * plantilla (`{{payload.from}}`).
50
+ * - `timezone`: la zona en la que se leen las fechas de pared de la
51
+ * operación. Sin elegir, la del workspace.
47
52
  * - `eventType`: un desplegable que lista los tipos de evento de la
48
53
  * credencial (`GET /calendly-actions/event-types`); el valor es la URI del
49
54
  * tipo. Admite plantilla para cuando viene del payload.
50
55
  */
51
- export type CalendlyParamType = "template" | "textarea" | "json" | "select" | "number" | "booleanSelect" | "datetime" | "eventType";
56
+ export type CalendlyParamType = "template" | "textarea" | "json" | "select" | "number" | "booleanSelect" | "datetime" | "timezone" | "eventType";
52
57
  export interface CalendlyParamSpec {
53
58
  name: string;
54
59
  label: string;
@@ -88,6 +88,16 @@ const eventType = () => ({
88
88
  required: true,
89
89
  description: "One of the event types on the connected account (e.g. “30 min intro call”).",
90
90
  });
91
+ /* La zona en la que se leen las fechas de pared de la operación. Sin elegir,
92
+ la del workspace: el dashboard lo dice debajo del selector y el nodo la lee
93
+ al ejecutar, como Calendar. */
94
+ const timezone = () => ({
95
+ name: "timezone",
96
+ label: "Time zone",
97
+ type: "timezone",
98
+ advanced: true,
99
+ description: "The zone the dates above are read in when they carry no offset. Empty = the workspace's time zone.",
100
+ });
91
101
  const pageLimit = (max, fallback) => ({
92
102
  name: "limit",
93
103
  label: "Max results",
@@ -131,6 +141,7 @@ exports.CALENDLY_OPERATION_SPECS = {
131
141
  placeholder: "{{payload.to}}",
132
142
  description: "Only events that start before this moment. Empty = no upper bound.",
133
143
  },
144
+ timezone(),
134
145
  {
135
146
  name: "inviteeEmail",
136
147
  label: "Invitee email",
@@ -278,6 +289,7 @@ exports.CALENDLY_OPERATION_SPECS = {
278
289
  placeholder: "{{payload.to}}",
279
290
  description: "End of the window — at most 7 days after From. A longer window is cut to 7 days.",
280
291
  },
292
+ timezone(),
281
293
  ],
282
294
  },
283
295
  createSchedulingLink: {
@@ -39,16 +39,21 @@ export declare function isCalendlyOperation(value: unknown): value is CalendlyOp
39
39
  */
40
40
  export declare const CALENDLY_ITERABLE_OPERATIONS: ReadonlyArray<CalendlyOperation>;
41
41
  /**
42
- * Tipos de parámetro. Los seis primeros son los de Apify. Los dos nuevos:
42
+ * Tipos de parámetro. Los seis primeros son los de Apify. Los tres nuevos:
43
43
  *
44
- * - `datetime`: una fecha con hora, ISO 8601, en la zona horaria del
45
- * usuario; el dashboard la pinta con el selector de Calendar y admite
46
- * plantilla (`{{payload.startTime}}`).
44
+ * - `datetime`: una fecha con hora de PARED (`2026-09-20T10:00:00`, sin
45
+ * desfase), que el nodo sitúa en la zona del parámetro `timezone` de esa
46
+ * operación —o en la del workspace si no se eligió—, igual que hace el
47
+ * nodo de Google Calendar. Con desfase (`Z`, `+02:00`) manda el desfase.
48
+ * El dashboard la pinta con el mismo calendario que Calendar y admite
49
+ * plantilla (`{{payload.from}}`).
50
+ * - `timezone`: la zona en la que se leen las fechas de pared de la
51
+ * operación. Sin elegir, la del workspace.
47
52
  * - `eventType`: un desplegable que lista los tipos de evento de la
48
53
  * credencial (`GET /calendly-actions/event-types`); el valor es la URI del
49
54
  * tipo. Admite plantilla para cuando viene del payload.
50
55
  */
51
- export type CalendlyParamType = "template" | "textarea" | "json" | "select" | "number" | "booleanSelect" | "datetime" | "eventType";
56
+ export type CalendlyParamType = "template" | "textarea" | "json" | "select" | "number" | "booleanSelect" | "datetime" | "timezone" | "eventType";
52
57
  export interface CalendlyParamSpec {
53
58
  name: string;
54
59
  label: string;
@@ -84,6 +84,16 @@ const eventType = () => ({
84
84
  required: true,
85
85
  description: "One of the event types on the connected account (e.g. “30 min intro call”).",
86
86
  });
87
+ /* La zona en la que se leen las fechas de pared de la operación. Sin elegir,
88
+ la del workspace: el dashboard lo dice debajo del selector y el nodo la lee
89
+ al ejecutar, como Calendar. */
90
+ const timezone = () => ({
91
+ name: "timezone",
92
+ label: "Time zone",
93
+ type: "timezone",
94
+ advanced: true,
95
+ description: "The zone the dates above are read in when they carry no offset. Empty = the workspace's time zone.",
96
+ });
87
97
  const pageLimit = (max, fallback) => ({
88
98
  name: "limit",
89
99
  label: "Max results",
@@ -127,6 +137,7 @@ export const CALENDLY_OPERATION_SPECS = {
127
137
  placeholder: "{{payload.to}}",
128
138
  description: "Only events that start before this moment. Empty = no upper bound.",
129
139
  },
140
+ timezone(),
130
141
  {
131
142
  name: "inviteeEmail",
132
143
  label: "Invitee email",
@@ -274,6 +285,7 @@ export const CALENDLY_OPERATION_SPECS = {
274
285
  placeholder: "{{payload.to}}",
275
286
  description: "End of the window — at most 7 days after From. A longer window is cut to 7 days.",
276
287
  },
288
+ timezone(),
277
289
  ],
278
290
  },
279
291
  createSchedulingLink: {
@@ -2,6 +2,10 @@ export type { NodeType, NodeRole, HandlePosition, NodeConnectionConfig, NodeUICo
2
2
  export { singleMeta, iterableMeta } from './types.js';
3
3
  export { NODE_CONNECTIONS, canReceiveFrom, canReceiveFromNodes, canSendToNodes, isTerminal, isNodeType, ALL_NODE_TYPES, } from './connections.js';
4
4
  export { NODE_UI, PREFIX_TO_TYPE, resolveNodeId, } from './ui.js';
5
+ export { SENTIMENT_LABELS, SENTIMENT_UNCLEAR, SENTIMENT_PORTS, SENTIMENT_PORT_LABEL, SENTIMENT_PORT_SUMMARY, esEtiquetaDeSentimiento, } from './sentiment.js';
6
+ export type { SentimentLabel, SentimentPort } from './sentiment.js';
7
+ export { outputGroupsFor, todosLosOutputGroups } from './types.js';
8
+ export type { OutputGroupsConfig } from './types.js';
5
9
  export { NODE_DISPATCH, getAllNodeCollections, getNodeDispatchConfig, pasaLoQueRecibe, } from './dispatch.js';
6
10
  export type { NodeRegistryEntry, NodeGroup, NodeVersionSpec, VersionCatalog, } from './registry.js';
7
11
  export { NODE_REGISTRY, getNodeRegistryEntry, NODE_DETAIL_PATHS, NODE_COLORS, NODE_STATE_KEYS, PREFIX_TO_NODE_TYPE, NODE_TYPE_TO_PREFIX, } from './registry.js';
package/dist/esm/index.js CHANGED
@@ -3,6 +3,8 @@ export { singleMeta, iterableMeta } from './types.js';
3
3
  export { NODE_CONNECTIONS, canReceiveFrom, canReceiveFromNodes, canSendToNodes, isTerminal, isNodeType, ALL_NODE_TYPES, } from './connections.js';
4
4
  // ── UI config (Dashboard only) ──
5
5
  export { NODE_UI, PREFIX_TO_TYPE, resolveNodeId, } from './ui.js';
6
+ export { SENTIMENT_LABELS, SENTIMENT_UNCLEAR, SENTIMENT_PORTS, SENTIMENT_PORT_LABEL, SENTIMENT_PORT_SUMMARY, esEtiquetaDeSentimiento, } from './sentiment.js';
7
+ export { outputGroupsFor, todosLosOutputGroups } from './types.js';
6
8
  // ── Dispatch config ──
7
9
  /* Ya no es «API only»: `downstreamPayload` lo lee tambien el panel, que sin el
8
10
  ensenaba como entrada de un nodo la salida del que pasa de largo. */
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Sentiment analysis en el AI Node — las etiquetas, los puertos y el orden.
3
+ *
4
+ * ## Por qué vive aquí y no en cada repo
5
+ *
6
+ * Las tres etiquetas son a la vez TRES cosas: el `enum` del esquema que se le
7
+ * da al modelo, el nombre de los puertos por los que el nodo despacha, y las
8
+ * pastillas de la tarjeta del lienzo. Si el api, hw-nodes y el dashboard
9
+ * escriben su propia lista, la primera que alguien reordene o rebautice deja
10
+ * una arista apuntando a un puerto que ya no existe — y eso no lo dice ningún
11
+ * compilador. Es el mismo motivo por el que las operaciones de cada nodo
12
+ * viven en este paquete.
13
+ *
14
+ * ## Por qué `unclear` no es una etiqueta más
15
+ *
16
+ * El modelo elige entre las TRES de `SENTIMENT_LABELS`; `unclear` no se le
17
+ * ofrece nunca. Es lo que hace el NODO cuando la confianza queda por debajo
18
+ * del umbral (`sentimentThreshold`) o cuando la respuesta no se puede leer.
19
+ * Ofrecérsela al modelo sería darle una salida cómoda para no mojarse, que es
20
+ * justo lo contrario de lo que se busca al poner un umbral.
21
+ *
22
+ * A diferencia del `other` del clasificador, `unclear` SÍ tiene puerto: el
23
+ * caso de uso es mandar a revisión humana lo que el modelo no ve claro, y sin
24
+ * puerto eso obligaba a adivinar por qué un item no salió por ninguna rama.
25
+ */
26
+ /** Lo que el modelo puede contestar, en orden de peor a mejor. */
27
+ export declare const SENTIMENT_LABELS: readonly ["negative", "neutral", "positive"];
28
+ export type SentimentLabel = (typeof SENTIMENT_LABELS)[number];
29
+ /** La salida del nodo cuando no hay etiqueta fiable. No se le ofrece al modelo. */
30
+ export declare const SENTIMENT_UNCLEAR = "unclear";
31
+ /** Los cuatro puertos del modo sentiment, en el orden en que se pintan. */
32
+ export declare const SENTIMENT_PORTS: readonly ["negative", "neutral", "positive", "unclear"];
33
+ export type SentimentPort = (typeof SENTIMENT_PORTS)[number];
34
+ /** El nombre que ve la persona, por puerto. */
35
+ export declare const SENTIMENT_PORT_LABEL: Record<SentimentPort, string>;
36
+ /** Qué significa cada puerto, para el modal de conexión y la doc. */
37
+ export declare const SENTIMENT_PORT_SUMMARY: Record<SentimentPort, string>;
38
+ /** ¿Es una de las tres que el modelo puede contestar? */
39
+ export declare function esEtiquetaDeSentimiento(v: unknown): v is SentimentLabel;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Sentiment analysis en el AI Node — las etiquetas, los puertos y el orden.
3
+ *
4
+ * ## Por qué vive aquí y no en cada repo
5
+ *
6
+ * Las tres etiquetas son a la vez TRES cosas: el `enum` del esquema que se le
7
+ * da al modelo, el nombre de los puertos por los que el nodo despacha, y las
8
+ * pastillas de la tarjeta del lienzo. Si el api, hw-nodes y el dashboard
9
+ * escriben su propia lista, la primera que alguien reordene o rebautice deja
10
+ * una arista apuntando a un puerto que ya no existe — y eso no lo dice ningún
11
+ * compilador. Es el mismo motivo por el que las operaciones de cada nodo
12
+ * viven en este paquete.
13
+ *
14
+ * ## Por qué `unclear` no es una etiqueta más
15
+ *
16
+ * El modelo elige entre las TRES de `SENTIMENT_LABELS`; `unclear` no se le
17
+ * ofrece nunca. Es lo que hace el NODO cuando la confianza queda por debajo
18
+ * del umbral (`sentimentThreshold`) o cuando la respuesta no se puede leer.
19
+ * Ofrecérsela al modelo sería darle una salida cómoda para no mojarse, que es
20
+ * justo lo contrario de lo que se busca al poner un umbral.
21
+ *
22
+ * A diferencia del `other` del clasificador, `unclear` SÍ tiene puerto: el
23
+ * caso de uso es mandar a revisión humana lo que el modelo no ve claro, y sin
24
+ * puerto eso obligaba a adivinar por qué un item no salió por ninguna rama.
25
+ */
26
+ /** Lo que el modelo puede contestar, en orden de peor a mejor. */
27
+ export const SENTIMENT_LABELS = ['negative', 'neutral', 'positive'];
28
+ /** La salida del nodo cuando no hay etiqueta fiable. No se le ofrece al modelo. */
29
+ export const SENTIMENT_UNCLEAR = 'unclear';
30
+ /** Los cuatro puertos del modo sentiment, en el orden en que se pintan. */
31
+ export const SENTIMENT_PORTS = [
32
+ ...SENTIMENT_LABELS,
33
+ SENTIMENT_UNCLEAR,
34
+ ];
35
+ /** El nombre que ve la persona, por puerto. */
36
+ export const SENTIMENT_PORT_LABEL = {
37
+ negative: 'Negative',
38
+ neutral: 'Neutral',
39
+ positive: 'Positive',
40
+ unclear: 'Unclear',
41
+ };
42
+ /** Qué significa cada puerto, para el modal de conexión y la doc. */
43
+ export const SENTIMENT_PORT_SUMMARY = {
44
+ negative: 'Complaints, frustration, criticism',
45
+ neutral: 'Factual, mixed, or no clear feeling',
46
+ positive: 'Praise, satisfaction, enthusiasm',
47
+ unclear: 'Below the confidence floor, or unreadable',
48
+ };
49
+ /** ¿Es una de las tres que el modelo puede contestar? */
50
+ export function esEtiquetaDeSentimiento(v) {
51
+ return (typeof v === 'string' &&
52
+ SENTIMENT_LABELS.includes(v));
53
+ }
@@ -54,83 +54,15 @@ export interface NodeUIConfig {
54
54
  connectModal?: 'branch' | 'output-group';
55
55
  routerTargetLabel?: string;
56
56
  /**
57
- * Generic named output groups — used by connectModal: 'output-group'.
58
- * The modal reads entity[outputGroupsField] as an array of groups,
59
- * each with a name (outputGroupNameField), a summary (outputGroupSummaryField),
60
- * and its own outputNodes[].
61
- * Works for Split (outputs[]), Conditional (branches[]), or any future branching node.
57
+ * Los juegos de salidas de un nodo que ramifica: UNO, o VARIOS con su
58
+ * `gate` cada uno [2026-09-22]. Con varios, quien pregunta se queda con
59
+ * el PRIMERO cuya puerta pasa — ver `outputGroupsFor`. El AI Node los
60
+ * necesita porque ramifica de dos maneras según el modo: por las
61
+ * categorías que escribe el usuario (`classifier`) o por los cuatro
62
+ * puertos fijos del análisis de sentimiento (`sentiment`). La forma
63
+ * vieja (un objeto suelto) sigue valiendo y significa «sin alternativas».
62
64
  */
63
- outputGroups?: {
64
- /** Entity field containing the array of groups (e.g. 'outputs', 'branches') */
65
- field?: string;
66
- /** Field within each group for display name (e.g. 'name') */
67
- nameField?: string;
68
- /** Field within each group for summary/description (e.g. 'field', 'filterMode') */
69
- summaryField?: string;
70
- /**
71
- * Varios campos del grupo, unidos por espacios, cuando el resumen que
72
- * de verdad identifica al grupo no cabe en uno solo.
73
- *
74
- * Existe por las reglas del router: su condición son TRES campos
75
- * (`field`, `operator`, `value`) y el modal a medida que se retiró las
76
- * pintaba juntas — «status eq active». Con un único `summaryField` una
77
- * regla sin `label` quedaba en «rule 1» y «status», que no dice a qué
78
- * se está conectando uno.
79
- *
80
- * Los campos vacíos se saltan, para que una regla con operador
81
- * `exists` no salga con un espacio colgando al final.
82
- */
83
- summaryFields?: string[];
84
- /** Label shown in the modal (e.g. 'output', 'branch') */
85
- label: string;
86
- /** Entity field for the fallback/else path (e.g. 'elseOutputNodes') */
87
- elseField?: string;
88
- /**
89
- * Fixed output groups — for nodes with a static set of outputs (e.g. approval: approve/reject).
90
- * Each entry maps to a top-level entity field that holds outputNodes[].
91
- * When set, `field`/`nameField` are ignored — modal shows these fixed options.
92
- */
93
- fixedGroups?: Array<{
94
- name: string;
95
- field: string;
96
- }>;
97
- /**
98
- * Optional content-aware gate — the output-group modal only opens
99
- * when `entity[gate.field] === gate.value`. Lets a node opt into
100
- * multi-branch routing only in some modes (e.g. AI Node in
101
- * classifier mode) without trapping every connection in the
102
- * picker the rest of the time. When the gate fails, the
103
- * FlowCanvas falls through to the universal
104
- * `entity.outputNodes` connect path.
105
- */
106
- gate?: {
107
- field: string;
108
- value: unknown;
109
- };
110
- /**
111
- * Qué es el puerto `main` de este nodo MIENTRAS su contenedor está
112
- * vacío. Sin esto, conectar a un nodo que ramifica y todavía no tiene
113
- * ninguna salida escribía una arista en `main` **en silencio**, y el
114
- * usuario no tenía forma de saber qué había pasado con ella.
115
- *
116
- * No hay una respuesta única, y por eso es un dato y no una regla.
117
- * Medido en `api/src/common/pipeline-run.service.ts`:
118
- *
119
- * 'passthrough' — router sin reglas: `rules.length === 0
120
- * ? refsForPort(entity, MAIN_PORT) : []`. La
121
- * arista SÍ dispara... hasta que se cree la
122
- * primera regla, y entonces deja de hacerlo
123
- * sin avisar.
124
- * 'never-dispatched' — split: no tiene salida `main`, y el propio
125
- * despachador registra un warning por cada
126
- * arista que se quedó en ese puerto.
127
- *
128
- * Se declara sólo donde se ha medido. Sin declarar, el lienzo se
129
- * comporta como siempre: conecta por el camino universal y no dice
130
- * nada, que es lo que hacía antes de existir este campo.
131
- */
132
- emptyContainerMain?: 'passthrough' | 'never-dispatched';
133
- };
65
+ outputGroups?: OutputGroupsConfig | OutputGroupsConfig[];
134
66
  /**
135
67
  * Maps output fields to alternate payload fields on the source entity.
136
68
  * When a downstream node is connected via a specific output field,
@@ -224,3 +156,106 @@ export interface PrefixMapping {
224
156
  /** Canvas node type used in React Flow nodeTypes map (e.g. "cache-node", "email-action") */
225
157
  canvasType: string;
226
158
  }
159
+ /**
160
+ * Un juego de salidas de un nodo que ramifica.
161
+ *
162
+ * Vivía dentro de `NodeUIConfig.special` como un objeto anónimo; sale aquí
163
+ * [2026-09-22] para poder declarar varios por nodo y para que quien los lea
164
+ * nombre el tipo en vez de repetir su forma.
165
+ */
166
+ export interface OutputGroupsConfig {
167
+ /** Entity field containing the array of groups (e.g. 'outputs', 'branches') */
168
+ field?: string;
169
+ /** Field within each group for display name (e.g. 'name') */
170
+ nameField?: string;
171
+ /** Field within each group for summary/description (e.g. 'field', 'filterMode') */
172
+ summaryField?: string;
173
+ /**
174
+ * Varios campos del grupo, unidos por espacios, cuando el resumen que
175
+ * de verdad identifica al grupo no cabe en uno solo.
176
+ *
177
+ * Existe por las reglas del router: su condición son TRES campos
178
+ * (`field`, `operator`, `value`) y el modal a medida que se retiró las
179
+ * pintaba juntas — «status eq active». Con un único `summaryField` una
180
+ * regla sin `label` quedaba en «rule 1» y «status», que no dice a qué
181
+ * se está conectando uno.
182
+ *
183
+ * Los campos vacíos se saltan, para que una regla con operador
184
+ * `exists` no salga con un espacio colgando al final.
185
+ */
186
+ summaryFields?: string[];
187
+ /** Label shown in the modal (e.g. 'output', 'branch') */
188
+ label: string;
189
+ /** Entity field for the fallback/else path (e.g. 'elseOutputNodes') */
190
+ elseField?: string;
191
+ /**
192
+ * Fixed output groups — for nodes with a static set of outputs (e.g. approval: approve/reject).
193
+ * When set, `field`/`nameField` are ignored — modal shows these fixed options.
194
+ *
195
+ * Cada entrada nombra su puerto de una de dos formas, y sólo una:
196
+ * · `field` — un campo de primer nivel de los de ANTES de que los puertos
197
+ * existieran (`rejectionOutputNodes` → puerto `rejected`); se traduce
198
+ * con el mapa de campos heredados.
199
+ * · `port` — el puerto, tal cual. Es lo que usan las salidas nacidas ya
200
+ * con puertos (las cuatro de sentiment): inventarles un campo heredado
201
+ * que nunca existió sería mentir en el registro.
202
+ */
203
+ fixedGroups?: Array<{
204
+ name: string;
205
+ field?: string;
206
+ port?: string;
207
+ /** Una línea de qué entrega ese puerto, para el modal de conexión. */
208
+ summary?: string;
209
+ }>;
210
+ /**
211
+ * Optional content-aware gate — the output-group modal only opens
212
+ * when `entity[gate.field] === gate.value`. Lets a node opt into
213
+ * multi-branch routing only in some modes (e.g. AI Node in
214
+ * classifier mode) without trapping every connection in the
215
+ * picker the rest of the time. When the gate fails, the
216
+ * FlowCanvas falls through to the universal
217
+ * `entity.outputNodes` connect path.
218
+ */
219
+ gate?: {
220
+ field: string;
221
+ value: unknown;
222
+ };
223
+ /**
224
+ * Qué es el puerto `main` de este nodo MIENTRAS su contenedor está
225
+ * vacío. Sin esto, conectar a un nodo que ramifica y todavía no tiene
226
+ * ninguna salida escribía una arista en `main` **en silencio**, y el
227
+ * usuario no tenía forma de saber qué había pasado con ella.
228
+ *
229
+ * No hay una respuesta única, y por eso es un dato y no una regla.
230
+ * Medido en `api/src/common/pipeline-run.service.ts`:
231
+ *
232
+ * 'passthrough' — router sin reglas: `rules.length === 0
233
+ * ? refsForPort(entity, MAIN_PORT) : []`. La
234
+ * arista SÍ dispara... hasta que se cree la
235
+ * primera regla, y entonces deja de hacerlo
236
+ * sin avisar.
237
+ * 'never-dispatched' — split: no tiene salida `main`, y el propio
238
+ * despachador registra un warning por cada
239
+ * arista que se quedó en ese puerto.
240
+ *
241
+ * Se declara sólo donde se ha medido. Sin declarar, el lienzo se
242
+ * comporta como siempre: conecta por el camino universal y no dice
243
+ * nada, que es lo que hacía antes de existir este campo.
244
+ */
245
+ emptyContainerMain?: 'passthrough' | 'never-dispatched';
246
+ }
247
+ /**
248
+ * El juego de salidas que aplica AHORA a esta entidad, o `undefined` si el
249
+ * nodo no ramifica en este momento.
250
+ *
251
+ * Es la única lectura de `special.outputGroups` que entiende las dos formas
252
+ * (uno o varios) y las dos puertas (con `gate` o sin ella). Todo el que
253
+ * pregunte tiene que pasar por aquí: el lienzo, el modal de conexión y el
254
+ * despachador. Escribirlo cada uno a su manera es como el router acabó con un
255
+ * camino propio que nadie arregló al arreglar el de los demás.
256
+ */
257
+ export declare function outputGroupsFor(config: NodeUIConfig | undefined | null, entity: Record<string, unknown> | undefined | null): OutputGroupsConfig | undefined;
258
+ /** Todos los juegos declarados, pasen o no su puerta. Para quien necesita el
259
+ * UNIVERSO de puertos que un tipo puede emitir (la auditoría de puertos),
260
+ * no el de ahora mismo. */
261
+ export declare function todosLosOutputGroups(config: NodeUIConfig | undefined | null): OutputGroupsConfig[];
package/dist/esm/types.js CHANGED
@@ -6,3 +6,29 @@ export function singleMeta() {
6
6
  export function iterableMeta(field, count) {
7
7
  return { iterable: true, iterateField: field, count };
8
8
  }
9
+ /**
10
+ * El juego de salidas que aplica AHORA a esta entidad, o `undefined` si el
11
+ * nodo no ramifica en este momento.
12
+ *
13
+ * Es la única lectura de `special.outputGroups` que entiende las dos formas
14
+ * (uno o varios) y las dos puertas (con `gate` o sin ella). Todo el que
15
+ * pregunte tiene que pasar por aquí: el lienzo, el modal de conexión y el
16
+ * despachador. Escribirlo cada uno a su manera es como el router acabó con un
17
+ * camino propio que nadie arregló al arreglar el de los demás.
18
+ */
19
+ export function outputGroupsFor(config, entity) {
20
+ const declarado = config?.special?.outputGroups;
21
+ if (!declarado)
22
+ return undefined;
23
+ const lista = Array.isArray(declarado) ? declarado : [declarado];
24
+ return lista.find((c) => !c.gate || entity?.[c.gate.field] === c.gate.value);
25
+ }
26
+ /** Todos los juegos declarados, pasen o no su puerta. Para quien necesita el
27
+ * UNIVERSO de puertos que un tipo puede emitir (la auditoría de puertos),
28
+ * no el de ahora mismo. */
29
+ export function todosLosOutputGroups(config) {
30
+ const declarado = config?.special?.outputGroups;
31
+ if (!declarado)
32
+ return [];
33
+ return Array.isArray(declarado) ? declarado : [declarado];
34
+ }
package/dist/esm/ui.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Dashboard UI config — handle positions, dot visibility, canvas prefixes.
3
3
  * Only imported by the Dashboard. Broker and API don't need this.
4
4
  */
5
+ import { SENTIMENT_PORTS, SENTIMENT_PORT_LABEL, SENTIMENT_PORT_SUMMARY, } from './sentiment.js';
5
6
  export const NODE_UI = {
6
7
  // ── Source nodes ──
7
8
  // Una fuente arranca el flujo: `fromNodes: false` significa que nada puede
@@ -45,7 +46,15 @@ export const NODE_UI = {
45
46
  // no handle and no route. In any other outputMode the node falls back to
46
47
  // the single right-out handle NodeShell renders when no dynamic outputs
47
48
  // materialize.
48
- ai: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { loopBackAllowed: true, connectModal: 'output-group', outputGroups: { field: 'categories', nameField: 'name', summaryField: 'description', label: 'category', gate: { field: 'outputMode', value: 'classifier' } } } },
49
+ // En modo `sentiment` (2026-09-22) el abanico son CUATRO puertos fijos
50
+ // —negative / neutral / positive / unclear— y no un contenedor del usuario:
51
+ // el modelo elige entre las tres primeras y el nodo manda a `unclear` lo que
52
+ // no llega al umbral de confianza. Por eso el AI Node declara DOS juegos de
53
+ // salidas, uno por modo; `outputGroupsFor` elige el que toca.
54
+ ai: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { loopBackAllowed: true, connectModal: 'output-group', outputGroups: [
55
+ { field: 'categories', nameField: 'name', summaryField: 'description', label: 'category', gate: { field: 'outputMode', value: 'classifier' } },
56
+ { label: 'sentiment', gate: { field: 'outputMode', value: 'sentiment' }, fixedGroups: SENTIMENT_PORTS.map((p) => ({ name: SENTIMENT_PORT_LABEL[p], port: p, summary: SENTIMENT_PORT_SUMMARY[p] })) },
57
+ ] } },
49
58
  markdown: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
50
59
  fileTransform: { fromNodes: true, toNodes: true },
51
60
  limit: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
package/dist/index.d.ts CHANGED
@@ -2,6 +2,10 @@ export type { NodeType, NodeRole, HandlePosition, NodeConnectionConfig, NodeUICo
2
2
  export { singleMeta, iterableMeta } from './types.js';
3
3
  export { NODE_CONNECTIONS, canReceiveFrom, canReceiveFromNodes, canSendToNodes, isTerminal, isNodeType, ALL_NODE_TYPES, } from './connections.js';
4
4
  export { NODE_UI, PREFIX_TO_TYPE, resolveNodeId, } from './ui.js';
5
+ export { SENTIMENT_LABELS, SENTIMENT_UNCLEAR, SENTIMENT_PORTS, SENTIMENT_PORT_LABEL, SENTIMENT_PORT_SUMMARY, esEtiquetaDeSentimiento, } from './sentiment.js';
6
+ export type { SentimentLabel, SentimentPort } from './sentiment.js';
7
+ export { outputGroupsFor, todosLosOutputGroups } from './types.js';
8
+ export type { OutputGroupsConfig } from './types.js';
5
9
  export { NODE_DISPATCH, getAllNodeCollections, getNodeDispatchConfig, pasaLoQueRecibe, } from './dispatch.js';
6
10
  export type { NodeRegistryEntry, NodeGroup, NodeVersionSpec, VersionCatalog, } from './registry.js';
7
11
  export { NODE_REGISTRY, getNodeRegistryEntry, NODE_DETAIL_PATHS, NODE_COLORS, NODE_STATE_KEYS, PREFIX_TO_NODE_TYPE, NODE_TYPE_TO_PREFIX, } from './registry.js';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DRIVE_OPERATIONS = exports.GOOGLE_CALENDAR_TOOLKIT_BY_TOOL_NAME = exports.GOOGLE_CALENDAR_TOOLKIT_SPECS = exports.isGoogleCalendarOperation = exports.GOOGLE_CALENDAR_OPERATION_SPECS = exports.GOOGLE_CALENDAR_OPERATIONS = exports.isGmailOperation = exports.resolveGmailSendFields = exports.GMAIL_SEND_LEGACY_FIELDS = exports.NATIVE_EMAIL_TOOLKIT_BY_TOOL_NAME = exports.GMAIL_TOOLKIT_BY_TOOL_NAME = exports.NATIVE_EMAIL_TOOLKIT_SPECS = exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC = exports.GMAIL_ALL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_OPERATIONS = exports.GMAIL_DROPDOWN_OPERATIONS = exports.GMAIL_OPERATION_GROUPS = exports.GMAIL_OPERATION_SPECS = exports.GMAIL_OPERATIONS = exports.versionCatalogErrors = exports.fieldsLost = exports.fieldsLostBetween = exports.currentVersion = exports.versionSpec = exports.versionsOf = exports.isVersioned = exports.NODE_TYPE_TO_PREFIX = exports.PREFIX_TO_NODE_TYPE = exports.NODE_STATE_KEYS = exports.NODE_COLORS = exports.NODE_DETAIL_PATHS = exports.getNodeRegistryEntry = exports.NODE_REGISTRY = exports.pasaLoQueRecibe = exports.getNodeDispatchConfig = exports.getAllNodeCollections = exports.NODE_DISPATCH = exports.resolveNodeId = exports.PREFIX_TO_TYPE = exports.NODE_UI = exports.ALL_NODE_TYPES = exports.isNodeType = exports.isTerminal = exports.canSendToNodes = exports.canReceiveFromNodes = exports.canReceiveFrom = exports.NODE_CONNECTIONS = exports.iterableMeta = exports.singleMeta = void 0;
4
- exports.BUCKET_ITERABLE_OPERATIONS = exports.BUCKET_OPERATION_SPECS = exports.BUCKET_OPERATIONS = exports.isJiraOperation = exports.JIRA_ITERABLE_OPERATIONS = exports.JIRA_DROPDOWN_OPERATIONS = exports.JIRA_OPERATION_SPECS = exports.JIRA_OPERATIONS = exports.isGithubOperation = exports.GITHUB_ITERABLE_OPERATIONS = exports.GITHUB_DROPDOWN_OPERATIONS = exports.GITHUB_OPERATION_SPECS = exports.GITHUB_OPERATIONS = exports.scopesQueFaltanEnShopify = exports.isShopifyOperation = exports.SHOPIFY_PRODUCT_STATUSES = exports.SHOPIFY_CANCEL_REASONS = exports.SHOPIFY_SEARCHABLE_RESOURCES = exports.SHOPIFY_TAGGABLE_RESOURCES = exports.SHOPIFY_OPERATION_SCOPES = exports.SHOPIFY_OPERATION_SPECS = exports.SHOPIFY_OPERATIONS = exports.isMailchimpOperation = exports.MAILCHIMP_CONTACT_STATUSES = exports.MAILCHIMP_OPERATION_SPECS = exports.MAILCHIMP_OPERATIONS = exports.herramientasDeDiscordPara = exports.DISCORD_TOOLKIT_BY_TOOL_NAME = exports.DISCORD_TOOLKIT_SPECS = exports.isDiscordOperation = exports.camposDeDiscordNoDisponibles = exports.discordPuedeEjecutar = exports.operacionesDeDiscordPara = exports.DISCORD_CAPACIDADES_POR_CREDENCIAL = exports.DISCORD_OPERATION_SPECS = exports.DISCORD_OPERATIONS = exports.camposNoDisponiblesPara = exports.puedeEjecutar = exports.operacionesPara = exports.isWhatsAppOperation = exports.WHATSAPP_OPERATIONS = exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = exports.isTelegramOperation = exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = exports.isDriveOperation = exports.DRIVE_OPERATION_SPECS = void 0;
5
- exports.MONGO_OPERATION_SPECS = exports.MONGO_OPERATIONS = exports.isPostgresOperation = exports.POSTGRES_OPERATION_SPECS = exports.POSTGRES_MODES = exports.POSTGRES_OPERATIONS = exports.isNotionOperation = exports.NOTION_DROPDOWN_OPERATIONS = exports.NOTION_OPERATION_SPECS = exports.NOTION_OPERATIONS = exports.isGoogleAnalyticsOperation = exports.GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS = exports.GOOGLE_ANALYTICS_OPERATION_SPECS = exports.GOOGLE_ANALYTICS_OPERATIONS = exports.isGoogleContactsOperation = exports.GOOGLE_CONTACTS_DEFAULT_PERSON_FIELDS = exports.GOOGLE_CONTACTS_OPERATION_GROUPS = exports.GOOGLE_CONTACTS_OPERATION_SPECS = exports.GOOGLE_CONTACTS_OPERATIONS_V2 = exports.GOOGLE_CONTACTS_OPERATIONS_V1 = exports.GOOGLE_CONTACTS_OPERATIONS = exports.SHEETS_TOOLKIT_DEFAULTABLE = exports.SHEETS_TOOLKIT_BY_TOOL_NAME = exports.SHEETS_TOOLKIT_SPECS = exports.isSheetsOperation = exports.SHEETS_OPERATION_SPECS = exports.SHEETS_OPERATIONS = exports.herramientasDeSlackPara = exports.SLACK_TOOLKIT_BY_TOOL_NAME = exports.SLACK_TOOLKIT_SPECS = exports.isSlackOperation = exports.camposDeSlackNoDisponibles = exports.slackPuedeEjecutar = exports.operacionesDeSlackPara = exports.SLACK_CAPACIDADES_POR_CREDENCIAL = exports.SLACK_OPERATION_SPECS = exports.SLACK_OPERATIONS = exports.isAirtableOperation = exports.AIRTABLE_ITERABLE_OPERATIONS = exports.AIRTABLE_OPERATION_SPECS = exports.AIRTABLE_OPERATIONS = exports.isCalendlyOperation = exports.CALENDLY_ITERABLE_OPERATIONS = exports.CALENDLY_OPERATION_SPECS = exports.CALENDLY_OPERATIONS = exports.isApifyOperation = exports.APIFY_ITERABLE_OPERATIONS = exports.APIFY_OPERATION_SPECS = exports.APIFY_OPERATIONS = exports.isBucketOperation = void 0;
6
- exports.armarRespuestaDelSync = exports.RESPUESTA_DEL_SYNC_POR_DEFECTO = exports.TOPE_DE_ESPERA_DEL_SYNC = exports.TOPES_DE_ESTADO = exports.SOBRES_DE_RESPUESTA = exports.MODOS_DE_PAYLOAD = exports.NIVELES_DE_DETALLE = exports.RESULTADOS_DEL_VALIDADOR = exports.contestaEnSync = exports.NODOS_QUE_CONTESTAN_EN_SYNC = exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.ventanaDeContextoDeOpenRouter = exports.opcionesDeModelosDeOpenRouter = exports.URL_DE_MODELOS_DE_OPENROUTER = exports.getModelLabel = exports.getDefaultModel = exports.getModelsFor = exports.MODEL_CONTEXT_WINDOWS = exports.LLM_MODELS = exports.LLM_PROVIDERS = exports.DOCS_TOOLKIT_DEFAULTABLE = exports.DOCS_TOOLKIT_BY_TOOL_NAME = exports.DOCS_TOOLKIT_SPECS = exports.isDocsOperation = exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = exports.isMongoOperation = void 0;
3
+ exports.GMAIL_SEND_LEGACY_FIELDS = exports.NATIVE_EMAIL_TOOLKIT_BY_TOOL_NAME = exports.GMAIL_TOOLKIT_BY_TOOL_NAME = exports.NATIVE_EMAIL_TOOLKIT_SPECS = exports.GMAIL_SEND_AND_WAIT_TOOL_SPEC = exports.GMAIL_ALL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_SPECS = exports.GMAIL_TOOLKIT_OPERATIONS = exports.GMAIL_DROPDOWN_OPERATIONS = exports.GMAIL_OPERATION_GROUPS = exports.GMAIL_OPERATION_SPECS = exports.GMAIL_OPERATIONS = exports.versionCatalogErrors = exports.fieldsLost = exports.fieldsLostBetween = exports.currentVersion = exports.versionSpec = exports.versionsOf = exports.isVersioned = exports.NODE_TYPE_TO_PREFIX = exports.PREFIX_TO_NODE_TYPE = exports.NODE_STATE_KEYS = exports.NODE_COLORS = exports.NODE_DETAIL_PATHS = exports.getNodeRegistryEntry = exports.NODE_REGISTRY = exports.pasaLoQueRecibe = exports.getNodeDispatchConfig = exports.getAllNodeCollections = exports.NODE_DISPATCH = exports.todosLosOutputGroups = exports.outputGroupsFor = exports.esEtiquetaDeSentimiento = exports.SENTIMENT_PORT_SUMMARY = exports.SENTIMENT_PORT_LABEL = exports.SENTIMENT_PORTS = exports.SENTIMENT_UNCLEAR = exports.SENTIMENT_LABELS = exports.resolveNodeId = exports.PREFIX_TO_TYPE = exports.NODE_UI = exports.ALL_NODE_TYPES = exports.isNodeType = exports.isTerminal = exports.canSendToNodes = exports.canReceiveFromNodes = exports.canReceiveFrom = exports.NODE_CONNECTIONS = exports.iterableMeta = exports.singleMeta = void 0;
4
+ exports.isGithubOperation = exports.GITHUB_ITERABLE_OPERATIONS = exports.GITHUB_DROPDOWN_OPERATIONS = exports.GITHUB_OPERATION_SPECS = exports.GITHUB_OPERATIONS = exports.scopesQueFaltanEnShopify = exports.isShopifyOperation = exports.SHOPIFY_PRODUCT_STATUSES = exports.SHOPIFY_CANCEL_REASONS = exports.SHOPIFY_SEARCHABLE_RESOURCES = exports.SHOPIFY_TAGGABLE_RESOURCES = exports.SHOPIFY_OPERATION_SCOPES = exports.SHOPIFY_OPERATION_SPECS = exports.SHOPIFY_OPERATIONS = exports.isMailchimpOperation = exports.MAILCHIMP_CONTACT_STATUSES = exports.MAILCHIMP_OPERATION_SPECS = exports.MAILCHIMP_OPERATIONS = exports.herramientasDeDiscordPara = exports.DISCORD_TOOLKIT_BY_TOOL_NAME = exports.DISCORD_TOOLKIT_SPECS = exports.isDiscordOperation = exports.camposDeDiscordNoDisponibles = exports.discordPuedeEjecutar = exports.operacionesDeDiscordPara = exports.DISCORD_CAPACIDADES_POR_CREDENCIAL = exports.DISCORD_OPERATION_SPECS = exports.DISCORD_OPERATIONS = exports.camposNoDisponiblesPara = exports.puedeEjecutar = exports.operacionesPara = exports.isWhatsAppOperation = exports.WHATSAPP_OPERATIONS = exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = exports.isTelegramOperation = exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = exports.isDriveOperation = exports.DRIVE_OPERATION_SPECS = exports.DRIVE_OPERATIONS = exports.GOOGLE_CALENDAR_TOOLKIT_BY_TOOL_NAME = exports.GOOGLE_CALENDAR_TOOLKIT_SPECS = exports.isGoogleCalendarOperation = exports.GOOGLE_CALENDAR_OPERATION_SPECS = exports.GOOGLE_CALENDAR_OPERATIONS = exports.isGmailOperation = exports.resolveGmailSendFields = void 0;
5
+ exports.NOTION_OPERATION_SPECS = exports.NOTION_OPERATIONS = exports.isGoogleAnalyticsOperation = exports.GOOGLE_ANALYTICS_DROPDOWN_OPERATIONS = exports.GOOGLE_ANALYTICS_OPERATION_SPECS = exports.GOOGLE_ANALYTICS_OPERATIONS = exports.isGoogleContactsOperation = exports.GOOGLE_CONTACTS_DEFAULT_PERSON_FIELDS = exports.GOOGLE_CONTACTS_OPERATION_GROUPS = exports.GOOGLE_CONTACTS_OPERATION_SPECS = exports.GOOGLE_CONTACTS_OPERATIONS_V2 = exports.GOOGLE_CONTACTS_OPERATIONS_V1 = exports.GOOGLE_CONTACTS_OPERATIONS = exports.SHEETS_TOOLKIT_DEFAULTABLE = exports.SHEETS_TOOLKIT_BY_TOOL_NAME = exports.SHEETS_TOOLKIT_SPECS = exports.isSheetsOperation = exports.SHEETS_OPERATION_SPECS = exports.SHEETS_OPERATIONS = exports.herramientasDeSlackPara = exports.SLACK_TOOLKIT_BY_TOOL_NAME = exports.SLACK_TOOLKIT_SPECS = exports.isSlackOperation = exports.camposDeSlackNoDisponibles = exports.slackPuedeEjecutar = exports.operacionesDeSlackPara = exports.SLACK_CAPACIDADES_POR_CREDENCIAL = exports.SLACK_OPERATION_SPECS = exports.SLACK_OPERATIONS = exports.isAirtableOperation = exports.AIRTABLE_ITERABLE_OPERATIONS = exports.AIRTABLE_OPERATION_SPECS = exports.AIRTABLE_OPERATIONS = exports.isCalendlyOperation = exports.CALENDLY_ITERABLE_OPERATIONS = exports.CALENDLY_OPERATION_SPECS = exports.CALENDLY_OPERATIONS = exports.isApifyOperation = exports.APIFY_ITERABLE_OPERATIONS = exports.APIFY_OPERATION_SPECS = exports.APIFY_OPERATIONS = exports.isBucketOperation = exports.BUCKET_ITERABLE_OPERATIONS = exports.BUCKET_OPERATION_SPECS = exports.BUCKET_OPERATIONS = exports.isJiraOperation = exports.JIRA_ITERABLE_OPERATIONS = exports.JIRA_DROPDOWN_OPERATIONS = exports.JIRA_OPERATION_SPECS = exports.JIRA_OPERATIONS = void 0;
6
+ exports.armarRespuestaDelSync = exports.RESPUESTA_DEL_SYNC_POR_DEFECTO = exports.TOPE_DE_ESPERA_DEL_SYNC = exports.TOPES_DE_ESTADO = exports.SOBRES_DE_RESPUESTA = exports.MODOS_DE_PAYLOAD = exports.NIVELES_DE_DETALLE = exports.RESULTADOS_DEL_VALIDADOR = exports.contestaEnSync = exports.NODOS_QUE_CONTESTAN_EN_SYNC = exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.ventanaDeContextoDeOpenRouter = exports.opcionesDeModelosDeOpenRouter = exports.URL_DE_MODELOS_DE_OPENROUTER = exports.getModelLabel = exports.getDefaultModel = exports.getModelsFor = exports.MODEL_CONTEXT_WINDOWS = exports.LLM_MODELS = exports.LLM_PROVIDERS = exports.DOCS_TOOLKIT_DEFAULTABLE = exports.DOCS_TOOLKIT_BY_TOOL_NAME = exports.DOCS_TOOLKIT_SPECS = exports.isDocsOperation = exports.DOCS_OPERATION_SPECS = exports.DOCS_OPERATIONS = exports.isMongoOperation = exports.MONGO_OPERATION_SPECS = exports.MONGO_OPERATIONS = exports.isPostgresOperation = exports.POSTGRES_OPERATION_SPECS = exports.POSTGRES_MODES = exports.POSTGRES_OPERATIONS = exports.isNotionOperation = exports.NOTION_DROPDOWN_OPERATIONS = void 0;
7
7
  var types_js_1 = require("./types.js");
8
8
  Object.defineProperty(exports, "singleMeta", { enumerable: true, get: function () { return types_js_1.singleMeta; } });
9
9
  Object.defineProperty(exports, "iterableMeta", { enumerable: true, get: function () { return types_js_1.iterableMeta; } });
@@ -21,6 +21,16 @@ var ui_js_1 = require("./ui.js");
21
21
  Object.defineProperty(exports, "NODE_UI", { enumerable: true, get: function () { return ui_js_1.NODE_UI; } });
22
22
  Object.defineProperty(exports, "PREFIX_TO_TYPE", { enumerable: true, get: function () { return ui_js_1.PREFIX_TO_TYPE; } });
23
23
  Object.defineProperty(exports, "resolveNodeId", { enumerable: true, get: function () { return ui_js_1.resolveNodeId; } });
24
+ var sentiment_js_1 = require("./sentiment.js");
25
+ Object.defineProperty(exports, "SENTIMENT_LABELS", { enumerable: true, get: function () { return sentiment_js_1.SENTIMENT_LABELS; } });
26
+ Object.defineProperty(exports, "SENTIMENT_UNCLEAR", { enumerable: true, get: function () { return sentiment_js_1.SENTIMENT_UNCLEAR; } });
27
+ Object.defineProperty(exports, "SENTIMENT_PORTS", { enumerable: true, get: function () { return sentiment_js_1.SENTIMENT_PORTS; } });
28
+ Object.defineProperty(exports, "SENTIMENT_PORT_LABEL", { enumerable: true, get: function () { return sentiment_js_1.SENTIMENT_PORT_LABEL; } });
29
+ Object.defineProperty(exports, "SENTIMENT_PORT_SUMMARY", { enumerable: true, get: function () { return sentiment_js_1.SENTIMENT_PORT_SUMMARY; } });
30
+ Object.defineProperty(exports, "esEtiquetaDeSentimiento", { enumerable: true, get: function () { return sentiment_js_1.esEtiquetaDeSentimiento; } });
31
+ var types_js_2 = require("./types.js");
32
+ Object.defineProperty(exports, "outputGroupsFor", { enumerable: true, get: function () { return types_js_2.outputGroupsFor; } });
33
+ Object.defineProperty(exports, "todosLosOutputGroups", { enumerable: true, get: function () { return types_js_2.todosLosOutputGroups; } });
24
34
  // ── Dispatch config ──
25
35
  /* Ya no es «API only»: `downstreamPayload` lo lee tambien el panel, que sin el
26
36
  ensenaba como entrada de un nodo la salida del que pasa de largo. */
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Sentiment analysis en el AI Node — las etiquetas, los puertos y el orden.
3
+ *
4
+ * ## Por qué vive aquí y no en cada repo
5
+ *
6
+ * Las tres etiquetas son a la vez TRES cosas: el `enum` del esquema que se le
7
+ * da al modelo, el nombre de los puertos por los que el nodo despacha, y las
8
+ * pastillas de la tarjeta del lienzo. Si el api, hw-nodes y el dashboard
9
+ * escriben su propia lista, la primera que alguien reordene o rebautice deja
10
+ * una arista apuntando a un puerto que ya no existe — y eso no lo dice ningún
11
+ * compilador. Es el mismo motivo por el que las operaciones de cada nodo
12
+ * viven en este paquete.
13
+ *
14
+ * ## Por qué `unclear` no es una etiqueta más
15
+ *
16
+ * El modelo elige entre las TRES de `SENTIMENT_LABELS`; `unclear` no se le
17
+ * ofrece nunca. Es lo que hace el NODO cuando la confianza queda por debajo
18
+ * del umbral (`sentimentThreshold`) o cuando la respuesta no se puede leer.
19
+ * Ofrecérsela al modelo sería darle una salida cómoda para no mojarse, que es
20
+ * justo lo contrario de lo que se busca al poner un umbral.
21
+ *
22
+ * A diferencia del `other` del clasificador, `unclear` SÍ tiene puerto: el
23
+ * caso de uso es mandar a revisión humana lo que el modelo no ve claro, y sin
24
+ * puerto eso obligaba a adivinar por qué un item no salió por ninguna rama.
25
+ */
26
+ /** Lo que el modelo puede contestar, en orden de peor a mejor. */
27
+ export declare const SENTIMENT_LABELS: readonly ["negative", "neutral", "positive"];
28
+ export type SentimentLabel = (typeof SENTIMENT_LABELS)[number];
29
+ /** La salida del nodo cuando no hay etiqueta fiable. No se le ofrece al modelo. */
30
+ export declare const SENTIMENT_UNCLEAR = "unclear";
31
+ /** Los cuatro puertos del modo sentiment, en el orden en que se pintan. */
32
+ export declare const SENTIMENT_PORTS: readonly ["negative", "neutral", "positive", "unclear"];
33
+ export type SentimentPort = (typeof SENTIMENT_PORTS)[number];
34
+ /** El nombre que ve la persona, por puerto. */
35
+ export declare const SENTIMENT_PORT_LABEL: Record<SentimentPort, string>;
36
+ /** Qué significa cada puerto, para el modal de conexión y la doc. */
37
+ export declare const SENTIMENT_PORT_SUMMARY: Record<SentimentPort, string>;
38
+ /** ¿Es una de las tres que el modelo puede contestar? */
39
+ export declare function esEtiquetaDeSentimiento(v: unknown): v is SentimentLabel;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * Sentiment analysis en el AI Node — las etiquetas, los puertos y el orden.
4
+ *
5
+ * ## Por qué vive aquí y no en cada repo
6
+ *
7
+ * Las tres etiquetas son a la vez TRES cosas: el `enum` del esquema que se le
8
+ * da al modelo, el nombre de los puertos por los que el nodo despacha, y las
9
+ * pastillas de la tarjeta del lienzo. Si el api, hw-nodes y el dashboard
10
+ * escriben su propia lista, la primera que alguien reordene o rebautice deja
11
+ * una arista apuntando a un puerto que ya no existe — y eso no lo dice ningún
12
+ * compilador. Es el mismo motivo por el que las operaciones de cada nodo
13
+ * viven en este paquete.
14
+ *
15
+ * ## Por qué `unclear` no es una etiqueta más
16
+ *
17
+ * El modelo elige entre las TRES de `SENTIMENT_LABELS`; `unclear` no se le
18
+ * ofrece nunca. Es lo que hace el NODO cuando la confianza queda por debajo
19
+ * del umbral (`sentimentThreshold`) o cuando la respuesta no se puede leer.
20
+ * Ofrecérsela al modelo sería darle una salida cómoda para no mojarse, que es
21
+ * justo lo contrario de lo que se busca al poner un umbral.
22
+ *
23
+ * A diferencia del `other` del clasificador, `unclear` SÍ tiene puerto: el
24
+ * caso de uso es mandar a revisión humana lo que el modelo no ve claro, y sin
25
+ * puerto eso obligaba a adivinar por qué un item no salió por ninguna rama.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.SENTIMENT_PORT_SUMMARY = exports.SENTIMENT_PORT_LABEL = exports.SENTIMENT_PORTS = exports.SENTIMENT_UNCLEAR = exports.SENTIMENT_LABELS = void 0;
29
+ exports.esEtiquetaDeSentimiento = esEtiquetaDeSentimiento;
30
+ /** Lo que el modelo puede contestar, en orden de peor a mejor. */
31
+ exports.SENTIMENT_LABELS = ['negative', 'neutral', 'positive'];
32
+ /** La salida del nodo cuando no hay etiqueta fiable. No se le ofrece al modelo. */
33
+ exports.SENTIMENT_UNCLEAR = 'unclear';
34
+ /** Los cuatro puertos del modo sentiment, en el orden en que se pintan. */
35
+ exports.SENTIMENT_PORTS = [
36
+ ...exports.SENTIMENT_LABELS,
37
+ exports.SENTIMENT_UNCLEAR,
38
+ ];
39
+ /** El nombre que ve la persona, por puerto. */
40
+ exports.SENTIMENT_PORT_LABEL = {
41
+ negative: 'Negative',
42
+ neutral: 'Neutral',
43
+ positive: 'Positive',
44
+ unclear: 'Unclear',
45
+ };
46
+ /** Qué significa cada puerto, para el modal de conexión y la doc. */
47
+ exports.SENTIMENT_PORT_SUMMARY = {
48
+ negative: 'Complaints, frustration, criticism',
49
+ neutral: 'Factual, mixed, or no clear feeling',
50
+ positive: 'Praise, satisfaction, enthusiasm',
51
+ unclear: 'Below the confidence floor, or unreadable',
52
+ };
53
+ /** ¿Es una de las tres que el modelo puede contestar? */
54
+ function esEtiquetaDeSentimiento(v) {
55
+ return (typeof v === 'string' &&
56
+ exports.SENTIMENT_LABELS.includes(v));
57
+ }
package/dist/types.d.ts CHANGED
@@ -54,83 +54,15 @@ export interface NodeUIConfig {
54
54
  connectModal?: 'branch' | 'output-group';
55
55
  routerTargetLabel?: string;
56
56
  /**
57
- * Generic named output groups — used by connectModal: 'output-group'.
58
- * The modal reads entity[outputGroupsField] as an array of groups,
59
- * each with a name (outputGroupNameField), a summary (outputGroupSummaryField),
60
- * and its own outputNodes[].
61
- * Works for Split (outputs[]), Conditional (branches[]), or any future branching node.
57
+ * Los juegos de salidas de un nodo que ramifica: UNO, o VARIOS con su
58
+ * `gate` cada uno [2026-09-22]. Con varios, quien pregunta se queda con
59
+ * el PRIMERO cuya puerta pasa — ver `outputGroupsFor`. El AI Node los
60
+ * necesita porque ramifica de dos maneras según el modo: por las
61
+ * categorías que escribe el usuario (`classifier`) o por los cuatro
62
+ * puertos fijos del análisis de sentimiento (`sentiment`). La forma
63
+ * vieja (un objeto suelto) sigue valiendo y significa «sin alternativas».
62
64
  */
63
- outputGroups?: {
64
- /** Entity field containing the array of groups (e.g. 'outputs', 'branches') */
65
- field?: string;
66
- /** Field within each group for display name (e.g. 'name') */
67
- nameField?: string;
68
- /** Field within each group for summary/description (e.g. 'field', 'filterMode') */
69
- summaryField?: string;
70
- /**
71
- * Varios campos del grupo, unidos por espacios, cuando el resumen que
72
- * de verdad identifica al grupo no cabe en uno solo.
73
- *
74
- * Existe por las reglas del router: su condición son TRES campos
75
- * (`field`, `operator`, `value`) y el modal a medida que se retiró las
76
- * pintaba juntas — «status eq active». Con un único `summaryField` una
77
- * regla sin `label` quedaba en «rule 1» y «status», que no dice a qué
78
- * se está conectando uno.
79
- *
80
- * Los campos vacíos se saltan, para que una regla con operador
81
- * `exists` no salga con un espacio colgando al final.
82
- */
83
- summaryFields?: string[];
84
- /** Label shown in the modal (e.g. 'output', 'branch') */
85
- label: string;
86
- /** Entity field for the fallback/else path (e.g. 'elseOutputNodes') */
87
- elseField?: string;
88
- /**
89
- * Fixed output groups — for nodes with a static set of outputs (e.g. approval: approve/reject).
90
- * Each entry maps to a top-level entity field that holds outputNodes[].
91
- * When set, `field`/`nameField` are ignored — modal shows these fixed options.
92
- */
93
- fixedGroups?: Array<{
94
- name: string;
95
- field: string;
96
- }>;
97
- /**
98
- * Optional content-aware gate — the output-group modal only opens
99
- * when `entity[gate.field] === gate.value`. Lets a node opt into
100
- * multi-branch routing only in some modes (e.g. AI Node in
101
- * classifier mode) without trapping every connection in the
102
- * picker the rest of the time. When the gate fails, the
103
- * FlowCanvas falls through to the universal
104
- * `entity.outputNodes` connect path.
105
- */
106
- gate?: {
107
- field: string;
108
- value: unknown;
109
- };
110
- /**
111
- * Qué es el puerto `main` de este nodo MIENTRAS su contenedor está
112
- * vacío. Sin esto, conectar a un nodo que ramifica y todavía no tiene
113
- * ninguna salida escribía una arista en `main` **en silencio**, y el
114
- * usuario no tenía forma de saber qué había pasado con ella.
115
- *
116
- * No hay una respuesta única, y por eso es un dato y no una regla.
117
- * Medido en `api/src/common/pipeline-run.service.ts`:
118
- *
119
- * 'passthrough' — router sin reglas: `rules.length === 0
120
- * ? refsForPort(entity, MAIN_PORT) : []`. La
121
- * arista SÍ dispara... hasta que se cree la
122
- * primera regla, y entonces deja de hacerlo
123
- * sin avisar.
124
- * 'never-dispatched' — split: no tiene salida `main`, y el propio
125
- * despachador registra un warning por cada
126
- * arista que se quedó en ese puerto.
127
- *
128
- * Se declara sólo donde se ha medido. Sin declarar, el lienzo se
129
- * comporta como siempre: conecta por el camino universal y no dice
130
- * nada, que es lo que hacía antes de existir este campo.
131
- */
132
- emptyContainerMain?: 'passthrough' | 'never-dispatched';
133
- };
65
+ outputGroups?: OutputGroupsConfig | OutputGroupsConfig[];
134
66
  /**
135
67
  * Maps output fields to alternate payload fields on the source entity.
136
68
  * When a downstream node is connected via a specific output field,
@@ -224,3 +156,106 @@ export interface PrefixMapping {
224
156
  /** Canvas node type used in React Flow nodeTypes map (e.g. "cache-node", "email-action") */
225
157
  canvasType: string;
226
158
  }
159
+ /**
160
+ * Un juego de salidas de un nodo que ramifica.
161
+ *
162
+ * Vivía dentro de `NodeUIConfig.special` como un objeto anónimo; sale aquí
163
+ * [2026-09-22] para poder declarar varios por nodo y para que quien los lea
164
+ * nombre el tipo en vez de repetir su forma.
165
+ */
166
+ export interface OutputGroupsConfig {
167
+ /** Entity field containing the array of groups (e.g. 'outputs', 'branches') */
168
+ field?: string;
169
+ /** Field within each group for display name (e.g. 'name') */
170
+ nameField?: string;
171
+ /** Field within each group for summary/description (e.g. 'field', 'filterMode') */
172
+ summaryField?: string;
173
+ /**
174
+ * Varios campos del grupo, unidos por espacios, cuando el resumen que
175
+ * de verdad identifica al grupo no cabe en uno solo.
176
+ *
177
+ * Existe por las reglas del router: su condición son TRES campos
178
+ * (`field`, `operator`, `value`) y el modal a medida que se retiró las
179
+ * pintaba juntas — «status eq active». Con un único `summaryField` una
180
+ * regla sin `label` quedaba en «rule 1» y «status», que no dice a qué
181
+ * se está conectando uno.
182
+ *
183
+ * Los campos vacíos se saltan, para que una regla con operador
184
+ * `exists` no salga con un espacio colgando al final.
185
+ */
186
+ summaryFields?: string[];
187
+ /** Label shown in the modal (e.g. 'output', 'branch') */
188
+ label: string;
189
+ /** Entity field for the fallback/else path (e.g. 'elseOutputNodes') */
190
+ elseField?: string;
191
+ /**
192
+ * Fixed output groups — for nodes with a static set of outputs (e.g. approval: approve/reject).
193
+ * When set, `field`/`nameField` are ignored — modal shows these fixed options.
194
+ *
195
+ * Cada entrada nombra su puerto de una de dos formas, y sólo una:
196
+ * · `field` — un campo de primer nivel de los de ANTES de que los puertos
197
+ * existieran (`rejectionOutputNodes` → puerto `rejected`); se traduce
198
+ * con el mapa de campos heredados.
199
+ * · `port` — el puerto, tal cual. Es lo que usan las salidas nacidas ya
200
+ * con puertos (las cuatro de sentiment): inventarles un campo heredado
201
+ * que nunca existió sería mentir en el registro.
202
+ */
203
+ fixedGroups?: Array<{
204
+ name: string;
205
+ field?: string;
206
+ port?: string;
207
+ /** Una línea de qué entrega ese puerto, para el modal de conexión. */
208
+ summary?: string;
209
+ }>;
210
+ /**
211
+ * Optional content-aware gate — the output-group modal only opens
212
+ * when `entity[gate.field] === gate.value`. Lets a node opt into
213
+ * multi-branch routing only in some modes (e.g. AI Node in
214
+ * classifier mode) without trapping every connection in the
215
+ * picker the rest of the time. When the gate fails, the
216
+ * FlowCanvas falls through to the universal
217
+ * `entity.outputNodes` connect path.
218
+ */
219
+ gate?: {
220
+ field: string;
221
+ value: unknown;
222
+ };
223
+ /**
224
+ * Qué es el puerto `main` de este nodo MIENTRAS su contenedor está
225
+ * vacío. Sin esto, conectar a un nodo que ramifica y todavía no tiene
226
+ * ninguna salida escribía una arista en `main` **en silencio**, y el
227
+ * usuario no tenía forma de saber qué había pasado con ella.
228
+ *
229
+ * No hay una respuesta única, y por eso es un dato y no una regla.
230
+ * Medido en `api/src/common/pipeline-run.service.ts`:
231
+ *
232
+ * 'passthrough' — router sin reglas: `rules.length === 0
233
+ * ? refsForPort(entity, MAIN_PORT) : []`. La
234
+ * arista SÍ dispara... hasta que se cree la
235
+ * primera regla, y entonces deja de hacerlo
236
+ * sin avisar.
237
+ * 'never-dispatched' — split: no tiene salida `main`, y el propio
238
+ * despachador registra un warning por cada
239
+ * arista que se quedó en ese puerto.
240
+ *
241
+ * Se declara sólo donde se ha medido. Sin declarar, el lienzo se
242
+ * comporta como siempre: conecta por el camino universal y no dice
243
+ * nada, que es lo que hacía antes de existir este campo.
244
+ */
245
+ emptyContainerMain?: 'passthrough' | 'never-dispatched';
246
+ }
247
+ /**
248
+ * El juego de salidas que aplica AHORA a esta entidad, o `undefined` si el
249
+ * nodo no ramifica en este momento.
250
+ *
251
+ * Es la única lectura de `special.outputGroups` que entiende las dos formas
252
+ * (uno o varios) y las dos puertas (con `gate` o sin ella). Todo el que
253
+ * pregunte tiene que pasar por aquí: el lienzo, el modal de conexión y el
254
+ * despachador. Escribirlo cada uno a su manera es como el router acabó con un
255
+ * camino propio que nadie arregló al arreglar el de los demás.
256
+ */
257
+ export declare function outputGroupsFor(config: NodeUIConfig | undefined | null, entity: Record<string, unknown> | undefined | null): OutputGroupsConfig | undefined;
258
+ /** Todos los juegos declarados, pasen o no su puerta. Para quien necesita el
259
+ * UNIVERSO de puertos que un tipo puede emitir (la auditoría de puertos),
260
+ * no el de ahora mismo. */
261
+ export declare function todosLosOutputGroups(config: NodeUIConfig | undefined | null): OutputGroupsConfig[];
package/dist/types.js CHANGED
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.singleMeta = singleMeta;
4
4
  exports.iterableMeta = iterableMeta;
5
+ exports.outputGroupsFor = outputGroupsFor;
6
+ exports.todosLosOutputGroups = todosLosOutputGroups;
5
7
  /** Helper to create _meta for single-object outputs */
6
8
  function singleMeta() {
7
9
  return { iterable: false, count: 1 };
@@ -10,3 +12,29 @@ function singleMeta() {
10
12
  function iterableMeta(field, count) {
11
13
  return { iterable: true, iterateField: field, count };
12
14
  }
15
+ /**
16
+ * El juego de salidas que aplica AHORA a esta entidad, o `undefined` si el
17
+ * nodo no ramifica en este momento.
18
+ *
19
+ * Es la única lectura de `special.outputGroups` que entiende las dos formas
20
+ * (uno o varios) y las dos puertas (con `gate` o sin ella). Todo el que
21
+ * pregunte tiene que pasar por aquí: el lienzo, el modal de conexión y el
22
+ * despachador. Escribirlo cada uno a su manera es como el router acabó con un
23
+ * camino propio que nadie arregló al arreglar el de los demás.
24
+ */
25
+ function outputGroupsFor(config, entity) {
26
+ const declarado = config?.special?.outputGroups;
27
+ if (!declarado)
28
+ return undefined;
29
+ const lista = Array.isArray(declarado) ? declarado : [declarado];
30
+ return lista.find((c) => !c.gate || entity?.[c.gate.field] === c.gate.value);
31
+ }
32
+ /** Todos los juegos declarados, pasen o no su puerta. Para quien necesita el
33
+ * UNIVERSO de puertos que un tipo puede emitir (la auditoría de puertos),
34
+ * no el de ahora mismo. */
35
+ function todosLosOutputGroups(config) {
36
+ const declarado = config?.special?.outputGroups;
37
+ if (!declarado)
38
+ return [];
39
+ return Array.isArray(declarado) ? declarado : [declarado];
40
+ }
package/dist/ui.js CHANGED
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.PREFIX_TO_TYPE = exports.NODE_UI = void 0;
8
8
  exports.resolveNodeId = resolveNodeId;
9
+ const sentiment_js_1 = require("./sentiment.js");
9
10
  exports.NODE_UI = {
10
11
  // ── Source nodes ──
11
12
  // Una fuente arranca el flujo: `fromNodes: false` significa que nada puede
@@ -49,7 +50,15 @@ exports.NODE_UI = {
49
50
  // no handle and no route. In any other outputMode the node falls back to
50
51
  // the single right-out handle NodeShell renders when no dynamic outputs
51
52
  // materialize.
52
- ai: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { loopBackAllowed: true, connectModal: 'output-group', outputGroups: { field: 'categories', nameField: 'name', summaryField: 'description', label: 'category', gate: { field: 'outputMode', value: 'classifier' } } } },
53
+ // En modo `sentiment` (2026-09-22) el abanico son CUATRO puertos fijos
54
+ // —negative / neutral / positive / unclear— y no un contenedor del usuario:
55
+ // el modelo elige entre las tres primeras y el nodo manda a `unclear` lo que
56
+ // no llega al umbral de confianza. Por eso el AI Node declara DOS juegos de
57
+ // salidas, uno por modo; `outputGroupsFor` elige el que toca.
58
+ ai: { fromNodes: true, toNodes: true, outputHandles: ['right'], special: { loopBackAllowed: true, connectModal: 'output-group', outputGroups: [
59
+ { field: 'categories', nameField: 'name', summaryField: 'description', label: 'category', gate: { field: 'outputMode', value: 'classifier' } },
60
+ { label: 'sentiment', gate: { field: 'outputMode', value: 'sentiment' }, fixedGroups: sentiment_js_1.SENTIMENT_PORTS.map((p) => ({ name: sentiment_js_1.SENTIMENT_PORT_LABEL[p], port: p, summary: sentiment_js_1.SENTIMENT_PORT_SUMMARY[p] })) },
61
+ ] } },
53
62
  markdown: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
54
63
  fileTransform: { fromNodes: true, toNodes: true },
55
64
  limit: { fromNodes: true, toNodes: true, special: { loopBackAllowed: true } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/node-types",
3
- "version": "1.84.0",
3
+ "version": "1.86.0",
4
4
  "description": "Shared node type definitions, connection rules, and dispatch config for HostWebhook",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",