@synergenius/flow-weaver 0.21.17 → 0.21.19

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.
@@ -9671,7 +9671,7 @@ var VERSION;
9671
9671
  var init_generated_version = __esm({
9672
9672
  "src/generated-version.ts"() {
9673
9673
  "use strict";
9674
- VERSION = "0.21.17";
9674
+ VERSION = "0.21.19";
9675
9675
  }
9676
9676
  });
9677
9677
 
@@ -39494,6 +39494,45 @@ Add '@param ${fromPort}' to the workflow JSDoc and include it in the params obje
39494
39494
  }
39495
39495
  }
39496
39496
  }
39497
+ const parentOf = /* @__PURE__ */ new Map();
39498
+ for (const inst of workflow.instances) {
39499
+ parentOf.set(inst.id, inst.parent ?? void 0);
39500
+ }
39501
+ const scopedParentIds = /* @__PURE__ */ new Set();
39502
+ for (const inst of workflow.instances) {
39503
+ const nt = instanceMap.get(inst.id);
39504
+ if (!nt) continue;
39505
+ const hasScopedPorts = [
39506
+ ...Object.values(nt.inputs ?? {}),
39507
+ ...Object.values(nt.outputs ?? {})
39508
+ ].some((p) => p.scope);
39509
+ if (hasScopedPorts) scopedParentIds.add(inst.id);
39510
+ }
39511
+ for (const conn of workflow.connections) {
39512
+ if (conn.from.scope || conn.to.scope) continue;
39513
+ const fromParent = parentOf.get(conn.from.node);
39514
+ const toParent = parentOf.get(conn.to.node);
39515
+ if (!parentOf.has(conn.from.node) || !parentOf.has(conn.to.node)) continue;
39516
+ if (!fromParent && !toParent) continue;
39517
+ const fromInScopedParent = fromParent && scopedParentIds.has(fromParent.id);
39518
+ const toInScopedParent = toParent && scopedParentIds.has(toParent.id);
39519
+ if (!fromInScopedParent && !toInScopedParent) continue;
39520
+ const fromIsParentOfTo = toParent && toParent.id === conn.from.node;
39521
+ const toIsParentOfFrom = fromParent && fromParent.id === conn.to.node;
39522
+ if (fromIsParentOfTo || toIsParentOfFrom) continue;
39523
+ const sameScope = fromParent && toParent && fromParent.id === toParent.id && fromParent.scope === toParent.scope;
39524
+ if (!sameScope) {
39525
+ const fromCtx = fromParent ? `${fromParent.id}.${fromParent.scope}` : "root";
39526
+ const toCtx = toParent ? `${toParent.id}.${toParent.scope}` : "root";
39527
+ this.errors.push({
39528
+ type: "error",
39529
+ code: "CROSS_SCOPE_CONNECTION",
39530
+ message: `Connection from "${conn.from.node}.${conn.from.port}" (in ${fromCtx}) to "${conn.to.node}.${conn.to.port}" (in ${toCtx}) crosses scope boundaries. Nodes in different scopes cannot connect directly.`,
39531
+ connection: conn,
39532
+ location: this.getConnectionLocation(conn)
39533
+ });
39534
+ }
39535
+ }
39497
39536
  }
39498
39537
  // ── H: Duplicate instance IDs ──────────────────────────────────────────
