@output.ai/core 0.2.4 → 0.3.0-dev.pr263-8f8e94a

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.
@@ -26,6 +26,14 @@ import {
26
26
  } from '@babel/types';
27
27
  import { ComponentFile, NodeType } from './consts.js';
28
28
 
29
+ // Path pattern regexes - shared across multiple helper functions
30
+ const STEPS_FILE_REGEX = /(^|\/)steps\.js$/;
31
+ const STEPS_FOLDER_REGEX = /\/steps\/[^/]+\.js$/;
32
+ const EVALUATORS_FILE_REGEX = /(^|\/)evaluators\.js$/;
33
+ const EVALUATORS_FOLDER_REGEX = /\/evaluators\/[^/]+\.js$/;
34
+ const PATH_TRAVERSAL_REGEX = /\.\.\//;
35
+ const SHARED_PATH_REGEX = /\/shared\//;
36
+
29
37
  /**
30
38
  * Resolve a relative module specifier against a base directory.
31
39
  * @param {string} fileDir - Base directory to resolve from.
@@ -104,25 +112,67 @@ export const toFunctionExpression = arrow => {
104
112
  };
105
113
 
106
114
  /**
107
- * Check if a module specifier or request string points to steps.js.
115
+ * Check if a module specifier or request string points to steps.js or is in a steps folder.
116
+ * Matches: steps.js, /steps.js, /steps/*.js
117
+ * This matches LOCAL steps only (no path traversal).
118
+ * @param {string} value - Module path or request string.
119
+ * @returns {boolean} True if it matches a local steps path.
120
+ */
121
+ export const isStepsPath = value => {
122
+ // Exclude shared steps (paths with ../ or containing /shared/)
123
+ if ( PATH_TRAVERSAL_REGEX.test( value ) || SHARED_PATH_REGEX.test( value ) ) {
124
+ return false;
125
+ }
126
+ return STEPS_FILE_REGEX.test( value ) || STEPS_FOLDER_REGEX.test( value );
127
+ };
128
+
129
+ /**
130
+ * Check if a module specifier or request string points to shared steps.
131
+ * Shared steps are steps imported from outside the current workflow directory.
132
+ * Matches paths with ../ traversal or /shared/ and containing steps pattern.
133
+ * @param {string} value - Module path or request string.
134
+ * @returns {boolean} True if it matches a shared steps path.
135
+ */
136
+ export const isSharedStepsPath = value => {
137
+ const hasStepsPattern = STEPS_FILE_REGEX.test( value ) || STEPS_FOLDER_REGEX.test( value );
138
+ if ( !hasStepsPattern ) {
139
+ return false;
140
+ }
141
+ return PATH_TRAVERSAL_REGEX.test( value ) || SHARED_PATH_REGEX.test( value );
142
+ };
143
+
144
+ /**
145
+ * Check if a path matches any steps pattern (local or shared).
146
+ * Used for validation purposes.
108
147
  * @param {string} value - Module path or request string.
109
- * @returns {boolean} True if it matches steps.js.
148
+ * @returns {boolean} True if it matches any steps path pattern.
110
149
  */
111
- export const isStepsPath = value => /(^|\/)steps\.js$/.test( value );
150
+ export const isAnyStepsPath = value =>
151
+ STEPS_FILE_REGEX.test( value ) || STEPS_FOLDER_REGEX.test( value );
112
152
 
113
153
  /**
114
- * Check if a module specifier or request string points to shared_steps.js.
154
+ * Check if a module specifier or request string points to evaluators.js or is in an evaluators folder.
155
+ * Matches: evaluators.js, /evaluators.js, /evaluators/*.js
115
156
  * @param {string} value - Module path or request string.
116
- * @returns {boolean} True if it matches shared_steps.js.
157
+ * @returns {boolean} True if it matches an evaluators path.
117
158
  */
118
- export const isSharedStepsPath = value => /(^|\/)shared_steps\.js$/.test( value );
159
+ export const isEvaluatorsPath = value =>
160
+ EVALUATORS_FILE_REGEX.test( value ) || EVALUATORS_FOLDER_REGEX.test( value );
119
161
 
