@synergenius/flow-weaver 0.20.1 → 0.20.2

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.
@@ -596,6 +596,12 @@ export function generateNodeInstanceTag(instance) {
596
596
  const tagEntries = instance.config.tags.map(t => t.tooltip ? `"${t.label}" "${t.tooltip}"` : `"${t.label}"`).join(', ');
597
597
  tagsAttr = ` [tags: ${tagEntries}]`;
598
598
  }
599
+ // Generate [suppress: "CODE", "CODE2"] attribute if present
600
+ let suppressAttr = '';
601
+ if (instance.config?.suppressWarnings?.length) {
602
+ const codes = instance.config.suppressWarnings.map(c => `"${c}"`).join(', ');
603
+ suppressAttr = ` [suppress: ${codes}]`;
604
+ }
599
605
  // Generate [size: width height] attribute if present
600
606
  let sizeAttr = '';
601
607
  if (instance.config?.width !== undefined && instance.config?.height !== undefined) {
@@ -606,7 +612,7 @@ export function generateNodeInstanceTag(instance) {
606
612
  if (instance.config?.x !== undefined && instance.config?.y !== undefined) {
607
613
  positionAttr = ` [position: ${Math.round(instance.config.x)} ${Math.round(instance.config.y)}]`;
608
614
  }
609
- return ` * @node ${instance.id} ${instance.nodeType}${parent}${labelAttr}${portOrderAttr}${portLabelAttr}${exprAttr}${pullExecutionAttr}${minimizedAttr}${colorAttr}${iconAttr}${tagsAttr}${sizeAttr}${positionAttr}`;
615
+ return ` * @node ${instance.id} ${instance.nodeType}${parent}${labelAttr}${portOrderAttr}${portLabelAttr}${exprAttr}${pullExecutionAttr}${minimizedAttr}${colorAttr}${iconAttr}${tagsAttr}${suppressAttr}${sizeAttr}${positionAttr}`;
610
616
  }
611
617
  /**
612
618
  * Generate a TypeScript function signature from a node type definition.
@@ -38,6 +38,7 @@ export { transformWorkflow } from './transform.js';
38
38
  export { type ValidationResult, validateWorkflow } from './validate.js';
39
39
  export { validationRuleRegistry } from './validation-registry.js';
40
40
  export * from './manipulation/index.js';
41
+ export { applyModifyOperation, validateModifyParams } from './modify-operation.js';
41
42
  export { withValidation, withMinimalValidation, withoutValidation, type RemoveOptions, type NodeFilter, type OperationResult, validatePortReference, portReferencesEqual, formatPortReference, generateUniqueNodeId, assertNodeTypeExists, assertNodeExists, assertNodeNotExists, } from './helpers.js';
42
43
  export * from './query.js';
43
44
  export * from './builder.js';
package/dist/api/index.js CHANGED
@@ -37,6 +37,7 @@ export { transformWorkflow } from './transform.js';
37
37
  export { validateWorkflow } from './validate.js';
38
38
  export { validationRuleRegistry } from './validation-registry.js';
39
39
  export * from './manipulation/index.js';
40
+ export { applyModifyOperation, validateModifyParams } from './modify-operation.js';
40
41
  export { withValidation, withMinimalValidation, withoutValidation, validatePortReference, portReferencesEqual, formatPortReference, generateUniqueNodeId, assertNodeTypeExists, assertNodeExists, assertNodeNotExists, } from './helpers.js';
41
42
  export * from './query.js';
42
43
  export * from './builder.js';
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ import type { TWorkflowAST } from '../ast/types.js';
3
+ export declare const modifyParamsSchemas: Record<string, z.ZodType>;
4
+ export declare function validateModifyParams(operation: string, params: Record<string, unknown>): {
5
+ success: true;
6
+ } | {
7
+ success: false;
8
+ error: string;
9
+ };
10
+ export declare function applyModifyOperation(ast: TWorkflowAST, operation: string, params: Record<string, unknown>): {
11
+ ast: TWorkflowAST;
12
+ warnings: string[];
13
+ extraData: Record<string, unknown>;
14
+ };
15
+ //# sourceMappingURL=modify-operation.d.ts.map
@@ -0,0 +1,177 @@
1
+ import { z } from 'zod';
2
+ import { addNode as manipAddNode, removeNode as manipRemoveNode, renameNode as manipRenameNode, addConnection as manipAddConnection, removeConnection as manipRemoveConnection, setNodePosition as manipSetNodePosition, setNodeLabel as manipSetNodeLabel, } from './manipulation/index.js';
3
+ import { findIsolatedNodes } from './query.js';
4
+ export const modifyParamsSchemas = {
5
+ addNode: z.object({
6
+ nodeId: z.string({ required_error: 'nodeId is required' }),
7
+ nodeType: z.string({ required_error: 'nodeType is required' }),
8
+ x: z.number().optional(),
9
+ y: z.number().optional(),
10
+ }),
11
+ removeNode: z.object({
12
+ nodeId: z.string({ required_error: 'nodeId is required' }),
13
+ }),
14
+ renameNode: z.object({
15
+ oldId: z.string({ required_error: 'oldId is required' }),
16
+ newId: z.string({ required_error: 'newId is required' }),
17
+ }),
18
+ addConnection: z.object({
19
+ from: z.string({ required_error: 'from is required (format: "node.port")' }),
20
+ to: z.string({ required_error: 'to is required (format: "node.port")' }),
21
+ }),
22
+ removeConnection: z.object({
23
+ from: z.string({ required_error: 'from is required (format: "node.port")' }),
24
+ to: z.string({ required_error: 'to is required (format: "node.port")' }),
25
+ }),
26
+ setNodePosition: z.object({
27
+ nodeId: z.string({ required_error: 'nodeId is required' }),
28
+ x: z.number({ required_error: 'x is required', invalid_type_error: 'x must be a number' }),
29
+ y: z.number({ required_error: 'y is required', invalid_type_error: 'y must be a number' }),
30
+ }),
31
+ setNodeLabel: z.object({
32
+ nodeId: z.string({ required_error: 'nodeId is required' }),
33
+ label: z.string({ required_error: 'label is required' }),
34
+ }),
35
+ };
36
+ export function validateModifyParams(operation, params) {
37
+ const schema = modifyParamsSchemas[operation];
38
+ if (!schema) {
39
+ return { success: false, error: `Unknown operation: ${operation}` };
40
+ }
41
+ const result = schema.safeParse(params);
42
+ if (!result.success) {
43
+ const messages = result.error.issues.map((i) => i.message).join('; ');
44
+ return { success: false, error: `${operation} params invalid: ${messages}` };
45
+ }
46
+ return { success: true };
47
+ }
48
+ export function applyModifyOperation(ast, operation, params) {
49
+ const p = params;
50
+ const warnings = [];
51
+ const extraData = {};
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- AST manipulation functions use loose typing
53
+ let modifiedAST = ast;
54
+ switch (operation) {
55
+ case 'addNode': {
56
+ const nodeId = p.nodeId;
57
+ const nodeType = p.nodeType;
58
+ const nodeTypeExists = modifiedAST.nodeTypes.some((nt) => nt.name === nodeType || nt.functionName === nodeType);
59
+ if (!nodeTypeExists) {
60
+ warnings.push(`Node type "${nodeType}" is not defined in the file. ` +
61
+ `The node will be added but may not render until the type is defined.`);
62
+ }
63
+ let autoX = typeof p.x === 'number' ? p.x : undefined;
64
+ let autoY = typeof p.y === 'number' ? p.y : undefined;
65
+ if (autoX === undefined || autoY === undefined) {
66
+ const positions = modifiedAST.instances
67
+ .map((inst) => inst.config)
68
+ .filter((c) => c !== undefined &&
69
+ c !== null &&
70
+ typeof c.x === 'number' &&
71
+ typeof c.y === 'number');
72
+ if (positions.length > 0) {
73
+ const maxX = Math.max(...positions.map((pos) => pos.x));
74
+ if (autoX === undefined)
75
+ autoX = maxX + 180;
76
+ if (autoY === undefined)
77
+ autoY = 0;
78
+ }
79
+ else {
80
+ if (autoX === undefined)
81
+ autoX = 0;
82
+ if (autoY === undefined)
83
+ autoY = 0;
84
+ }
85
+ }
86
+ modifiedAST = manipAddNode(modifiedAST, {
87
+ type: 'NodeInstance',
88
+ id: nodeId,
89
+ nodeType,
90
+ config: { x: autoX, y: autoY },
91
+ });
92
+ break;
93
+ }
94
+ case 'removeNode': {
95
+ const nodeId = p.nodeId;
96
+ const removedConnections = modifiedAST.connections
97
+ .filter((c) => c.from.node === nodeId || c.to.node === nodeId)
98
+ .map((c) => ({
99
+ from: `${c.from.node}.${c.from.port}`,
100
+ to: `${c.to.node}.${c.to.port}`,
101
+ }));
102
+ modifiedAST = manipRemoveNode(modifiedAST, nodeId);
103
+ if (removedConnections.length > 0) {
104
+ extraData.removedConnections = removedConnections;
105
+ }
106
+ break;
107
+ }
108
+ case 'renameNode': {
109
+ modifiedAST = manipRenameNode(modifiedAST, p.oldId, p.newId);
110
+ break;
111
+ }
112
+ case 'addConnection': {
113
+ const from = p.from;
114
+ const to = p.to;
115
+ const [fromNode, fromPort] = from.split('.');
116
+ const [toNode, toPort] = to.split('.');
117
+ if (!fromPort || !toPort) {
118
+ throw new Error('Connection format must be "node.port" (e.g., "Start.execute")');
119
+ }
120
+ const validNodes = [
121
+ 'Start',
122
+ 'Exit',
123
+ ...modifiedAST.instances.map((i) => i.id),
124
+ ];
125
+ if (!validNodes.includes(fromNode)) {
126
+ throw new Error(`Source node "${fromNode}" not found. Available: ${validNodes.join(', ')}`);
127
+ }
128
+ if (!validNodes.includes(toNode)) {
129
+ throw new Error(`Target node "${toNode}" not found. Available: ${validNodes.join(', ')}`);
130
+ }
131
+ if (fromNode !== 'Start' && fromNode !== 'Exit') {
132
+ const inst = modifiedAST.instances.find((i) => i.id === fromNode);
133
+ const nt = modifiedAST.nodeTypes.find((t) => t.name === inst?.nodeType);
134
+ if (nt && !nt.outputs[fromPort]) {
135
+ throw new Error(`Node "${fromNode}" has no output "${fromPort}". Available: ${Object.keys(nt.outputs).join(', ')}`);
136
+ }
137
+ }
138
+ if (toNode !== 'Start' && toNode !== 'Exit') {
139
+ const inst = modifiedAST.instances.find((i) => i.id === toNode);
140
+ const nt = modifiedAST.nodeTypes.find((t) => t.name === inst?.nodeType);
141
+ if (nt && !nt.inputs[toPort]) {
142
+ throw new Error(`Node "${toNode}" has no input "${toPort}". Available: ${Object.keys(nt.inputs).join(', ')}`);
143
+ }
144
+ }
145
+ modifiedAST = manipAddConnection(modifiedAST, from, to);
146
+ if (modifiedAST.options?.autoConnect) {
147
+ modifiedAST = { ...modifiedAST, options: { ...modifiedAST.options, autoConnect: undefined } };
148
+ warnings.push('autoConnect was disabled because connections were manually modified');
149
+ }
150
+ break;
151
+ }
152
+ case 'removeConnection': {
153
+ modifiedAST = manipRemoveConnection(modifiedAST, p.from, p.to);
154
+ if (modifiedAST.options?.autoConnect) {
155
+ modifiedAST = { ...modifiedAST, options: { ...modifiedAST.options, autoConnect: undefined } };
156
+ warnings.push('autoConnect was disabled because connections were manually modified');
157
+ }
158
+ const newlyIsolated = findIsolatedNodes(modifiedAST);
159
+ if (newlyIsolated.length > 0) {
160
+ extraData.newlyIsolatedNodes = newlyIsolated;
161
+ }
162
+ break;
163
+ }
164
+ case 'setNodePosition': {
165
+ modifiedAST = manipSetNodePosition(modifiedAST, p.nodeId, p.x, p.y);
166
+ break;
167
+ }
168
+ case 'setNodeLabel': {
169
+ modifiedAST = manipSetNodeLabel(modifiedAST, p.nodeId, p.label);
170
+ break;
171
+ }
172
+ default:
173
+ throw new Error(`Unknown operation: ${operation}`);
174
+ }
175
+ return { ast: modifiedAST, warnings, extraData };
176
+ }
177
+ //# sourceMappingURL=modify-operation.js.map
@@ -345,6 +345,8 @@ export type TNodeInstanceConfig = {
345
345
  minimized?: boolean;
346
346
  width?: number;
347
347
  height?: number;
348
+ /** Warning codes to suppress for this instance (e.g., ["UNUSED_OUTPUT_PORT"]) */
349
+ suppressWarnings?: string[];
348
350
  };