39499
39538
  validateDuplicateInstanceIds(workflow) {
@@ -93231,7 +93270,7 @@ function displayInstalledPackage(pkg) {
93231
93270
  // src/cli/index.ts
93232
93271
  init_logger();
93233
93272
  init_error_utils();
93234
- var version2 = true ? "0.21.17" : "0.0.0-dev";
93273
+ var version2 = true ? "0.21.19" : "0.0.0-dev";
93235
93274
  var program2 = new Command();
93236
93275
  program2.name("fw").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
93237
93276
  logger.banner(version2);
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.21.17";
1
+ export declare const VERSION = "0.21.19";
2
2
  //# sourceMappingURL=generated-version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by scripts/generate-version.ts — do not edit manually
2
- export const VERSION = '0.21.17';
2
+ export const VERSION = '0.21.19';
3
3
  //# sourceMappingURL=generated-version.js.map
@@ -32,6 +32,7 @@ export type TNpmNodeType = {
32
32
  };
33
33
  /**
34
34
  * Get list of packages that have TypeScript declarations (.d.ts files).
35
+ * Only includes direct dependencies from package.json (not transitive or dev).
35
36
  * Excludes @types/* packages as they are type augmentations.
36
37
  *
37
38
  * @param workdir - Directory to start searching from
@@ -11,64 +11,34 @@ import { extractFunctionLikes } from './function-like.js';
11
11
  import { inferDataTypeFromTS } from './type-mappings.js';
12
12
  import { getSharedProject } from './shared-project.js';
13
13
  /**
14
- * Find all node_modules directories starting from fromDir and walking up.
14
+ * Read direct dependency names from the closest package.json.
15
+ * Only includes `dependencies` (not devDependencies, peerDependencies, etc.)
16
+ * since those are the packages available at runtime in workflows.
15
17
  */
16
- function findNodeModulesDirs(fromDir) {
17
- const dirs = [];
18
- let current = path.resolve(fromDir);
18
+ function readDirectDependencies(workdir) {
19
+ let current = path.resolve(workdir);
19
20
  const root = path.parse(current).root;
20
21
  while (current !== root) {
21
- const candidate = path.join(current, 'node_modules');
22
- if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
23
- dirs.push(candidate);
22
+ const pkgJsonPath = path.join(current, 'package.json');
23
+ if (fs.existsSync(pkgJsonPath)) {
24
+ try {
25
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
26
+ return Object.keys(pkgJson.dependencies ?? {});
27
+ }
28
+ catch {
29
+ // malformed package.json, keep walking
30
+ }
24
31
  }
25
32
  const parent = path.dirname(current);
26
33
  if (parent === current)
27
34
  break;
28
35
  current = parent;
29
36
  }
30
- return dirs;
31
- }
32
- /**
33
- * List all packages in a node_modules directory (including scoped packages).
34
- */
35
- function listPackagesInNodeModules(nmDir) {
36
- const packages = [];
37
- if (!fs.existsSync(nmDir))
38
- return packages;
39
- try {
40
- const entries = fs.readdirSync(nmDir, { withFileTypes: true });
41
- for (const entry of entries) {
42
- if (!entry.isDirectory())
43
- continue;
44
- // Handle scoped packages (@scope/pkg)
45
- if (entry.name.startsWith('@')) {
46
- const scopeDir = path.join(nmDir, entry.name);
47
- try {
48
- const scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
49
- for (const scopedEntry of scopedEntries) {
50
- if (scopedEntry.isDirectory()) {
51
- packages.push(`${entry.name}/${scopedEntry.name}`);
52
- }
53
- }
54
- }
55
- catch {
56
- // Ignore permission errors
57
- }
58
- }
59
- else {
60
- // Regular package
61
- packages.push(entry.name);
62
- }
63
- }
64
- }
65
- catch {
66
- // Ignore permission errors
67
- }
68
- return packages;
37
+ return [];
69
38
  }
70
39
  /**
71
40
  * Get list of packages that have TypeScript declarations (.d.ts files).
41
+ * Only includes direct dependencies from package.json (not transitive or dev).
72
42
  * Excludes @types/* packages as they are type augmentations.
73
43
  *
74
44
  * @param workdir - Directory to start searching from
@@ -76,40 +46,26 @@ function listPackagesInNodeModules(nmDir) {
76
46
  * @returns Object with packages array, each containing name and typesPath
77
47
  */
78
48
  export function getTypedPackages(workdir, nodeModulesOverride) {
79
- const nodeModulesDirs = nodeModulesOverride
80
- ? [nodeModulesOverride]
81
- : findNodeModulesDirs(workdir);
49
+ const directDeps = readDirectDependencies(workdir);
82
50
  const typed = [];
83
- const seenPackages = new Set();
84
- for (const nmDir of nodeModulesDirs) {
85
- const packages = listPackagesInNodeModules(nmDir);
86
- for (const pkg of packages) {
87
- // Skip @types/* packages - they are type augmentations, not actual packages
88
- if (pkg.startsWith('@types/'))
89
- continue;
90
- // Skip if already seen (from a closer node_modules)
91
- if (seenPackages.has(pkg))
92
- continue;
93
- seenPackages.add(pkg);
94
- // Check if package has types
95
- const typesPath = resolvePackageTypesPath(pkg, workdir, nodeModulesOverride);
96
- if (typesPath) {
97
- typed.push({ name: pkg, typesPath });
98
- }
51
+ for (const pkg of directDeps) {
52
+ if (pkg.startsWith('@types/'))
53
+ continue;
54
+ const typesPath = resolvePackageTypesPath(pkg, workdir, nodeModulesOverride);
55
+ if (typesPath) {
56
+ typed.push({ name: pkg, typesPath });
99
57
  }
100
58
  }
101
59
  return { packages: typed };
102
60
  }
103
- /**
104
- * Capitalize first letter of a string.
105
- */
61
+ const PRIMITIVE_TYPES = new Set(['string', 'number', 'boolean', 'any', 'unknown', 'never']);
106
62
  function capitalize(str) {
107
63
  return str.charAt(0).toUpperCase() + str.slice(1);
108
64
  }
109
65
  /**
110
66
  * Infer node type from a function declaration in a .d.ts file.
111
67
  */
112
- function inferNodeTypeFromDtsFunction(fn, packageName, _dtsPath) {
68
+ function inferNodeTypeFromDtsFunction(fn, packageName) {
113
69
  const fnName = fn.getName();
114
70
  if (!fnName)
115
71
  return null;
@@ -149,8 +105,7 @@ function inferNodeTypeFromDtsFunction(fn, packageName, _dtsPath) {
149
105
  }
150
106
  const unwrappedText = returnType.getText();
151
107
  if (unwrappedText !== 'void' && unwrappedText !== 'undefined') {
152
- const primitiveTypes = new Set(['string', 'number', 'boolean', 'any', 'unknown', 'never']);
153
- const isPrimitive = primitiveTypes.has(unwrappedText);
108
+ const isPrimitive = PRIMITIVE_TYPES.has(unwrappedText);
154
109
  const isArray = unwrappedText.endsWith('[]') || unwrappedText.startsWith('Array<');
155
110
  const properties = returnType.getProperties();
156
111
  const isObjectLike = !isPrimitive && !isArray && returnType.isObject() && properties.length > 0;
@@ -239,7 +194,7 @@ export function getPackageExports(packageName, workdir, nodeModulesOverride) {
239
194
  if (!fnName || seenFunctionNames.has(fnName))
240
195
  continue;
241
196
  seenFunctionNames.add(fnName);
242
- const nodeType = inferNodeTypeFromDtsFunction(fn, packageName, typesPath);
197
+ const nodeType = inferNodeTypeFromDtsFunction(fn, packageName);
243
198
  if (nodeType) {
244
199
  nodeTypes.push(nodeType);
245
200
  }
package/dist/validator.js CHANGED
@@ -1374,6 +1374,67 @@ export class WorkflowValidator {
1374
1374
  }
1375
1375
  }
1376
1376
  }
1377
+ // Check: non-scoped connections must not cross scope boundaries
1378
+ // when the parent is a scoped node type (has scoped ports).
1379
+ // A child in scope "a" cannot connect to a child in scope "b" (or root),
1380
+ // because they execute in separate callback contexts.
1381
+ // Only applies to per-port scoped parents (not flat grouping scopes).
1382
+ const parentOf = new Map();
1383
+ for (const inst of workflow.instances) {
1384
+ parentOf.set(inst.id, inst.parent ?? undefined);
1385
+ }
1386
+ // Build set of node IDs that are scoped parent types (have scoped ports)
1387
+ const scopedParentIds = new Set();
1388
+ for (const inst of workflow.instances) {
1389
+ const nt = instanceMap.get(inst.id);
1390
+ if (!nt)
1391
+ continue;
1392
+ const hasScopedPorts = [
1393
+ ...Object.values(nt.inputs ?? {}),
1394
+ ...Object.values(nt.outputs ?? {}),
1395
+ ].some((p) => p.scope);
1396
+ if (hasScopedPorts)
1397
+ scopedParentIds.add(inst.id);
1398
+ }
1399
+ for (const conn of workflow.connections) {
1400
+ // Skip scoped connections (already validated above)
1401
+ if (conn.from.scope || conn.to.scope)
1402
+ continue;
1403
+ const fromParent = parentOf.get(conn.from.node);
1404
+ const toParent = parentOf.get(conn.to.node);
1405
+ // Both must exist in the workflow
1406
+ if (!parentOf.has(conn.from.node) || !parentOf.has(conn.to.node))
1407
+ continue;
1408
+ // If neither node is inside a scope, skip (root-to-root is fine)
1409
+ if (!fromParent && !toParent)
1410
+ continue;
1411
+ // Only validate when the parent is a scoped node type (has scoped ports).
1412
+ // Flat grouping scopes (createScope without scoped ports) are allowed to cross.
1413
+ const fromInScopedParent = fromParent && scopedParentIds.has(fromParent.id);
1414
+ const toInScopedParent = toParent && scopedParentIds.has(toParent.id);
1415
+ if (!fromInScopedParent && !toInScopedParent)
1416
+ continue;
1417
+ // Allow connections where one side is the scoped PARENT itself (not a child)
1418
+ const fromIsParentOfTo = toParent && toParent.id === conn.from.node;
1419
+ const toIsParentOfFrom = fromParent && fromParent.id === conn.to.node;
1420
+ if (fromIsParentOfTo || toIsParentOfFrom)
1421
+ continue;
1422
+ // Check if they share the same parent+scope
1423
+ const sameScope = fromParent && toParent &&
1424
+ fromParent.id === toParent.id &&
1425
+ fromParent.scope === toParent.scope;
1426
+ if (!sameScope) {
1427
+ const fromCtx = fromParent ? `${fromParent.id}.${fromParent.scope}` : 'root';
1428
+ const toCtx = toParent ? `${toParent.id}.${toParent.scope}` : 'root';
1429
+ this.errors.push({
1430
+ type: 'error',
1431
+ code: 'CROSS_SCOPE_CONNECTION',
1432
+ message: `Connection from "${conn.from.node}.${conn.from.port}" (in ${fromCtx}) to "${conn.to.node}.${conn.to.port}" (in ${toCtx}) crosses scope boundaries. Nodes in different scopes cannot connect directly.`,
1433
+ connection: conn,
1434
+ location: this.getConnectionLocation(conn),
1435
+ });
1436
+ }
1437
+ }
1377
1438
  }
1378
1439
  // ── H: Duplicate instance IDs ──────────────────────────────────────────
1379
1440
  validateDuplicateInstanceIds(workflow) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver",
3
- "version": "0.21.17",
3
+ "version": "0.21.19",
4
4
  "description": "Deterministic workflow compiler for AI agents. Compiles to standalone TypeScript, no runtime dependencies.",
5
5
  "private": false,
6
6
  "type": "module",