@synergenius/flow-weaver 0.8.3 → 0.9.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.
@@ -1,4 +1,4 @@
1
- import type { TNodeInstanceAST, TPortDefinition, TWorkflowAST } from "./ast/index.js";
1
+ import type { TNodeTypeAST, TNodeInstanceAST, TPortDefinition, TWorkflowAST } from "./ast/index.js";
2
2
  export interface GenerateAnnotationsOptions {
3
3
  includeComments?: boolean;
4
4
  includeMetadata?: boolean;
@@ -41,5 +41,10 @@ export declare function assignPortOrders(ports: [string, TPortDefinition][], _di
41
41
  * Exported for reuse in generate-in-place.ts to maintain DRY principle
42
42
  */
43
43
  export declare function generateNodeInstanceTag(instance: TNodeInstanceAST): string;
44
+ /**
45
+ * Generate a TypeScript function signature from a node type definition.
46
+ * Handles three modes: stub (declare function), expression (pure function), normal (execute/onSuccess/onFailure).
47
+ */
48
+ export declare function generateFunctionSignature(nodeType: TNodeTypeAST): string[];
44
49
  export declare const annotationGenerator: AnnotationGenerator;
45
50
  //# sourceMappingURL=annotation-generator.d.ts.map
@@ -24,6 +24,10 @@ export class AnnotationGenerator {
24
24
  if (nodeType.variant === 'COERCION') {
25
25
  return;
26
26
  }
27
+ // Skip inferred stubs — they have no @flowWeaver annotation and were auto-detected
28
+ if (nodeType.variant === 'STUB' && nodeType.inferred) {
29
+ return;
30
+ }
27
31
  lines.push(...this.generateNodeTypeAnnotation(nodeType, indent, includeComments, includeMetadata));
28
32
  lines.push("");
29
33
  });
@@ -47,8 +51,14 @@ export class AnnotationGenerator {
47
51
  }
48
52
  lines.push(` *`);
49
53
  }
50
- // @flowWeaver nodeType marker
51
- lines.push(" * @flowWeaver nodeType");
54
+ // @flowWeaver marker: use 'node' shorthand for stubs and expression nodes
55
+ const isStub = nodeType.variant === 'STUB';
56
+ if (isStub || nodeType.expression) {
57
+ lines.push(" * @flowWeaver node");
58
+ }
59
+ else {
60
+ lines.push(" * @flowWeaver nodeType");
61
+ }
52
62
  // Add label if present
