@octaviaflow/flow-rules 0.1.0 → 0.2.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.
Files changed (74) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.js +3010 -28
  3. package/package.json +14 -5
  4. package/dist/catalog/actions/console-log/config.js +0 -70
  5. package/dist/catalog/actions/console-log/index.js +0 -89
  6. package/dist/catalog/actions/delay/config.js +0 -79
  7. package/dist/catalog/actions/delay/index.js +0 -71
  8. package/dist/catalog/actions/delay/migrate.js +0 -38
  9. package/dist/catalog/actions/delay/types.js +0 -17
  10. package/dist/catalog/actions/http-request/config.js +0 -111
  11. package/dist/catalog/actions/http-request/index.js +0 -223
  12. package/dist/catalog/ai/ai-agent/index.js +0 -72
  13. package/dist/catalog/ai/idp-extract/index.js +0 -90
  14. package/dist/catalog/api/graphql-query/config.js +0 -31
  15. package/dist/catalog/api/graphql-query/index.js +0 -75
  16. package/dist/catalog/communication/email-send/config.js +0 -80
  17. package/dist/catalog/communication/email-send/index.js +0 -152
  18. package/dist/catalog/communication/google-chat/config.js +0 -28
  19. package/dist/catalog/communication/google-chat/index.js +0 -36
  20. package/dist/catalog/communication/slack-message/config.js +0 -36
  21. package/dist/catalog/communication/slack-message/index.js +0 -37
  22. package/dist/catalog/communication/teams-message/config.js +0 -29
  23. package/dist/catalog/communication/teams-message/index.js +0 -39
  24. package/dist/catalog/communication/webhook-call/config.js +0 -31
  25. package/dist/catalog/communication/webhook-call/index.js +0 -43
  26. package/dist/catalog/communication/whatsapp-send/config.js +0 -36
  27. package/dist/catalog/communication/whatsapp-send/index.js +0 -57
  28. package/dist/catalog/general/query/config.js +0 -257
  29. package/dist/catalog/general/query/index.js +0 -271
  30. package/dist/catalog/general/query/types.js +0 -82
  31. package/dist/catalog/general/transform/config.js +0 -95
  32. package/dist/catalog/general/transform/index.js +0 -63
  33. package/dist/catalog/index.js +0 -168
  34. package/dist/catalog/logic/condition/config.js +0 -93
  35. package/dist/catalog/logic/condition/index.js +0 -64
  36. package/dist/catalog/logic/for-each/config.js +0 -75
  37. package/dist/catalog/logic/for-each/index.js +0 -92
  38. package/dist/catalog/logic/for-each/pathOptions.js +0 -151
  39. package/dist/catalog/logic/group/index.js +0 -46
  40. package/dist/catalog/policies.js +0 -62
  41. package/dist/catalog/storage/csv-action/index.js +0 -38
  42. package/dist/catalog/storage/dropbox-action/index.js +0 -33
  43. package/dist/catalog/storage/excel-action/index.js +0 -32
  44. package/dist/catalog/storage/file-action/index.js +0 -119
  45. package/dist/catalog/storage/file-operation/config.js +0 -45
  46. package/dist/catalog/storage/file-operation/index.js +0 -111
  47. package/dist/catalog/storage/ftp-action/index.js +0 -33
  48. package/dist/catalog/storage/google-drive-action/index.js +0 -38
  49. package/dist/catalog/storage/json-action/index.js +0 -31
  50. package/dist/catalog/storage/onedrive-action/index.js +0 -33
  51. package/dist/catalog/storage/s3-action/index.js +0 -36
  52. package/dist/catalog/storage/storage-action/index.js +0 -72
  53. package/dist/catalog/storage/storageCapabilities.js +0 -207
  54. package/dist/catalog/storage/xml-action/index.js +0 -31
  55. package/dist/catalog/transforms/format/config.js +0 -19
  56. package/dist/catalog/transforms/format/index.js +0 -39
  57. package/dist/catalog/triggers/connector-trigger/index.js +0 -72
  58. package/dist/catalog/triggers/manual-trigger/config.js +0 -60
  59. package/dist/catalog/triggers/manual-trigger/index.js +0 -74
  60. package/dist/catalog/triggers/schedule-trigger/config.js +0 -42
  61. package/dist/catalog/triggers/schedule-trigger/index.js +0 -102
  62. package/dist/catalog/triggers/webhook-trigger/config.js +0 -27
  63. package/dist/catalog/triggers/webhook-trigger/index.js +0 -89
  64. package/dist/catalog/types.js +0 -135
  65. package/dist/catalog/utility/debug/config.js +0 -90
  66. package/dist/catalog/utility/debug/index.js +0 -38
  67. package/dist/catalog/utility/error/config.js +0 -25
  68. package/dist/catalog/utility/error/index.js +0 -46
  69. package/dist/catalog/utility/variables/config.js +0 -70
  70. package/dist/catalog/utility/variables/index.js +0 -40
  71. package/dist/catalog/workflow.js +0 -25
  72. package/dist/ids.js +0 -53
  73. package/dist/rules/nodeConfigStatus.js +0 -217
  74. package/dist/rules/workflowValidator.js +0 -358
