@output.ai/core 0.2.2 → 0.3.0-dev.pr263-a59dd0e
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.
- package/package.json +1 -1
- package/src/worker/loader.js +2 -3
- package/src/worker/loader.spec.js +21 -0
- package/src/worker/loader_tools.js +45 -4
- package/src/worker/loader_tools.spec.js +80 -0
- package/src/worker/webpack_loaders/consts.js +0 -14
- package/src/worker/webpack_loaders/tools.js +71 -27
- package/src/worker/webpack_loaders/tools.spec.js +63 -56
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +17 -11
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +99 -33
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +401 -78
|
@@ -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, CoreModule
|
|
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,36 +18,92 @@ import {
|
|
|
18
18
|
const traverse = traverseModule.default ?? traverseModule;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
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
|
|
25
|
+
*/
|
|
26
|
+
const getFileKindLabel = filename => {
|
|
27
|
+
if ( isAnyStepsPath( filename ) ) {
|
|
28
|
+
return 'steps.js';
|
|
29
|
+
}
|
|
30
|
+
if ( isEvaluatorsPath( filename ) ) {
|
|
31
|
+
return 'evaluators.js';
|
|
32
|
+
}
|
|
33
|
+
if ( /workflow\.js$/.test( filename ) ) {
|
|
34
|
+
return 'workflow.js';
|
|
35
|
+
}
|
|
36
|
+
return filename;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
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)
|
|
22
45
|
*/
|
|
23
46
|
const validateWorkflowImports = ( { specifier, filename } ) => {
|
|
24
47
|
const isCore = Object.values( CoreModule ).includes( specifier );
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
48
|
+
const fileKind = getFileKind( specifier );
|
|
49
|
+
const isComponent = Object.values( ComponentFile ).includes( fileKind );
|
|
50
|
+
const isNonComponentFile = fileKind === null;
|
|
51
|
+
|
|
52
|
+
if ( !isCore && !isComponent && !isNonComponentFile ) {
|
|
28
53
|
throw new Error( `Invalid dependency in workflow.js: '${specifier}'. \
|
|
29
|
-
Only components (${Object.values( ComponentFile ) } ), @output.ai/core, or
|
|
54
|
+
Only components (${Object.values( ComponentFile ) } ), @output.ai/core, or non-component files are allowed in ${filename}` );
|
|
30
55
|
}
|
|
31
56
|
};
|
|
32
57
|
|
|
33
58
|
/**
|
|
34
|
-
* Check if evaluators
|
|
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)
|
|
35
67
|
*/
|
|
36
|
-
const validateStepEvaluatorImports = ( {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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}` );
|
|
40
76
|
}
|
|
41
77
|
};
|
|
42
78
|
|
|
43
79
|
/**
|
|
44
|
-
* Validate import for evaluators, steps,
|
|
80
|
+
* Validate import for evaluators, steps, workflow
|
|
45
81
|
*/
|
|
46
82
|
const executeImportValidations = ( { fileKind, specifier, filename } ) => {
|
|
47
83
|
if ( fileKind === ComponentFile.WORKFLOW ) {
|
|
48
|
-
validateWorkflowImports( {
|
|
84
|
+
validateWorkflowImports( { specifier, filename } );
|
|
49
85
|
} else if ( Object.values( ComponentFile ).includes( fileKind ) ) {
|
|
50
|
-
validateStepEvaluatorImports( {
|
|
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}` );
|
|
51
107
|
}
|
|
52
108
|
};
|
|
53
109
|
|
|
@@ -56,13 +112,13 @@ const executeImportValidations = ( { fileKind, specifier, filename } ) => {
|
|
|
56
112
|
* Returns the source unchanged unless a validation error is found.
|
|
57
113
|
*
|
|
58
114
|
* Rules enforced:
|
|
59
|
-
* -
|
|
60
|
-
* - evaluators.js
|
|
61
|
-
* -
|
|
62
|
-
* -
|
|
63
|
-
* - steps.js:
|
|
64
|
-
* -
|
|
65
|
-
*
|
|
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`
|
|
66
122
|
*
|
|
67
123
|
* @param {string|Buffer} source
|
|
68
124
|
* @param {any} inputMap
|
|
@@ -82,7 +138,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
82
138
|
// Collect local declarations and imported identifiers by type
|
|
83
139
|
const localStepIds = new Set();
|
|
84
140
|
const localEvaluatorIds = new Set();
|
|
85
|
-
const importedStepIds = new Set();
|
|
141
|
+
const importedStepIds = new Set();
|
|
86
142
|
const importedEvaluatorIds = new Set();
|
|
87
143
|
const importedWorkflowIds = new Set();
|
|
88
144
|
|
|
@@ -94,12 +150,12 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
94
150
|
executeImportValidations( { fileKind, specifier, filename } );
|
|
95
151
|
|
|
96
152
|
// Collect imported identifiers for later call checks
|
|
153
|
+
const importedKind = getFileKind( specifier );
|
|
97
154
|
const accumulator = ( {
|
|
98
155
|
[ComponentFile.STEPS]: importedStepIds,
|
|
99
|
-
[ComponentFile.SHARED_STEPS]: importedStepIds,
|
|
100
156
|
[ComponentFile.EVALUATORS]: importedEvaluatorIds,
|
|
101
157
|
[ComponentFile.WORKFLOW]: importedWorkflowIds
|
|
102
|
-
} )[
|
|
158
|
+
} )[importedKind];
|
|
103
159
|
if ( accumulator ) {
|
|
104
160
|
for ( const s of path.node.specifiers ) {
|
|
105
161
|
if ( isImportSpecifier( s ) || isImportDefaultSpecifier( s ) ) {
|
|
@@ -114,12 +170,21 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
114
170
|
return;
|
|
115
171
|
}
|
|
116
172
|
|
|
117
|
-
//
|
|
118
|
-
if ( isIdentifier( init.callee, { name: 'step' } )
|
|
119
|
-
|
|
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
|
+
}
|
|
120
185
|
}
|
|
121
|
-
if ( isIdentifier( init.callee, { name: '
|
|
122
|
-
|
|
186
|
+
if ( isIdentifier( init.callee, { name: 'workflow' } ) ) {
|
|
187
|
+
validateInstantiationLocation( 'workflow', filename );
|
|
123
188
|
}
|
|
124
189
|
|
|
125
190
|
// CommonJS requires: validate source and collect identifiers
|
|
@@ -134,7 +199,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
134
199
|
// Collect imported identifiers from require patterns
|
|
135
200
|
if ( isStringLiteral( firstArg ) ) {
|
|
136
201
|
const reqType = getFileKind( toAbsolutePath( fileDir, req ) );
|
|
137
|
-
if (
|
|
202
|
+
if ( reqType === ComponentFile.STEPS && isObjectPattern( path.node.id ) ) {
|
|
138
203
|
for ( const prop of path.node.id.properties ) {
|
|
139
204
|
if ( isObjectProperty( prop ) && isIdentifier( prop.value ) ) {
|
|
140
205
|
importedStepIds.add( prop.value.name );
|
|
@@ -157,7 +222,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
157
222
|
} );
|
|
158
223
|
|
|
159
224
|
// Function-body call validations for steps/evaluators files
|
|
160
|
-
if ( [ ComponentFile.STEPS, ComponentFile.
|
|
225
|
+
if ( [ ComponentFile.STEPS, ComponentFile.EVALUATORS ].includes( fileKind ) ) {
|
|
161
226
|
traverse( ast, {
|
|
162
227
|
ObjectProperty: path => {
|
|
163
228
|
if ( !isIdentifier( path.node.key, { name: 'fn' } ) ) {
|
|
@@ -173,6 +238,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
173
238
|
const callee = cPath.node.callee;
|
|
174
239
|
if ( isIdentifier( callee ) ) {
|
|
175
240
|
const { name } = callee;
|
|
241
|
+
const fileLabel = getFileKindLabel( filename );
|
|
176
242
|
const violation = [
|
|
177
243
|
[ 'step', localStepIds.has( name ) || importedStepIds.has( name ) ],
|
|
178
244
|
[ 'evaluator', localEvaluatorIds.has( name ) || importedEvaluatorIds.has( name ) ],
|
|
@@ -180,7 +246,7 @@ export default function workflowValidatorLoader( source, inputMap ) {
|
|
|
180
246
|
].find( v => v[1] )?.[0];
|
|
181
247
|
|
|
182
248
|
if ( violation ) {
|
|
183
|
-
throw new Error( `Invalid call in ${
|
|
249
|
+
throw new Error( `Invalid call in ${fileLabel} fn: calling a ${violation} ('${name}') is not allowed in ${filename}` );
|
|
184
250
|
}
|
|
185
251
|
}
|
|
186
252
|
}
|