53
63
  if (includeMetadata && nodeType.label) {
54
64
  lines.push(` * @label ${nodeType.label}`);
@@ -117,28 +127,7 @@ export class AnnotationGenerator {
117
127
  return `{ ${props.join(", ")} }`;
118
128
  }
119
129
  generateFunctionSignature(nodeType) {
120
- const lines = [];
121
- // Build parameters (excluding execute which will be first)
122
- const params = ["execute: boolean"];
123
- Object.entries(nodeType.inputs).forEach(([name, port]) => {
124
- if (isExecutePort(name))
125
- return;
126
- const optional = port.optional ? "?" : "";
127
- const defaultVal = port.default !== undefined ? ` = ${JSON.stringify(port.default)}` : "";
128
- params.push(`${name}${optional}: ${this.mapDataTypeToTS(port.dataType)}${defaultVal}`);
129
- });
130
- // Build return type (including onSuccess/onFailure)
131
- const returns = ["onSuccess: boolean", "onFailure: boolean"];
132
- Object.entries(nodeType.outputs).forEach(([name, port]) => {
133
- if (isSuccessPort(name) || isFailurePort(name))
134
- return;
135
- returns.push(`${name}: ${this.mapDataTypeToTS(port.dataType)}`);
136
- });
137
- lines.push(`function ${nodeType.functionName}(${params.join(", ")}) {`);
138
- lines.push(` if (!execute) return { onSuccess: false, onFailure: false };`);
139
- lines.push(` return { onSuccess: true, onFailure: false };`);
140
- lines.push(`}`);
141
- return lines;
130
+ return generateFunctionSignature(nodeType);
142
131
  }
143
132
  generateWorkflowAnnotation(workflow, indent, includeComments, skipParamReturns = false) {
144
133
  const lines = [];
@@ -329,6 +318,11 @@ export class AnnotationGenerator {
329
318
  }
330
319
  generateWorkflowFunctionSignature(workflow) {
331
320
  const lines = [];
321
+ // Stub workflows use const declaration format
322
+ if (workflow.stub) {
323
+ lines.push(`export const ${workflow.functionName} = 'flowWeaver:draft';`);
324
+ return lines;
325
+ }
332
326
  const startPorts = workflow.startPorts || {};
333
327
  const exitPorts = workflow.exitPorts || {};
334
328
  // Build parameter types (excluding execute)
@@ -614,5 +608,62 @@ export function generateNodeInstanceTag(instance) {
614
608
  }
615
609
  return ` * @node ${instance.id} ${instance.nodeType}${parent}${labelAttr}${portOrderAttr}${portLabelAttr}${exprAttr}${pullExecutionAttr}${minimizedAttr}${colorAttr}${iconAttr}${tagsAttr}${sizeAttr}${positionAttr}`;
616
610
  }
611
+ /**
612
+ * Generate a TypeScript function signature from a node type definition.
613
+ * Handles three modes: stub (declare function), expression (pure function), normal (execute/onSuccess/onFailure).
614
+ */
615
+ export function generateFunctionSignature(nodeType) {
616
+ const lines = [];
617
+ const isStub = nodeType.variant === 'STUB';
618
+ const isExpression = isStub || nodeType.expression;
619
+ if (isExpression) {
620
+ const params = [];
621
+ Object.entries(nodeType.inputs).forEach(([name, port]) => {
622
+ if (isExecutePort(name))
623
+ return;
624
+ const optional = port.optional ? "?" : "";
625
+ params.push(`${name}${optional}: ${mapToTypeScript(port.dataType)}`);
626
+ });
627
+ const returns = [];
628
+ Object.entries(nodeType.outputs).forEach(([name, port]) => {
629
+ if (isSuccessPort(name) || isFailurePort(name))
630
+ return;
631
+ returns.push(`${name}: ${mapToTypeScript(port.dataType)}`);
632
+ });
633
+ const returnType = returns.length === 1
634
+ ? mapToTypeScript((Object.entries(nodeType.outputs).find(([n]) => !isSuccessPort(n) && !isFailurePort(n))?.[1].dataType || 'ANY'))
635
+ : `{ ${returns.join("; ")} }`;
636
+ if (isStub) {
637
+ lines.push(`declare function ${nodeType.functionName}(${params.join(", ")}): ${returnType};`);
638
+ }
639
+ else {
640
+ lines.push(`function ${nodeType.functionName}(${params.join(", ")}): ${returnType} {`);
641
+ lines.push(` // TODO: implement`);
642
+ lines.push(` throw new Error('Not implemented');`);
643
+ lines.push(`}`);
644
+ }
645
+ }
646
+ else {
647
+ const params = ["execute: boolean"];
648
+ Object.entries(nodeType.inputs).forEach(([name, port]) => {
649
+ if (isExecutePort(name))
650
+ return;
651
+ const optional = port.optional ? "?" : "";
652
+ const defaultVal = port.default !== undefined ? ` = ${JSON.stringify(port.default)}` : "";
653
+ params.push(`${name}${optional}: ${mapToTypeScript(port.dataType)}${defaultVal}`);
654
+ });
655
+ const returns = ["onSuccess: boolean", "onFailure: boolean"];
656
+ Object.entries(nodeType.outputs).forEach(([name, port]) => {
657
+ if (isSuccessPort(name) || isFailurePort(name))
658
+ return;
659
+ returns.push(`${name}: ${mapToTypeScript(port.dataType)}`);
660
+ });
661
+ lines.push(`function ${nodeType.functionName}(${params.join(", ")}) {`);
662
+ lines.push(` if (!execute) return { onSuccess: false, onFailure: false };`);
663
+ lines.push(` return { onSuccess: true, onFailure: false };`);
664
+ lines.push(`}`);
665
+ }
666
+ return lines;
667
+ }
617
668
  export const annotationGenerator = new AnnotationGenerator();
618
669
  //# sourceMappingURL=annotation-generator.js.map
@@ -37,6 +37,11 @@ export interface GenerateOptions extends Partial<ASTGenerateOptions> {
37
37
  * @example { 'add': '../node-types/add.js', 'greet': '../node-types/greet.js' }
38
38
  */
39
39
  externalNodeTypes?: Record<string, string>;
40
+ /**
41
+ * Allow generation even when stub nodes exist. Stub nodes will emit
42
+ * a throw statement at runtime. Default: false (refuse to generate with stubs).
43
+ */
44
+ generateStubs?: boolean;
40
45
  }
41
46
  /**
42
47
  * Generate an import statement in the appropriate module format
@@ -38,7 +38,14 @@ export function generateModuleExports(functionNames) {
38
38
  return `module.exports = { ${functionNames.join(', ')} };`;
39
39
  }
40
40
  export function generateCode(ast, options) {
41
- const { production = false, sourceMap = false, allWorkflows = [], moduleFormat = 'esm', externalRuntimePath, constants = [], externalNodeTypes = {}, } = options || {};
41
+ const { production = false, sourceMap = false, allWorkflows = [], moduleFormat = 'esm', externalRuntimePath, constants = [], externalNodeTypes = {}, generateStubs = false, } = options || {};
42
+ // Check for stub nodes — refuse to generate unless explicitly allowed
43
+ const stubNodeTypes = ast.nodeTypes.filter((nt) => nt.variant === 'STUB');
44
+ if (stubNodeTypes.length > 0 && !generateStubs) {
45
+ const stubNames = stubNodeTypes.map((nt) => nt.functionName).join(', ');
46
+ throw new Error(`Cannot generate code: workflow has ${stubNodeTypes.length} stub node(s) without implementation: ${stubNames}. ` +
47
+ `Implement them or pass { generateStubs: true } to emit placeholder throws.`);
48
+ }
42
49
  // Determine if workflow should be async based on node composition
43
50
  const { shouldBeAsync, warning } = validateWorkflowAsync(ast, ast.nodeTypes);
44
51
  if (warning && !production) {
@@ -18,8 +18,11 @@ export interface ValidationResult {
18
18
  * Agent rules are always applied automatically.
19
19
  *
20
20
  * @param ast - The workflow AST to validate
21
- * @param customRules - Optional array of additional custom validation rules
21
+ * @param options - Validation options: custom rules and/or draft mode
22
22
  * @returns ValidationResult with errors and warnings
23
23
  */
24
- export declare function validateWorkflow(ast: TWorkflowAST, customRules?: TValidationRule[]): ValidationResult;
24
+ export declare function validateWorkflow(ast: TWorkflowAST, options?: {
25
+ customRules?: TValidationRule[];
26
+ mode?: 'strict' | 'draft';
27
+ }): ValidationResult;
25
28
  //# sourceMappingURL=validate.d.ts.map
@@ -13,14 +13,14 @@ import { getAgentValidationRules } from "../validation/agent-rules.js";
13
13
  * Agent rules are always applied automatically.
14
14
  *
15
15
  * @param ast - The workflow AST to validate
16
- * @param customRules - Optional array of additional custom validation rules
16
+ * @param options - Validation options: custom rules and/or draft mode
17
17
  * @returns ValidationResult with errors and warnings
18
18
  */
19
- export function validateWorkflow(ast, customRules) {
19
+ export function validateWorkflow(ast, options) {
20
20
  // Use the consolidated validator
21
- const result = validator.validate(ast);
21
+ const result = validator.validate(ast, { mode: options?.mode });
22
22
  // Apply agent-specific rules + any custom rules
23
- const allRules = [...getAgentValidationRules(), ...(customRules || [])];
23
+ const allRules = [...getAgentValidationRules(), ...(options?.customRules || [])];
24
24
  for (const rule of allRules) {
25
25
  const ruleResults = rule.validate(ast);
26
26
  for (const err of ruleResults) {
@@ -104,6 +104,8 @@ export type TWorkflowAST = {
104
104
  availableFunctionNames?: string[];
105
105
  /** Sugar macros (@map, @filter) that expand to full scope patterns. Stored for round-trip preservation. */
106
106
  macros?: TWorkflowMacro[];
107
+ /** Whether this workflow was defined as a stub (const declaration, no function body). */
108
+ stub?: boolean;
107
109
  /** Reserved for plugin extensibility */
108
110
  metadata?: TWorkflowMetadata;
109
111
  };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Implement command — replaces a stub node with a real function skeleton
3
+ */
4
+ export interface ImplementOptions {
5
+ workflowName?: string;
6
+ preview?: boolean;
7
+ }
8
+ export declare function implementCommand(input: string, nodeName: string, options?: ImplementOptions): Promise<void>;
9
+ //# sourceMappingURL=implement.d.ts.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Implement command — replaces a stub node with a real function skeleton
3
+ */
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import { parseWorkflow } from '../../api/index.js';
7
+ import { generateFunctionSignature } from '../../annotation-generator.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import { getErrorMessage } from '../../utils/error-utils.js';
10
+ /**
11
+ * Find a `declare function <name>(...): ...;` declaration in source text,
12
+ * handling multiline signatures. Returns the full matched text and its
13
+ * leading indentation, or null if not found.
14
+ */
15
+ function findDeclareFunction(source, functionName) {
16
+ const lines = source.split('\n');
17
+ const escaped = functionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ const startPattern = new RegExp(`^(\\s*)declare\\s+function\\s+${escaped}\\s*\\(`);
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const m = lines[i].match(startPattern);
21
+ if (!m)
22
+ continue;
23
+ const indent = m[1] || '';
24
+ // Accumulate lines until we find one ending with ;
25
+ let accumulated = lines[i];
26
+ let j = i;
27
+ while (!accumulated.trimEnd().endsWith(';') && j < lines.length - 1) {
28
+ j++;
29
+ accumulated += '\n' + lines[j];
30
+ }
31
+ return { match: accumulated, indent };
32
+ }
33
+ return null;
34
+ }
35
+ export async function implementCommand(input, nodeName, options = {}) {
36
+ const { workflowName, preview = false } = options;
37
+ try {
38
+ const filePath = path.resolve(input);
39
+ if (!fs.existsSync(filePath)) {
40
+ logger.error(`File not found: ${input}`);
41
+ process.exit(1);
42
+ }
43
+ const parseResult = await parseWorkflow(filePath, { workflowName });
44
+ if (parseResult.errors.length > 0) {
45
+ logger.error('Parse errors:');
46
+ parseResult.errors.forEach((e) => logger.error(` ${e}`));
47
+ process.exit(1);
48
+ }
49
+ const ast = parseResult.ast;
50
+ // Find the stub node type
51
+ const stubNodeType = ast.nodeTypes.find((nt) => nt.variant === 'STUB' && (nt.functionName === nodeName || nt.name === nodeName));
52
+ if (!stubNodeType) {
53
+ const existingNt = ast.nodeTypes.find((nt) => nt.functionName === nodeName || nt.name === nodeName);
54
+ if (existingNt) {
55
+ logger.warn(`Node "${nodeName}" is already implemented.`);
56
+ process.exit(0);
57
+ }
58
+ const available = ast.nodeTypes
59
+ .filter((nt) => nt.variant === 'STUB')
60
+ .map((nt) => nt.functionName);
61
+ if (available.length === 0) {
62
+ logger.error('No stub nodes found in this workflow.');
63
+ }
64
+ else {
65
+ logger.error(`Stub node "${nodeName}" not found. Available stubs: ${available.join(', ')}`);
66
+ }
67
+ process.exit(1);
68
+ }
69
+ const source = fs.readFileSync(filePath, 'utf8');
70
+ const found = findDeclareFunction(source, stubNodeType.functionName);
71
+ if (!found) {
72
+ logger.error(`Could not find "declare function ${stubNodeType.functionName}" in source file.`);
73
+ process.exit(1);
74
+ }
75
+ // Generate the real function signature
76
+ const implementedType = { ...stubNodeType, variant: 'FUNCTION' };
77
+ const signatureLines = generateFunctionSignature(implementedType);
78
+ const replacement = signatureLines.map((line) => found.indent + line).join('\n');
79
+ if (preview) {
80
+ logger.section(`Preview: ${stubNodeType.functionName}`);
81
+ logger.newline();
82
+ // eslint-disable-next-line no-console
83
+ console.log(replacement);
84
+ }
85
+ else {
86
+ const updated = source.replace(found.match, replacement);
87
+ fs.writeFileSync(filePath, updated, 'utf8');
88
+ logger.success(`Implemented ${stubNodeType.functionName} in ${path.basename(filePath)}`);
89
+ }
90
+ }
91
+ catch (error) {
92
+ logger.error(`Implement failed: ${getErrorMessage(error)}`);
93
+ process.exit(1);
94
+ }
95
+ }
96
+ //# sourceMappingURL=implement.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Status command — reports implementation progress for stub workflows
3
+ */
4
+ export interface StatusOptions {
5
+ workflowName?: string;
6
+ json?: boolean;
7
+ }
8
+ export declare function statusCommand(input: string, options?: StatusOptions): Promise<void>;
9
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Status command — reports implementation progress for stub workflows
3
+ */
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import { parseWorkflow } from '../../api/index.js';
7
+ import { validator } from '../../validator.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import { getErrorMessage } from '../../utils/error-utils.js';
10
+ function formatPortList(ports) {
11
+ return Object.entries(ports)
12
+ .filter(([name]) => name !== 'execute' && name !== 'onSuccess' && name !== 'onFailure')
13
+ .map(([name, port]) => `${name}(${port.dataType.toLowerCase()})`);
14
+ }
15
+ export async function statusCommand(input, options = {}) {
16
+ const { workflowName, json = false } = options;
17
+ try {
18
+ const filePath = path.resolve(input);
19
+ if (!fs.existsSync(filePath)) {
20
+ if (json) {
21
+ console.log(JSON.stringify({ error: `File not found: ${input}` }));
22
+ }
23
+ else {
24
+ logger.error(`File not found: ${input}`);
25
+ }
26
+ process.exit(1);
27
+ }
28
+ const parseResult = await parseWorkflow(filePath, { workflowName });
29
+ if (parseResult.errors.length > 0) {
30
+ if (json) {
31
+ console.log(JSON.stringify({ error: `Parse errors: ${parseResult.errors.join(', ')}` }));
32
+ }
33
+ else {
34
+ logger.error('Parse errors:');
35
+ parseResult.errors.forEach((e) => logger.error(` ${e}`));
36
+ }
37
+ process.exit(1);
38
+ }
39
+ const ast = parseResult.ast;
40
+ // Collect node statuses
41
+ const instanceTypeMap = new Map();
42
+ for (const nt of ast.nodeTypes) {
43
+ instanceTypeMap.set(nt.name, nt);
44
+ if (nt.functionName !== nt.name) {
45
+ instanceTypeMap.set(nt.functionName, nt);
46
+ }
47
+ }
48
+ const nodes = [];
49
+ const seen = new Set();
50
+ for (const instance of ast.instances) {
51
+ const nt = instanceTypeMap.get(instance.nodeType);
52
+ if (!nt || seen.has(nt.functionName))
53
+ continue;
54
+ seen.add(nt.functionName);
55
+ nodes.push({
56
+ name: nt.functionName,
57
+ status: nt.variant === 'STUB' ? 'STUB' : 'OK',
58
+ inputs: formatPortList(nt.inputs),
59
+ outputs: formatPortList(nt.outputs),
60
+ });
61
+ }
62
+ const implemented = nodes.filter((n) => n.status === 'OK').length;
63
+ const total = nodes.length;
64
+ // Run draft validation for structural checks
65
+ const validation = validator.validate(ast, { mode: 'draft' });
66
+ const structuralErrors = validation.errors
67
+ .filter((e) => e.code !== 'STUB_NODE')
68
+ .map((e) => e.message);
69
+ const result = {
70
+ name: ast.name,
71
+ implemented,
72
+ total,
73
+ nodes,
74
+ structurallyValid: structuralErrors.length === 0,
75
+ structuralErrors,
76
+ };
77
+ if (json) {
78
+ // eslint-disable-next-line no-console
79
+ console.log(JSON.stringify(result, null, 2));
80
+ }
81
+ else {
82
+ logger.section(`${ast.name}: ${implemented}/${total} nodes implemented`);
83
+ logger.newline();
84
+ const maxNameLen = Math.max(...nodes.map((n) => n.name.length), 0);
85
+ for (const node of nodes) {
86
+ const tag = node.status === 'STUB' ? '[STUB]' : '[OK] ';
87
+ const paddedName = node.name.padEnd(maxNameLen);
88
+ const inputStr = node.inputs.length > 0 ? `inputs: ${node.inputs.join(', ')}` : '';
89
+ const outputStr = node.outputs.length > 0 ? `outputs: ${node.outputs.join(', ')}` : '';
90
+ const arrow = inputStr && outputStr ? ' → ' : '';
91
+ const portsStr = `${inputStr}${arrow}${outputStr}`;
92
+ if (node.status === 'STUB') {
93
+ logger.warn(` ${paddedName} ${tag} ${portsStr}`);
94
+ }
95
+ else {
96
+ logger.success(` ${paddedName} ${tag} ${portsStr}`);
97
+ }
98
+ }
99
+ if (structuralErrors.length > 0) {
100
+ logger.newline();
101
+ logger.error(`Structural errors (${structuralErrors.length}):`);
102
+ structuralErrors.forEach((e) => logger.error(` - ${e}`));
103
+ }
104
+ else {
105
+ logger.newline();
106
+ logger.success('Graph structure is valid.');
107
+ }
108
+ }
109
+ }
110
+ catch (error) {
111
+ if (json) {
112
+ // eslint-disable-next-line no-console
113
+ console.log(JSON.stringify({ error: getErrorMessage(error) }));
114
+ }
115
+ else {
116
+ logger.error(`Status failed: ${getErrorMessage(error)}`);
117
+ }
118
+ process.exit(1);
119
+ }
120
+ }
121
+ //# sourceMappingURL=status.js.map