349
351
  /**
350
352
  * Node Instance AST - A usage of a node type in a workflow.
@@ -31,6 +31,8 @@ export interface NodeParseResult {
31
31
  job?: string;
32
32
  /** CI/CD deployment environment */
33
33
  environment?: string;
34
+ /** Warning codes to suppress for this instance */
35
+ suppress?: string[];
34
36
  }
35
37
  /**
36
38
  * Parse a @node line and return structured result.
@@ -4,7 +4,7 @@
4
4
  * Parser for @node declarations using Chevrotain.
5
5
  */
6
6
  import { CstParser } from 'chevrotain';
7
- import { JSDocLexer, NodeTag, Identifier, Dot, Integer, LabelPrefix, ExprPrefix, PortOrderPrefix, PortLabelPrefix, MinimizedKeyword, PullExecutionPrefix, SizePrefix, PositionPrefix, ColorPrefix, IconPrefix, JobPrefix, EnvironmentAttrPrefix, TagsPrefix, StringLiteral, LBracket, RBracket, Comma, Equals, EventEq, CronEq, MatchEq, TimeoutEq, LimitEq, PeriodEq, allTokens, } from './tokens.js';
7
+ import { JSDocLexer, NodeTag, Identifier, Dot, Integer, LabelPrefix, ExprPrefix, PortOrderPrefix, PortLabelPrefix, MinimizedKeyword, PullExecutionPrefix, SizePrefix, PositionPrefix, ColorPrefix, IconPrefix, JobPrefix, EnvironmentAttrPrefix, TagsPrefix, SuppressPrefix, StringLiteral, LBracket, RBracket, Comma, Equals, EventEq, CronEq, MatchEq, TimeoutEq, LimitEq, PeriodEq, allTokens, } from './tokens.js';
8
8
  // =============================================================================