120
162
  /**
121
- * Check if a module specifier or request string points to evaluators.js.
163
+ * Check if a module specifier or request string points to shared evaluators.
164
+ * Shared evaluators are evaluators imported from outside the current workflow directory.
165
+ * Matches paths with ../ traversal or /shared/ and containing evaluators pattern.
122
166
  * @param {string} value - Module path or request string.
123
- * @returns {boolean} True if it matches evaluators.js.
167
+ * @returns {boolean} True if it matches a shared evaluators path.
124
168
  */
125
- export const isEvaluatorsPath = value => /(^|\/)evaluators\.js$/.test( value );
169
+ export const isSharedEvaluatorsPath = value => {
170
+ const hasEvaluatorsPattern = EVALUATORS_FILE_REGEX.test( value ) || EVALUATORS_FOLDER_REGEX.test( value );
171
+ if ( !hasEvaluatorsPattern ) {
172
+ return false;
173
+ }
174
+ return PATH_TRAVERSAL_REGEX.test( value ) || SHARED_PATH_REGEX.test( value );
175
+ };
126
176
 
127
177
  /**
128
178
  * Check if a module specifier or request string points to workflow.js.
@@ -132,24 +182,23 @@ export const isEvaluatorsPath = value => /(^|\/)evaluators\.js$/.test( value );
132
182
  export const isWorkflowPath = value => /(^|\/)workflow\.js$/.test( value );
133
183
 
134
184
  /**
135
- * Check if a module specifier or request string points to types.js.
185
+ * Check if a path is a component file (steps, evaluators, or workflow).
136
186
  * @param {string} value - Module path or request string.
137
- * @returns {boolean} True if it matches types.js.
187
+ * @returns {boolean} True if it matches any component file path.
138
188
  */
139
- export const isTypesPath = value => /(^|\/)types\.js$/.test( value );
189
+ export const isComponentFile = value =>
190
+ isAnyStepsPath( value ) || isEvaluatorsPath( value ) || isWorkflowPath( value );
140
191
 
141
192
  /**
142
193
  * Determine file kind based on its path.
143
- * @param {string} filename
144
- * @returns {'workflow'|'steps'|'shared_steps'|'evaluators'|null}
194
+ * Returns the component type if it's a component file, null otherwise.
195
+ * @param {string} path
196
+ * @returns {'workflow'|'steps'|'evaluators'|null}
145
197
  */