@@ -1,358 +0,0 @@
1
- /**
2
- * Workflow Validation System — rules engine v2 (DEV-220)
3
- *
4
- * Validates nodes, edges, and overall workflow structure on every
5
- * STRUCTURAL canvas change (WorkflowContext applies it selectively —
6
- * selection/viewport/drag churn never re-runs it). Results feed:
7
- * - the FlowToolbarIssues indicator (badge + hover overlay)
8
- * - per-node error/warning badges (flowAdapter → DS BaseNode status)
9
- * - activation gating (useFlowLifecycle: errors block Activate;
10
- * Save is NEVER blocked; warnings never block anything)
11
- *
12
- * Severity model (ADR-flow-pause-and-rules-v2):
13
- * error → the flow cannot work as built (missing trigger, broken edge,
14
- * cycle, missing required config). Blocks Activate.
15
- * warning → it will run, but probably not the way the user thinks
16
- * ("won't run" orphans, multi-flow canvas, duplicate edges).
17
- */
18
- import { getNodeConfigStatus } from './nodeConfigStatus';
19
- const nodeName = (n) => n.label || n.id;
20
- /**
21
- * Validate a single node.
22
- */
23
- export function validateNode(node, allNodes, allEdges) {
24
- const errors = [];
25
- // Check required fields
26
- if (!node.id || node.id.trim().length === 0) {
27
- errors.push({
28
- id: `node-${node.id}-id`,
29
- type: 'node',
30
- severity: 'error',
31
- message: 'Node ID is required',
32
- nodeId: node.id,
33
- });
34
- }
35
- if (!node.label || node.label.trim().length === 0) {
36
- errors.push({
37
- id: `node-${node.id}-label`,
38
- type: 'node',
39
- severity: 'error',
40
- message: 'Node label is required',
41
- nodeId: node.id,
42
- });
43
- }
44
- // Config completeness — computed LIVE from the action definition's
45
- // required fields (the persisted `isValid` flag goes stale between the
46
- // loader and the current edit). Missing required fields are an ERROR and
47
- // are named so the issues list is actionable.
48
- const cfg = getNodeConfigStatus(node);
49
- if (cfg.missingRequired.length > 0) {
50
- const fields = cfg.missingRequired.slice(0, 4).join(', ');
51
- const more = cfg.missingRequired.length > 4 ? ` +${cfg.missingRequired.length - 4} more` : '';
52
- errors.push({
53
- id: `node-${node.id}-config`,
54
- type: 'node',
55
- severity: 'error',
56
- // Actionable for END USERS: name the exact fields AND the fix action.
57
- message: `Missing required settings: ${fields}${more}. Open the step and fill them in.`,
58
- nodeId: node.id,
59
- });
60
- }
61
- // Trigger nodes must have at least one outgoing connection to be useful
62
- const hasOutput = allEdges.some(edge => edge.source === node.id);
63
- if (!hasOutput && node.type === 'trigger') {
64
- errors.push({
65
- id: `node-${node.id}-output`,
66
- type: 'node',
67
- severity: 'warning',
68
- message: `Trigger "${nodeName(node)}" has no outgoing connections`,
69
- nodeId: node.id,
70
- });
71
- }
72
- return errors;
73
- }
74
- /**
75
- * Validate a single edge. `allEdges` powers real duplicate detection —
76
- * only the SECOND-and-later instance of a duplicate is flagged, so one
77
- * duplicate pair yields one warning, not two.
78
- */
79
- export function validateEdge(edge, allNodes, allEdges = []) {
80
- const errors = [];
81
- // Check if source node exists
82
- const sourceNode = allNodes.find(n => n.id === edge.source);
83
- if (!sourceNode) {
84
- errors.push({
85
- id: `edge-${edge.id}-source`,
86
- type: 'edge',
87
- severity: 'error',
88
- message: `Source node "${edge.source}" does not exist`,
89
- edgeId: edge.id,
90
- });
91
- }
92
- // Check if target node exists
93
- const targetNode = allNodes.find(n => n.id === edge.target);
94
- if (!targetNode) {
95
- errors.push({
96
- id: `edge-${edge.id}-target`,
97
- type: 'edge',
98
- severity: 'error',
99
- message: `Target node "${edge.target}" does not exist`,
100
- edgeId: edge.id,
101
- });
102
- }
103
- // Check for self-loops
104
- if (edge.source === edge.target) {
105
- errors.push({
106
- id: `edge-${edge.id}-loop`,
107
- type: 'edge',
108
- severity: 'error',
109
- message: 'Nodes cannot connect to themselves',
110
- edgeId: edge.id,
111
- });
112
- }
113
- // Duplicate edges: same source/target (and same handles when present).
114
- // (v1 shipped with `duplicateCount = 1` hard-coded — the rule could
115
- // never fire. DEV-220 fixed it.)
116
- const key = (e) => `${e.source}|${e.sourceHandle ?? ''}→${e.target}|${e.targetHandle ?? ''}`;
117
- const myKey = key(edge);
118
- const firstIndex = allEdges.findIndex(e => key(e) === myKey);
119
- const myIndex = allEdges.findIndex(e => e.id === edge.id);
120
- if (firstIndex !== -1 && myIndex > firstIndex) {
121
- errors.push({
122
- id: `edge-${edge.id}-duplicate`,
123
- type: 'edge',
124
- severity: 'warning',
125
- message: `Duplicate connection between "${sourceNode ? nodeName(sourceNode) : edge.source}" and "${targetNode ? nodeName(targetNode) : edge.target}"`,
126
- edgeId: edge.id,
127
- });
128
- }
129
- return errors;
130
- }
131
- /**
132
- * Validate entire workflow.
133
- */
134
- export function validateWorkflow(nodes, edges) {
135
- const allErrors = [];
136
- // Check for at least one trigger node
137
- const hasTrigger = nodes.some(node => node.type === 'trigger');
138
- if (!hasTrigger) {
139
- allErrors.push({
140
- id: 'workflow-trigger',
141
- type: 'workflow',
142
- severity: 'error',
143
- message: 'Workflow must have at least one trigger node',
144
- });
145
- }
146
- // Check for at least one action node
147
- const hasAction = nodes.some(node => node.type === 'action');
148
- if (!hasAction && nodes.length > 0) {
149
- allErrors.push({
150
- id: 'workflow-action',
151
- type: 'workflow',
152
- severity: 'warning',
153
- message: 'Workflow should have at least one action node',
154
- });
155
- }
156
- // Validate individual nodes
157
- nodes.forEach(node => {
158
- const nodeErrors = validateNode(node, nodes, edges);
159
- allErrors.push(...nodeErrors);
160
- });
161
- // Validate individual edges
162
- edges.forEach(edge => {
163
- const edgeErrors = validateEdge(edge, nodes, edges);
164
- allErrors.push(...edgeErrors);
165
- });
166
- // ── Cycle detection (DEV-220) ──
167
- // Kahn's algorithm over the execution edges; anything the topological
168
- // sort can't reach sits on (or strictly behind) a cycle. The Engine
169
- // APPENDS such steps to the order silently, so a cycle produces
170
- // undefined ordering at run time — an error here, not a curiosity.
171
- if (nodes.length > 0 && edges.length > 0) {
172
- const inDegree = new Map(nodes.map(n => [n.id, 0]));
173
- const out = new Map();
174
- for (const e of edges) {
175
- if (e.source === e.target)
176
- continue; // self-loops flagged per-edge already
177
- if (!inDegree.has(e.source) || !inDegree.has(e.target))
178
- continue; // dangling flagged already
179
- inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
180
- const list = out.get(e.source);
181
- if (list)
182
- list.push(e.target);
183
- else
184
- out.set(e.source, [e.target]);
185
- }
186
- const queue = nodes.filter(n => (inDegree.get(n.id) ?? 0) === 0).map(n => n.id);
187
- let visited = 0;
188
- while (queue.length) {
189
- const cur = queue.shift();
190
- visited++;
191
- for (const next of out.get(cur) ?? []) {
192
- const d = (inDegree.get(next) ?? 1) - 1;
193
- inDegree.set(next, d);
194
- if (d === 0)
195
- queue.push(next);
196
- }
197
- }
198
- if (visited < nodes.length) {
199
- const stuck = nodes.filter(n => (inDegree.get(n.id) ?? 0) > 0);
200
- const names = stuck.slice(0, 3).map(nodeName).join(', ');
201
- const more = stuck.length > 3 ? ` +${stuck.length - 3} more` : '';
202
- allErrors.push({
203
- id: 'workflow-cycle',
204
- type: 'workflow',
205
- severity: 'error',
206
- message: `Circular connection detected involving: ${names}${more}. Break the loop — execution order is undefined inside a cycle.`,
207
- nodeId: stuck[0]?.id,
208
- });
209
- }
210
- }
211
- // ── DEV-207: "won't run" warnings ──
212
- // Execution is reachability-from-entry: a node runs only if it is reachable,
213
- // following edges, from a trigger. Flag configured nodes that WON'T run so
214
- // the consequence is explicit (a stray, unconnected Slack step silently not
215
- // firing is the exact footgun this prevents).
216
- const isTrigger = (n) => n.type === 'trigger' || String(n.actionDefinitionId ?? '').endsWith('_trigger');
217
- const triggerIds = nodes.filter(isTrigger).map(n => n.id);
218
- // Forward reachability from every trigger, closed over parent/child so a
219
- // container's body counts as reachable when the container is (mirrors the
220
- // Engine's executionScope).
221
- const adjacency = new Map();
222
- for (const e of edges) {
223
- const list = adjacency.get(e.source);
224
- if (list)
225
- list.push(e.target);
226
- else
227
- adjacency.set(e.source, [e.target]);
228
- }
229
- const reachable = new Set(triggerIds);
230
- const queue = [...triggerIds];
231
- while (queue.length) {
232
- const cur = queue.shift();
233
- for (const next of adjacency.get(cur) ?? []) {
234
- if (!reachable.has(next)) {
235
- reachable.add(next);
236
- queue.push(next);
237
- }
238
- }
239
- }
240
- let changed = true;
241
- while (changed) {
242
- changed = false;
243
- for (const n of nodes) {
244
- if (reachable.has(n.id)) {
245
- if (n.parentId && !reachable.has(n.parentId)) {
246
- reachable.add(n.parentId);
247
- changed = true;
248
- }
249
- }
250
- else if (n.parentId && reachable.has(n.parentId)) {
251
- reachable.add(n.id);
252
- changed = true;
253
- }
254
- }
255
- }
256
- nodes.forEach(node => {
257
- if (isTrigger(node))
258
- return;
259
- const isConnected = edges.some(e => e.source === node.id || e.target === node.id);
260
- // With ≥1 trigger, "won't run" is precise: not reachable from any trigger.
261
- // With NO trigger yet (a draft), fall back to the connectivity check so we
262
- // still flag a fully-orphaned node without spamming every step of a
263
- // trigger-less work-in-progress.
264
- const wontRun = triggerIds.length > 0 ? !reachable.has(node.id) : !isConnected;
265
- if (wontRun) {
266
- allErrors.push({
267
- id: `node-${node.id}-orphan`,
268
- type: 'node',
269
- severity: 'warning',
270
- message: isConnected
271
- ? `"${nodeName(node)}" isn't reachable from a trigger and won't run.`
272
- : `"${nodeName(node)}" isn't connected to a flow and won't run.`,
273
- nodeId: node.id,
274
- });
275
- }
276
- });
277
- // ── Multi-flow canvas advisory (DEV-220 / ADR multi-graph decision) ──
278
- // Multiple disconnected trigger trees on one canvas are LEGAL (component-
279
- // scoped execution isolates them), but usually a maintainability smell —
280
- // warn, never block. Components are weakly-connected (undirected) with the
281
- // same parent/child closure as reachability.
282
- if (triggerIds.length > 1) {
283
- const parentOf = new Map(nodes.filter(n => n.parentId).map(n => [n.id, n.parentId]));
284
- const undirected = new Map();
285
- const link = (a, b) => {
286
- const la = undirected.get(a);
287
- if (la)
288
- la.push(b);
289
- else
290
- undirected.set(a, [b]);
291
- };
292
- for (const e of edges) {
293
- link(e.source, e.target);
294
- link(e.target, e.source);
295
- }
296
- for (const [child, parent] of parentOf) {
297
- link(child, parent);
298
- link(parent, child);
299
- }
300
- const seen = new Set();
301
- let flowsWithTrigger = 0;
302
- for (const t of triggerIds) {
303
- if (seen.has(t))
304
- continue;
305
- flowsWithTrigger++;
306
- const stack = [t];
307
- seen.add(t);
308
- while (stack.length) {
309
- const cur = stack.pop();
310
- for (const next of undirected.get(cur) ?? []) {
311
- if (!seen.has(next)) {
312
- seen.add(next);
313
- stack.push(next);
314
- }
315
- }
316
- }
317
- }
318
- if (flowsWithTrigger > 1) {
319
- allErrors.push({
320
- id: 'workflow-multi-flow',
321
- type: 'workflow',
322
- severity: 'warning',
323
- message: `This canvas holds ${flowsWithTrigger} separate flows. Each trigger runs only its own flow — consider splitting them into separate flows for clarity.`,
324
- });
325
- }
326
- }
327
- const errors = allErrors.filter(e => e.severity === 'error');
328
- const warnings = allErrors.filter(e => e.severity === 'warning');
329
- return {
330
- isValid: errors.length === 0,
331
- errors,
332
- warnings,
333
- };
334
- }
335
- /**
336
- * Get validation status for a specific node
337
- */
338
- export function getNodeValidationStatus(nodeId, validationResult) {
339
- const hasError = validationResult.errors.some(e => e.nodeId === nodeId);
340
- if (hasError)
341
- return 'error';
342
- const hasWarning = validationResult.warnings.some(e => e.nodeId === nodeId);
343
- if (hasWarning)
344
- return 'warning';
345
- return 'valid';
346
- }
347
- /**
348
- * Get validation status for a specific edge
349
- */
350
- export function getEdgeValidationStatus(edgeId, validationResult) {
351
- const hasError = validationResult.errors.some(e => e.edgeId === edgeId);
352
- if (hasError)
353
- return 'error';
354
- const hasWarning = validationResult.warnings.some(e => e.edgeId === edgeId);
355
- if (hasWarning)
356
- return 'warning';
357
- return 'valid';
358
- }