9
9
  // Parser Definition
10
10
  // =============================================================================
@@ -54,6 +54,7 @@ class NodeParser extends CstParser {
54
54
  { ALT: () => this.SUBRULE(this.jobAttr) },
55
55
  { ALT: () => this.SUBRULE(this.environmentAttr) },
56
56
  { ALT: () => this.SUBRULE(this.tagsAttr) },
57
+ { ALT: () => this.SUBRULE(this.suppressAttr) },
57
58
  ]);
58
59
  },
59
60
  });
@@ -179,6 +180,16 @@ class NodeParser extends CstParser {
179
180
  },
180
181
  });
181
182
  });
183
+ // suppress: "CODE", "CODE2"
184
+ suppressAttr = this.RULE('suppressAttr', () => {
185
+ this.CONSUME(SuppressPrefix);
186
+ this.AT_LEAST_ONE_SEP({
187
+ SEP: Comma,
188
+ DEF: () => {
189
+ this.CONSUME(StringLiteral, { LABEL: 'suppressCode' });
190
+ },
191
+ });
192
+ });
182
193
  // "label" ["tooltip"]
183
194
  tagEntry = this.RULE('tagEntry', () => {
184
195
  this.CONSUME(StringLiteral, { LABEL: 'tagLabel' });
@@ -217,6 +228,7 @@ class NodeVisitor extends BaseVisitor {
217
228
  let tags;
218
229
  let job;
219
230
  let environment;
231
+ let suppress;
220
232
  if (ctx.parentScopeRef) {
221
233
  parentScope = this.visit(ctx.parentScopeRef);
222
234
  }
@@ -249,6 +261,8 @@ class NodeVisitor extends BaseVisitor {
249
261
  job = attrs.job;
250
262
  if (attrs.environment)
251
263
  environment = attrs.environment;
264
+ if (attrs.suppress)
265
+ suppress = [...(suppress || []), ...attrs.suppress];
252
266
  }
253
267
  }
254
268
  return {
@@ -268,6 +282,7 @@ class NodeVisitor extends BaseVisitor {
268
282
  ...(tags && { tags }),
269
283
  ...(job && { job }),
270
284
  ...(environment && { environment }),
285
+ ...(suppress && { suppress }),
271
286
  };
272
287
  }
273
288
  parentScopeRef(ctx) {
@@ -289,6 +304,7 @@ class NodeVisitor extends BaseVisitor {
289
304
  let tags;
290
305
  let job;
291
306
  let environment;
307
+ let suppress;
292
308
  if (ctx.labelAttr) {
293
309
  for (const attr of ctx.labelAttr) {
294
310
  label = this.visit(attr);
@@ -356,6 +372,12 @@ class NodeVisitor extends BaseVisitor {
356
372
  environment = this.visit(attr);
357
373
  }
358
374
  }
375
+ if (ctx.suppressAttr) {
376
+ for (const attr of ctx.suppressAttr) {
377
+ const codes = this.visit(attr);
378
+ suppress = [...(suppress || []), ...codes];
379
+ }
380
+ }
359
381
  return {
360
382
  label,
361
383
  expressions,
@@ -370,6 +392,7 @@ class NodeVisitor extends BaseVisitor {
370
392
  tags,
371
393
  job,
372
394
  environment,
395
+ suppress,
373
396
  };
374
397
  }
375
398
  labelAttr(ctx) {
@@ -466,6 +489,9 @@ class NodeVisitor extends BaseVisitor {
466
489
  environmentAttr(ctx) {
467
490
  return this.unescapeString(ctx.environmentValue[0].image);
468
491
  }
492
+ suppressAttr(ctx) {
493
+ return ctx.suppressCode.map((tok) => this.unescapeString(tok.image));
494
+ }
469
495
  tagsAttr(ctx) {
470
496
  const result = [];
471
497
  if (ctx.tagEntry) {
@@ -51,6 +51,7 @@ export declare const IconPrefix: import("chevrotain").TokenType;
51
51
  export declare const JobPrefix: import("chevrotain").TokenType;
52
52
  export declare const EnvironmentAttrPrefix: import("chevrotain").TokenType;
53
53
  export declare const TagsPrefix: import("chevrotain").TokenType;
54
+ export declare const SuppressPrefix: import("chevrotain").TokenType;
54
55
  export declare const EventEq: import("chevrotain").TokenType;
55
56
  export declare const CronEq: import("chevrotain").TokenType;
56
57
  export declare const MatchEq: import("chevrotain").TokenType;
@@ -200,6 +200,10 @@ export const TagsPrefix = createToken({
200
200
  name: 'TagsPrefix',
201
201
  pattern: /tags:/,
202
202
  });
203
+ export const SuppressPrefix = createToken({
204
+ name: 'SuppressPrefix',
205
+ pattern: /suppress:/,
206
+ });
203
207
  export const EventEq = createToken({
204
208
  name: 'EventEq',
205
209
  pattern: /event=/,
@@ -378,6 +382,7 @@ export const allTokens = [
378
382
  JobPrefix,
379
383
  EnvironmentAttrPrefix,
380
384
  TagsPrefix,
385
+ SuppressPrefix,
381
386
  EventEq,
382
387
  CronEq,
383
388
  MatchEq,
@@ -0,0 +1,29 @@
1
+ export declare function modifyAddNodeCommand(file: string, opts: {
2
+ nodeId: string;
3
+ nodeType: string;
4
+ }): Promise<void>;
5
+ export declare function modifyRemoveNodeCommand(file: string, opts: {
6
+ nodeId: string;
7
+ }): Promise<void>;
8
+ export declare function modifyAddConnectionCommand(file: string, opts: {
9
+ from: string;
10
+ to: string;
11
+ }): Promise<void>;
12
+ export declare function modifyRemoveConnectionCommand(file: string, opts: {
13
+ from: string;
14
+ to: string;
15
+ }): Promise<void>;
16
+ export declare function modifyRenameNodeCommand(file: string, opts: {
17
+ oldId: string;
18
+ newId: string;
19
+ }): Promise<void>;
20
+ export declare function modifySetPositionCommand(file: string, opts: {
21
+ nodeId: string;
22
+ x: string;
23
+ y: string;
24
+ }): Promise<void>;
25
+ export declare function modifySetLabelCommand(file: string, opts: {
26
+ nodeId: string;
27
+ label: string;
28
+ }): Promise<void>;
29
+ //# sourceMappingURL=modify.d.ts.map
@@ -0,0 +1,57 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { parseWorkflow } from '../../api/index.js';
4
+ import { generateInPlace } from '../../api/generate-in-place.js';
5
+ import { applyModifyOperation, validateModifyParams } from '../../api/modify-operation.js';
6
+ import { logger } from '../utils/logger.js';
7
+ async function readParseModifyWrite(file, operation, params) {
8
+ const validation = validateModifyParams(operation, params);
9
+ if (!validation.success) {
10
+ throw new Error(validation.error);
11
+ }
12
+ const filePath = path.resolve(file);
13
+ const source = fs.readFileSync(filePath, 'utf-8');
14
+ const parseResult = await parseWorkflow(filePath);
15
+ if (parseResult.errors.length > 0) {
16
+ throw new Error(`Parse errors:\n${parseResult.errors.join('\n')}`);
17
+ }
18
+ const { ast: modifiedAST, warnings } = applyModifyOperation(parseResult.ast, operation, params);
19
+ const result = generateInPlace(source, modifiedAST);
20
+ fs.writeFileSync(filePath, result.code, 'utf-8');
21
+ for (const w of warnings) {
22
+ logger.warn(w);
23
+ }
24
+ }
25
+ export async function modifyAddNodeCommand(file, opts) {
26
+ await readParseModifyWrite(file, 'addNode', { nodeId: opts.nodeId, nodeType: opts.nodeType });
27
+ logger.success(`Added node "${opts.nodeId}" (type: ${opts.nodeType}) to ${file}`);
28
+ }
29
+ export async function modifyRemoveNodeCommand(file, opts) {
30
+ await readParseModifyWrite(file, 'removeNode', { nodeId: opts.nodeId });
31
+ logger.success(`Removed node "${opts.nodeId}" from ${file}`);
32
+ }
33
+ export async function modifyAddConnectionCommand(file, opts) {
34
+ await readParseModifyWrite(file, 'addConnection', { from: opts.from, to: opts.to });
35
+ logger.success(`Added connection ${opts.from} -> ${opts.to} in ${file}`);
36
+ }
37
+ export async function modifyRemoveConnectionCommand(file, opts) {
38
+ await readParseModifyWrite(file, 'removeConnection', { from: opts.from, to: opts.to });
39
+ logger.success(`Removed connection ${opts.from} -> ${opts.to} from ${file}`);
40
+ }
41
+ export async function modifyRenameNodeCommand(file, opts) {
42
+ await readParseModifyWrite(file, 'renameNode', { oldId: opts.oldId, newId: opts.newId });
43
+ logger.success(`Renamed node "${opts.oldId}" to "${opts.newId}" in ${file}`);
44
+ }
45
+ export async function modifySetPositionCommand(file, opts) {
46
+ await readParseModifyWrite(file, 'setNodePosition', {
47
+ nodeId: opts.nodeId,
48
+ x: Number(opts.x),
49
+ y: Number(opts.y),
50
+ });
51
+ logger.success(`Set position of "${opts.nodeId}" to (${opts.x}, ${opts.y}) in ${file}`);
52
+ }
53
+ export async function modifySetLabelCommand(file, opts) {
54
+ await readParseModifyWrite(file, 'setNodeLabel', { nodeId: opts.nodeId, label: opts.label });
55
+ logger.success(`Set label of "${opts.nodeId}" to "${opts.label}" in ${file}`);
56
+ }
57
+ //# sourceMappingURL=modify.js.map