146
198
  export const getFileKind = path => {
147
- if ( isStepsPath( path ) ) {
199
+ if ( isAnyStepsPath( path ) ) {
148
200
  return ComponentFile.STEPS;
149
201
  }
150
- if ( isSharedStepsPath( path ) ) {
151
- return ComponentFile.SHARED_STEPS;
152
- }
153
202
  if ( isEvaluatorsPath( path ) ) {
154
203
  return ComponentFile.EVALUATORS;
155
204
  }
@@ -278,12 +327,12 @@ export const buildStepsNameMap = ( path, cache ) => buildComponentNameMap( {
278
327
 
279
328
  /**
280
329
  * Build a map from exported shared step identifier to declared step name.
281
- * Parses `shared_steps.js` for `export const X = step({ name: '...' })`.
282
- * Uses the same factory as regular steps.
330
+ * Same as buildStepsNameMap but for shared steps.
283
331
  *
284
332
  * @param {string} path - Absolute path to the shared steps module file.
285
- * @param {Map<string, Map<string,string>>} cache - Cache of computed name maps.
333
+ * @param {Map<string, Map<string,string>>} cache - Cache of computed step name maps.
286
334
  * @returns {Map<string,string>} Exported identifier -> step name.
335
+ * @throws {Error} When a step name is invalid (non-static or missing).
287
336
  */
288
337
  export const buildSharedStepsNameMap = ( path, cache ) => buildComponentNameMap( {
289
338
  path,
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
2
+ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join, resolve as resolvePath } from 'node:path';
5
5
  import * as t from '@babel/types';
@@ -12,7 +12,9 @@ import {
12
12
  toFunctionExpression,
13
13
  isStepsPath,
14
14
  isSharedStepsPath,
15
+ isAnyStepsPath,
15
16
  isEvaluatorsPath,
17
+ isSharedEvaluatorsPath,
16
18
  isWorkflowPath,
17
19
  createThisMethodCall,
18
20
  resolveNameFromOptions,
@@ -137,14 +139,49 @@ describe( 'workflow_rewriter tools', () => {
137
139
  expect( t.isFunctionExpression( fn2 ) ).toBe( true );
138
140
  } );
139
141
 
140
- it( 'isStepsPath: matches steps.js at root or subpath', () => {
142
+ it( 'isStepsPath: matches LOCAL steps.js (no path traversal)', () => {
143
+ // Local steps (without ../ or /shared/)
141
144
  expect( isStepsPath( 'steps.js' ) ).toBe( true );
142
145
  expect( isStepsPath( './steps.js' ) ).toBe( true );
143
146
  expect( isStepsPath( '/a/b/steps.js' ) ).toBe( true );
147
+ expect( isStepsPath( './steps/fetch.js' ) ).toBe( true );
148
+ // Shared steps (with ../ or /shared/) should NOT match isStepsPath
149
+ expect( isStepsPath( '../steps.js' ) ).toBe( false );
150
+ expect( isStepsPath( '../../shared/steps/common.js' ) ).toBe( false );
151
+ // Non-steps
144
152
  expect( isStepsPath( 'steps.ts' ) ).toBe( false );
145
153
  expect( isStepsPath( 'workflow.js' ) ).toBe( false );
146
154
  } );
147
155
 
156
+ it( 'isSharedStepsPath: matches steps imported from outside workflow directory', () => {
157
+ // Shared steps: must have steps pattern AND have path traversal or /shared/
158
+ expect( isSharedStepsPath( '../steps.js' ) ).toBe( true );
159
+ expect( isSharedStepsPath( '../../steps.js' ) ).toBe( true );
160
+ expect( isSharedStepsPath( '../../shared/steps/common.js' ) ).toBe( true );
161
+ expect( isSharedStepsPath( '../other_workflow/steps.js' ) ).toBe( true );
162
+ expect( isSharedStepsPath( '/src/shared/steps/common.js' ) ).toBe( true );
163
+ // Local steps (no traversal, no /shared/) should NOT match
164
+ expect( isSharedStepsPath( './steps.js' ) ).toBe( false );
165
+ expect( isSharedStepsPath( 'steps.js' ) ).toBe( false );
166
+ expect( isSharedStepsPath( './steps/fetch.js' ) ).toBe( false );
167
+ // Non-steps should NOT match
168
+ expect( isSharedStepsPath( '../utils.js' ) ).toBe( false );
169
+ expect( isSharedStepsPath( 'evaluators.js' ) ).toBe( false );
170
+ } );
171
+
172
+ it( 'isAnyStepsPath: matches any steps pattern (local or shared)', () => {
173
+ // Local steps
174
+ expect( isAnyStepsPath( 'steps.js' ) ).toBe( true );
175
+ expect( isAnyStepsPath( './steps.js' ) ).toBe( true );
176
+ expect( isAnyStepsPath( './steps/fetch.js' ) ).toBe( true );
177
+ // Shared steps
178
+ expect( isAnyStepsPath( '../steps.js' ) ).toBe( true );
179
+ expect( isAnyStepsPath( '../../shared/steps/common.js' ) ).toBe( true );
180
+ // Non-steps
181
+ expect( isAnyStepsPath( 'workflow.js' ) ).toBe( false );
182
+ expect( isAnyStepsPath( 'utils.js' ) ).toBe( false );
183
+ } );
184
+
148
185
  it( 'isWorkflowPath: matches workflow.js at root or subpath', () => {
149
186
  expect( isWorkflowPath( 'workflow.js' ) ).toBe( true );
150
187
  expect( isWorkflowPath( './workflow.js' ) ).toBe( true );
@@ -153,22 +190,31 @@ describe( 'workflow_rewriter tools', () => {
153
190
  expect( isWorkflowPath( 'steps.js' ) ).toBe( false );
154
191
  } );
155
192
 
156
- it( 'isSharedStepsPath: matches shared_steps.js at root or subpath', () => {
157
- expect( isSharedStepsPath( 'shared_steps.js' ) ).toBe( true );
158
- expect( isSharedStepsPath( './shared_steps.js' ) ).toBe( true );
159
- expect( isSharedStepsPath( '/a/b/shared_steps.js' ) ).toBe( true );
160
- expect( isSharedStepsPath( 'shared_steps.ts' ) ).toBe( false );
161
- expect( isSharedStepsPath( 'evaluators.js' ) ).toBe( false );
162
- } );
163
-
164
193
  it( 'isEvaluatorsPath: matches evaluators.js at root or subpath', () => {
165
194
  expect( isEvaluatorsPath( 'evaluators.js' ) ).toBe( true );
166
195
  expect( isEvaluatorsPath( './evaluators.js' ) ).toBe( true );
167
196
  expect( isEvaluatorsPath( '/a/b/evaluators.js' ) ).toBe( true );
197
+ expect( isEvaluatorsPath( './evaluators/quality.js' ) ).toBe( true );
168
198
  expect( isEvaluatorsPath( 'evaluators.ts' ) ).toBe( false );
169
199
  expect( isEvaluatorsPath( 'steps.js' ) ).toBe( false );
170
200
  } );
171
201
 
202
+ it( 'isSharedEvaluatorsPath: matches evaluators imported from outside workflow directory', () => {
203
+ // Shared evaluators: must have evaluators pattern AND have path traversal or /shared/
204
+ expect( isSharedEvaluatorsPath( '../evaluators.js' ) ).toBe( true );
205
+ expect( isSharedEvaluatorsPath( '../../evaluators.js' ) ).toBe( true );
206
+ expect( isSharedEvaluatorsPath( '../../shared/evaluators/quality.js' ) ).toBe( true );
207
+ expect( isSharedEvaluatorsPath( '../other_workflow/evaluators.js' ) ).toBe( true );
208
+ expect( isSharedEvaluatorsPath( '/src/shared/evaluators/quality.js' ) ).toBe( true );
209
+ // Local evaluators (no traversal, no /shared/) should NOT match
210
+ expect( isSharedEvaluatorsPath( './evaluators.js' ) ).toBe( false );
211
+ expect( isSharedEvaluatorsPath( 'evaluators.js' ) ).toBe( false );
212
+ expect( isSharedEvaluatorsPath( './evaluators/quality.js' ) ).toBe( false );
213
+ // Non-evaluators should NOT match
214
+ expect( isSharedEvaluatorsPath( '../utils.js' ) ).toBe( false );
215
+ expect( isSharedEvaluatorsPath( 'steps.js' ) ).toBe( false );
216
+ } );
217
+
172
218
  it( 'createThisMethodCall: builds this.method(\'name\', ...args) call', () => {
173
219
  const call = createThisMethodCall( 'invoke', 'n', [ t.numericLiteral( 1 ), t.identifier( 'x' ) ] );
174
220
  expect( t.isCallExpression( call ) ).toBe( true );
@@ -179,9 +225,10 @@ describe( 'workflow_rewriter tools', () => {
179
225
  expect( call.arguments.length ).toBe( 3 );
180
226
  } );
181
227
 
182
- it( 'buildSharedStepsNameMap: reads names from shared_steps module and caches result', () => {
228
+ it( 'buildSharedStepsNameMap: reads names from shared steps module and caches result', () => {
183
229
  const dir = mkdtempSync( join( tmpdir(), 'tools-shared-steps-' ) );
184
- const stepsPath = join( dir, 'shared_steps.js' );
230
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
231
+ const stepsPath = join( dir, 'shared', 'steps', 'common.js' );
185
232
  writeFileSync( stepsPath, [
186
233
  'export const StepA = step({ name: "shared.step.a" })',
187
234
  'export const StepB = step({ name: "shared.step.b" })'
@@ -199,9 +246,13 @@ describe( 'workflow_rewriter tools', () => {
199
246
  it( 'getFileKind: classifies file by its path', () => {
200
247
  expect( getFileKind( '/p/workflow.js' ) ).toBe( 'workflow' );
201
248
  expect( getFileKind( '/p/steps.js' ) ).toBe( 'steps' );
202
- expect( getFileKind( '/p/shared_steps.js' ) ).toBe( 'shared_steps' );
249
+ // Files in steps folder are steps
250
+ expect( getFileKind( '/p/steps/fetch.js' ) ).toBe( 'steps' );
251
+ expect( getFileKind( '/p/shared/steps/common.js' ) ).toBe( 'steps' );
203
252
  expect( getFileKind( '/p/evaluators.js' ) ).toBe( 'evaluators' );
253
+ expect( getFileKind( '/p/evaluators/quality.js' ) ).toBe( 'evaluators' );
204
254
  expect( getFileKind( '/p/other.js' ) ).toBe( null );
255
+ expect( getFileKind( '/p/utils.js' ) ).toBe( null );
256
+ expect( getFileKind( '/p/clients/api.js' ) ).toBe( null );
205
257
  } );
206
258
  } );
207
-
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
2
+ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import loader from './index.mjs';
@@ -48,12 +48,15 @@ const obj = {
48
48
  rmSync( dir, { recursive: true, force: true } );
49
49
  } );
50
50
 
51
- it( 'rewrites ESM shared_steps imports to invokeSharedStep', async () => {
51
+ it( 'rewrites ESM shared steps imports to invokeSharedStep', async () => {
52
+ // Create directory structure: shared/steps/common.js
52
53
  const dir = mkdtempSync( join( tmpdir(), 'ast-loader-esm-shared-' ) );
53
- writeFileSync( join( dir, 'shared_steps.js' ), 'export const SharedA = step({ name: \'shared.a\' });' );
54
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
55
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
56
+ writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const SharedA = step({ name: \'shared.a\' });' );
54
57
 
55
58
  const source = `
56
- import { SharedA } from './shared_steps.js';
59
+ import { SharedA } from '../../shared/steps/common.js';
57
60
 
58
61
  const obj = {
59
62
  fn: async (x) => {
@@ -61,21 +64,24 @@ const obj = {
61
64
  }
62
65
  }`;
63
66
 
64
- const { code } = await runLoader( source, join( dir, 'file.js' ) );
67
+ const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
65
68
 
66
- expect( code ).not.toMatch( /from '\.\/shared_steps\.js'/ );
69
+ expect( code ).not.toMatch( /from '\.\.\/\.\.\/shared\/steps\/common\.js'/ );
67
70
  expect( code ).toMatch( /fn:\s*async function \(x\)/ );
68
71
  expect( code ).toMatch( /this\.invokeSharedStep\('shared\.a',\s*1\)/ );
69
72
 
70
73
  rmSync( dir, { recursive: true, force: true } );
71
74
  } );
72
75
 
73
- it( 'rewrites CJS shared_steps requires to invokeSharedStep', async () => {
76
+ it( 'rewrites CJS shared steps requires to invokeSharedStep', async () => {
77
+ // Create directory structure: shared/steps/common.js
74
78
  const dir = mkdtempSync( join( tmpdir(), 'ast-loader-cjs-shared-' ) );
75
- writeFileSync( join( dir, 'shared_steps.js' ), 'export const SharedB = step({ name: \'shared.b\' });' );
79
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
80
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
81
+ writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const SharedB = step({ name: \'shared.b\' });' );
76
82
 
77
83
  const source = `
78
- const { SharedB } = require( './shared_steps.js' );
84
+ const { SharedB } = require( '../../shared/steps/common.js' );
79
85
 
80
86
  const obj = {
81
87
  fn: async (y) => {
@@ -83,9 +89,9 @@ const obj = {
83
89
  }
84
90
  }`;
85
91
 
86
- const { code } = await runLoader( source, join( dir, 'file.js' ) );
92
+ const { code } = await runLoader( source, join( dir, 'workflows', 'my_workflow', 'workflow.js' ) );
87
93
 
88
- expect( code ).not.toMatch( /require\('\.\/shared_steps\.js'\)/ );
94
+ expect( code ).not.toMatch( /require\('\.\.\/\.\.\/shared\/steps\/common\.js'\)/ );
89
95
  expect( code ).toMatch( /fn:\s*async function \(y\)/ );
90
96
  expect( code ).toMatch( /this\.invokeSharedStep\('shared\.b'\)/ );
91
97
 
@@ -1,7 +1,7 @@
1
1
  import traverseModule from '@babel/traverse';
2
2
  import { dirname } from 'node:path';
3
- import { parse, toAbsolutePath, getFileKind } from '../tools.js';
4
- import { ComponentFile } from '../consts.js';
3
+ import { parse, toAbsolutePath, getFileKind, isAnyStepsPath, isEvaluatorsPath, isWorkflowPath } from '../tools.js';
4
+ import { ComponentFile, CoreModule } from '../consts.js';
5
5
  import {
6
6
  isCallExpression,
7
7
  isFunctionExpression,
@@ -18,21 +18,92 @@ import {
18
18
  const traverse = traverseModule.default ?? traverseModule;
19
19
 
20
20
  /**
21
- * Check if evaluators, steps or shared_steps import invalid dependencies
21
+ * Determine the file kind label for error messages.
22
+ * Handles both flat files (steps.js) and folder-based files (steps/fetch_data.js).
23
+ * @param {string} filename - The file path
24
+ * @returns {string} Human-readable file kind for error messages
22
25
  */
23
- const validateStepEvaluatorImports = ( { fileKind, specifier, filename } ) => {
24
- if ( Object.values( ComponentFile ).includes( getFileKind( specifier ) ) ) {
25
- throw new Error( `Invalid dependency in ${fileKind}.js: '${specifier}'. \
26
- Steps, shared_steps, evaluators or workflows are not allowed dependencies in ${filename}` );
26
+ const getFileKindLabel = filename => {
27
+ if ( isAnyStepsPath( filename ) ) {
28
+ return 'steps.js';
27
29
  }
30
+ if ( isEvaluatorsPath( filename ) ) {
31
+ return 'evaluators.js';
32
+ }
33
+ if ( /workflow\.js$/.test( filename ) ) {
34
+ return 'workflow.js';
35
+ }
36
+ return filename;
28
37
  };
29
38
 
30
39
  /**
31
- * Validate import for evaluators, steps, shared_steps
40
+ * Check if workflow dependencies are valid.
41
+ * Workflows can import:
42
+ * - Components (steps, evaluators, workflow)
43
+ * - Core modules (@output.ai/core, local_core)
44
+ * - ANY file that is NOT a component file (flexible utility imports)
45
+ */
46
+ const validateWorkflowImports = ( { specifier, filename } ) => {
47
+ const isCore = Object.values( CoreModule ).includes( specifier );
48
+ const fileKind = getFileKind( specifier );
49
+ const isComponent = Object.values( ComponentFile ).includes( fileKind );
50
+ const isNonComponentFile = fileKind === null;
51
+
52
+ if ( !isCore && !isComponent && !isNonComponentFile ) {
53
+ throw new Error( `Invalid dependency in workflow.js: '${specifier}'. \
54
+ Only components (${Object.values( ComponentFile ) } ), @output.ai/core, or non-component files are allowed in ${filename}` );
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Check if evaluators or steps import invalid dependencies.
60
+ * Steps and evaluators CANNOT import:
61
+ * - Other steps (local or shared) - activity isolation
62
+ * - Other evaluators (local or shared) - activity isolation
63
+ * - Workflows
64
+ *
65
+ * Steps and evaluators CAN import:
66
+ * - ANY file that is NOT a component file (flexible utility imports)
67
+ */
68
+ const validateStepEvaluatorImports = ( { specifier, filename } ) => {
69
+ const importedFileKind = getFileKind( specifier );
70
+
71
+ // Activity isolation: steps/evaluators cannot import other steps, evaluators, or workflows
72
+ if ( Object.values( ComponentFile ).includes( importedFileKind ) ) {
73
+ const fileLabel = getFileKindLabel( filename );
74
+ throw new Error( `Invalid dependency in ${fileLabel}: '${specifier}'. \
75
+ Steps, evaluators or workflows are not allowed dependencies in ${filename}` );
76
+ }
77
+ };
78
+
79
+ /**
80
+ * Validate import for evaluators, steps, workflow
32
81
  */
33
82
  const executeImportValidations = ( { fileKind, specifier, filename } ) => {
34
- if ( Object.values( ComponentFile ).includes( fileKind ) && fileKind !== ComponentFile.WORKFLOW ) {
35
- validateStepEvaluatorImports( { fileKind, specifier, filename } );
83
+ if ( fileKind === ComponentFile.WORKFLOW ) {
84
+ validateWorkflowImports( { specifier, filename } );
85
+ } else if ( Object.values( ComponentFile ).includes( fileKind ) ) {
86
+ validateStepEvaluatorImports( { specifier, filename } );
87
+ }
88
+ };
89
+
90
+ /**
91
+ * Validate that component instantiation calls occur in the correct file locations.
92
+ * - step() must be called in a file whose path contains 'steps'
93
+ * - evaluator() must be called in a file whose path contains 'evaluators'
94
+ * - workflow() must be called in a file whose path contains 'workflow'
95
+ * @param {string} calleeName - The factory function name (step, evaluator, workflow)
96
+ * @param {string} filename - The file path where the call occurs
97
+ */
98
+ const validateInstantiationLocation = ( calleeName, filename ) => {
99
+ if ( calleeName === 'step' && !isAnyStepsPath( filename ) ) {
100
+ throw new Error( `Invalid instantiation location: step() can only be called in files with 'steps' in the path. Found in: ${filename}` );
101
+ }
102
+ if ( calleeName === 'evaluator' && !isEvaluatorsPath( filename ) ) {
103
+ throw new Error( `Invalid instantiation location: evaluator() can only be called in files with 'evaluators' in the path. Found in: ${filename}` );
104
+ }
105
+ if ( calleeName === 'workflow' && !isWorkflowPath( filename ) ) {
106
+ throw new Error( `Invalid instantiation location: workflow() can only be called in files with 'workflow' in the path. Found in: ${filename}` );
36
107
  }
37
108
  };
38
109
 
@@ -41,12 +112,13 @@ const executeImportValidations = ( { fileKind, specifier, filename } ) => {
41
112
  * Returns the source unchanged unless a validation error is found.
42
113
  *
43
114
  * Rules enforced:
44
- * - evaluators.js `fn`: at each evaluator().fn body: calling any evaluator, step, shared_step or workflow is forbidden
45
- * - evaluators.js: may not import evaluators.js, steps.js/shared_steps.js, workflow.js
46
- * - shared_steps.js `fn`: at each step().fn body: calling any evaluator, step, shared_step or workflow is forbidden
47
- * - shared_steps.js: may not import evaluators.js, steps.js, shared_steps.js, workflow.js
48
- * - steps.js: at each step().fn body: calling any evaluator, step, shared_step or workflow is forbidden
49
- * - steps.js: may not import evaluators.js, steps.js, shared_steps.js, workflow.js
115
+ * - Instantiation location: step() must be in steps path, evaluator() in evaluators path, workflow() in workflow path
116
+ * - evaluators.js `fn`: at each evaluator().fn body: calling any evaluator, step, or workflow is forbidden
117
+ * - evaluators.js: may not import evaluators.js, steps.js, workflow.js, or any shared steps/evaluators
118
+ * - steps.js: at each step().fn body: calling any evaluator, step, or workflow is forbidden
119
+ * - steps.js: may not import evaluators.js, steps.js, workflow.js, or any shared steps/evaluators
120
+ * - workflow.js: may import components (evaluators.js, steps.js, workflow.js including shared);
121
+ * and any non-component file, or `@output.ai/core`
50
122
  *
51
123
  * @param {string|Buffer} source
52
124
  * @param {any} inputMap
@@ -66,7 +138,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
66
138
  // Collect local declarations and imported identifiers by type
67
139
  const localStepIds = new Set();
68
140
  const localEvaluatorIds = new Set();
69
- const importedStepIds = new Set(); // includes shared_steps
141
+ const importedStepIds = new Set();
70
142
  const importedEvaluatorIds = new Set();
71
143
  const importedWorkflowIds = new Set();
72
144
 
@@ -78,12 +150,12 @@ export default function workflowValidatorLoader( source, inputMap ) {
78
150
  executeImportValidations( { fileKind, specifier, filename } );
79
151
 
80
152
  // Collect imported identifiers for later call checks
153
+ const importedKind = getFileKind( specifier );
81
154
  const accumulator = ( {
82
155
  [ComponentFile.STEPS]: importedStepIds,
83
- [ComponentFile.SHARED_STEPS]: importedStepIds,
84
156
  [ComponentFile.EVALUATORS]: importedEvaluatorIds,
85
157
  [ComponentFile.WORKFLOW]: importedWorkflowIds
86
- } )[fileKind];
158
+ } )[importedKind];
87
159
  if ( accumulator ) {
88
160
  for ( const s of path.node.specifiers ) {
89
161
  if ( isImportSpecifier( s ) || isImportDefaultSpecifier( s ) ) {
@@ -98,12 +170,21 @@ export default function workflowValidatorLoader( source, inputMap ) {
98
170
  return;
99
171
  }
100
172
 
101
- // Collect local step/evaluator declarations: const X = step({...}) / evaluator({...})
102
- if ( isIdentifier( init.callee, { name: 'step' } ) && isIdentifier( path.node.id ) ) {
103
- localStepIds.add( path.node.id.name );
173
+ // Validate instantiation location for step/evaluator/workflow calls
174
+ if ( isIdentifier( init.callee, { name: 'step' } ) ) {
175
+ validateInstantiationLocation( 'step', filename );
176
+ if ( isIdentifier( path.node.id ) ) {
177
+ localStepIds.add( path.node.id.name );
178
+ }
179
+ }
180
+ if ( isIdentifier( init.callee, { name: 'evaluator' } ) ) {
181
+ validateInstantiationLocation( 'evaluator', filename );
182
+ if ( isIdentifier( path.node.id ) ) {
183
+ localEvaluatorIds.add( path.node.id.name );
184
+ }
104
185
  }
105
- if ( isIdentifier( init.callee, { name: 'evaluator' } ) && isIdentifier( path.node.id ) ) {
106
- localEvaluatorIds.add( path.node.id.name );
186
+ if ( isIdentifier( init.callee, { name: 'workflow' } ) ) {
187
+ validateInstantiationLocation( 'workflow', filename );
107
188
  }
108
189
 
109
190
  // CommonJS requires: validate source and collect identifiers
@@ -118,7 +199,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
118
199
  // Collect imported identifiers from require patterns
119
200
  if ( isStringLiteral( firstArg ) ) {
120
201
  const reqType = getFileKind( toAbsolutePath( fileDir, req ) );
121
- if ( [ ComponentFile.STEPS, ComponentFile.SHARED_STEPS ].includes( reqType ) && isObjectPattern( path.node.id ) ) {
202
+ if ( reqType === ComponentFile.STEPS && isObjectPattern( path.node.id ) ) {
122
203
  for ( const prop of path.node.id.properties ) {
123
204
  if ( isObjectProperty( prop ) && isIdentifier( prop.value ) ) {
124
205
  importedStepIds.add( prop.value.name );
@@ -141,7 +222,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
141
222
  } );
142
223
 
143
224
  // Function-body call validations for steps/evaluators files
144
- if ( [ ComponentFile.STEPS, ComponentFile.SHARED_STEPS, ComponentFile.EVALUATORS ].includes( fileKind ) ) {
225
+ if ( [ ComponentFile.STEPS, ComponentFile.EVALUATORS ].includes( fileKind ) ) {
145
226
  traverse( ast, {
146
227
  ObjectProperty: path => {
147
228
  if ( !isIdentifier( path.node.key, { name: 'fn' } ) ) {
@@ -157,6 +238,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
157
238
  const callee = cPath.node.callee;
158
239
  if ( isIdentifier( callee ) ) {
159
240
  const { name } = callee;
241
+ const fileLabel = getFileKindLabel( filename );
160
242
  const violation = [
161
243
  [ 'step', localStepIds.has( name ) || importedStepIds.has( name ) ],
162
244
  [ 'evaluator', localEvaluatorIds.has( name ) || importedEvaluatorIds.has( name ) ],
@@ -164,7 +246,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
164
246
  ].find( v => v[1] )?.[0];
165
247
 
166
248
  if ( violation ) {
167
- throw new Error( `Invalid call in ${fileKind}.js fn: calling a ${violation} ('${name}') is not allowed in ${filename}` );
249
+ throw new Error( `Invalid call in ${fileLabel} fn: calling a ${violation} ('${name}') is not allowed in ${filename}` );
168
250
  }
169
251
  }
170
252
  }