@zibby/core 0.1.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.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/package.json +94 -0
- package/src/agents/base.js +361 -0
- package/src/constants.js +47 -0
- package/src/enrichment/base.js +49 -0
- package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
- package/src/enrichment/enrichers/dom-enricher.js +171 -0
- package/src/enrichment/enrichers/page-state-enricher.js +129 -0
- package/src/enrichment/enrichers/position-enricher.js +67 -0
- package/src/enrichment/index.js +96 -0
- package/src/enrichment/mcp-integration.js +149 -0
- package/src/enrichment/mcp-ref-enricher.js +78 -0
- package/src/enrichment/pipeline.js +192 -0
- package/src/enrichment/trace-text-enricher.js +115 -0
- package/src/framework/AGENTS.md +98 -0
- package/src/framework/agents/base.js +72 -0
- package/src/framework/agents/claude-strategy.js +278 -0
- package/src/framework/agents/cursor-strategy.js +459 -0
- package/src/framework/agents/index.js +105 -0
- package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
- package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
- package/src/framework/code-generator.js +301 -0
- package/src/framework/constants.js +33 -0
- package/src/framework/context-loader.js +101 -0
- package/src/framework/function-bridge.js +78 -0
- package/src/framework/function-skill-registry.js +20 -0
- package/src/framework/graph-compiler.js +342 -0
- package/src/framework/graph.js +610 -0
- package/src/framework/index.js +28 -0
- package/src/framework/node-registry.js +163 -0
- package/src/framework/node.js +259 -0
- package/src/framework/output-parser.js +71 -0
- package/src/framework/skill-registry.js +55 -0
- package/src/framework/state-utils.js +52 -0
- package/src/framework/state.js +67 -0
- package/src/framework/tool-resolver.js +65 -0
- package/src/index.js +342 -0
- package/src/runtime/generation/base.js +46 -0
- package/src/runtime/generation/index.js +70 -0
- package/src/runtime/generation/mcp-ref-strategy.js +197 -0
- package/src/runtime/generation/stable-id-strategy.js +170 -0
- package/src/runtime/stable-id-runtime.js +248 -0
- package/src/runtime/verification/base.js +44 -0
- package/src/runtime/verification/index.js +67 -0
- package/src/runtime/verification/playwright-json-strategy.js +119 -0
- package/src/runtime/zibby-runtime.js +299 -0
- package/src/sync/index.js +2 -0
- package/src/sync/uploader.js +29 -0
- package/src/tools/run-playwright-test.js +158 -0
- package/src/utils/adf-converter.js +68 -0
- package/src/utils/ast-utils.js +37 -0
- package/src/utils/ci-setup.js +124 -0
- package/src/utils/cursor-utils.js +71 -0
- package/src/utils/logger.js +144 -0
- package/src/utils/mcp-config-writer.js +115 -0
- package/src/utils/node-schema-parser.js +522 -0
- package/src/utils/post-process-events.js +55 -0
- package/src/utils/result-handler.js +102 -0
- package/src/utils/ripple-effect.js +84 -0
- package/src/utils/selector-generator.js +239 -0
- package/src/utils/streaming-parser.js +387 -0
- package/src/utils/test-post-processor.js +211 -0
- package/src/utils/timeline.js +217 -0
- package/src/utils/trace-parser.js +325 -0
- package/src/utils/video-organizer.js +91 -0
- package/templates/browser-test-automation/README.md +114 -0
- package/templates/browser-test-automation/graph.js +54 -0
- package/templates/browser-test-automation/nodes/execute-live.js +250 -0
- package/templates/browser-test-automation/nodes/generate-script.js +77 -0
- package/templates/browser-test-automation/nodes/index.js +3 -0
- package/templates/browser-test-automation/nodes/preflight.js +59 -0
- package/templates/browser-test-automation/nodes/utils.js +154 -0
- package/templates/browser-test-automation/result-handler.js +286 -0
- package/templates/code-analysis/graph.js +72 -0
- package/templates/code-analysis/index.js +18 -0
- package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
- package/templates/code-analysis/nodes/create-pr-node.js +175 -0
- package/templates/code-analysis/nodes/finalize-node.js +118 -0
- package/templates/code-analysis/nodes/generate-code-node.js +425 -0
- package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
- package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
- package/templates/code-analysis/nodes/setup-node.js +142 -0
- package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
- package/templates/code-analysis/prompts/generate-code.md +33 -0
- package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
- package/templates/code-analysis/state.js +40 -0
- package/templates/code-implementation/graph.js +35 -0
- package/templates/code-implementation/index.js +7 -0
- package/templates/code-implementation/state.js +14 -0
- package/templates/global-setup.js +56 -0
- package/templates/index.js +94 -0
- package/templates/register-nodes.js +24 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Schema Parser
|
|
3
|
+
* Extracts exact return schema from node execute functions using AST analysis
|
|
4
|
+
* Provides 100% accurate state variable information for the Variable Inspector
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as acorn from 'acorn';
|
|
8
|
+
import * as walk from 'acorn-walk';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a node definition and extract its state output schema
|
|
12
|
+
* @param {Object} nodeDefinition - Node definition object with execute function
|
|
13
|
+
* @returns {Object} Schema describing the node's state output
|
|
14
|
+
*/
|
|
15
|
+
export function parseNodeSchema(nodeDefinition) {
|
|
16
|
+
if (!nodeDefinition || typeof nodeDefinition.execute !== 'function') {
|
|
17
|
+
return createUnknownSchema('No execute function found');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const code = nodeDefinition.execute.toString();
|
|
21
|
+
return parseExecuteSchema(code);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse execute function code and extract return schema
|
|
26
|
+
* @param {string} code - Execute function source code
|
|
27
|
+
* @returns {Object} Schema describing the return value
|
|
28
|
+
*/
|
|
29
|
+
export function parseExecuteSchema(code) {
|
|
30
|
+
if (!code || typeof code !== 'string') {
|
|
31
|
+
return createUnknownSchema('Invalid code');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Handle different function syntaxes:
|
|
36
|
+
// 1. async (state) => { ... }
|
|
37
|
+
// 2. async function(state) { ... }
|
|
38
|
+
// 3. async execute(state) { ... } (method syntax)
|
|
39
|
+
let wrappedCode;
|
|
40
|
+
const trimmed = code.trimStart();
|
|
41
|
+
|
|
42
|
+
if (trimmed.startsWith('async execute(')) {
|
|
43
|
+
// async execute(state) { ... } -> async (state) => { ... }
|
|
44
|
+
wrappedCode = `const __fn = async ${trimmed.substring('async execute'.length).replace(/^\(/, '(').replace(/\)\s*\{/, ') => {')}`;
|
|
45
|
+
} else if (trimmed.startsWith('execute(')) {
|
|
46
|
+
// execute(state) { ... } -> (state) => { ... }
|
|
47
|
+
wrappedCode = `const __fn = ${trimmed.substring('execute'.length).replace(/\)\s*\{/, ') => {')}`;
|
|
48
|
+
} else {
|
|
49
|
+
wrappedCode = `const __fn = ${trimmed}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ast = acorn.parse(wrappedCode, {
|
|
53
|
+
ecmaVersion: 'latest',
|
|
54
|
+
sourceType: 'module',
|
|
55
|
+
allowAwaitOutsideModules: true
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const returnSchemas = [];
|
|
59
|
+
const variableScopes = new Map();
|
|
60
|
+
|
|
61
|
+
// First pass: collect variable declarations
|
|
62
|
+
walk.simple(ast, {
|
|
63
|
+
VariableDeclarator(node) {
|
|
64
|
+
if (node.id.type === 'Identifier' && node.init) {
|
|
65
|
+
variableScopes.set(node.id.name, node.init);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Second pass: extract return statements
|
|
71
|
+
walk.simple(ast, {
|
|
72
|
+
ReturnStatement(node) {
|
|
73
|
+
if (node.argument) {
|
|
74
|
+
const schema = extractSchemaFromNode(node.argument, variableScopes);
|
|
75
|
+
returnSchemas.push(schema);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (returnSchemas.length === 0) {
|
|
81
|
+
return createUnknownSchema('No return statements found');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Merge all return schemas (handles multiple return paths)
|
|
85
|
+
return mergeSchemas(returnSchemas);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return createUnknownSchema(`Parse error: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract schema from an AST node
|
|
93
|
+
* @param {Object} node - AST node
|
|
94
|
+
* @param {Map} variableScopes - Map of variable names to their init nodes
|
|
95
|
+
* @returns {Object} Schema object
|
|
96
|
+
*/
|
|
97
|
+
function extractSchemaFromNode(node, variableScopes) {
|
|
98
|
+
switch (node.type) {
|
|
99
|
+
case 'ObjectExpression':
|
|
100
|
+
return extractObjectSchema(node, variableScopes);
|
|
101
|
+
|
|
102
|
+
case 'ArrayExpression':
|
|
103
|
+
return extractArraySchema(node, variableScopes);
|
|
104
|
+
|
|
105
|
+
case 'Literal':
|
|
106
|
+
return extractLiteralSchema(node);
|
|
107
|
+
|
|
108
|
+
case 'Identifier':
|
|
109
|
+
return resolveIdentifier(node.name, variableScopes);
|
|
110
|
+
|
|
111
|
+
case 'MemberExpression':
|
|
112
|
+
return extractMemberSchema(node);
|
|
113
|
+
|
|
114
|
+
case 'ConditionalExpression': {
|
|
115
|
+
// condition ? consequent : alternate
|
|
116
|
+
const consequent = extractSchemaFromNode(node.consequent, variableScopes);
|
|
117
|
+
const alternate = extractSchemaFromNode(node.alternate, variableScopes);
|
|
118
|
+
return mergeSchemas([consequent, alternate]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'LogicalExpression':
|
|
122
|
+
// a || b, a && b
|
|
123
|
+
if (node.operator === '||') {
|
|
124
|
+
return extractSchemaFromNode(node.left, variableScopes);
|
|
125
|
+
}
|
|
126
|
+
return extractSchemaFromNode(node.right, variableScopes);
|
|
127
|
+
|
|
128
|
+
case 'CallExpression':
|
|
129
|
+
return extractCallSchema(node);
|
|
130
|
+
|
|
131
|
+
case 'AwaitExpression':
|
|
132
|
+
return extractSchemaFromNode(node.argument, variableScopes);
|
|
133
|
+
|
|
134
|
+
case 'TemplateLiteral':
|
|
135
|
+
return { type: 'string' };
|
|
136
|
+
|
|
137
|
+
case 'UnaryExpression':
|
|
138
|
+
if (node.operator === '!') return { type: 'boolean' };
|
|
139
|
+
return { type: 'unknown' };
|
|
140
|
+
|
|
141
|
+
case 'BinaryExpression':
|
|
142
|
+
if (['===', '!==', '==', '!=', '<', '>', '<=', '>='].includes(node.operator)) {
|
|
143
|
+
return { type: 'boolean' };
|
|
144
|
+
}
|
|
145
|
+
if (['+', '-', '*', '/', '%'].includes(node.operator)) {
|
|
146
|
+
return { type: 'number' };
|
|
147
|
+
}
|
|
148
|
+
return { type: 'unknown' };
|
|
149
|
+
|
|
150
|
+
default:
|
|
151
|
+
return { type: 'unknown', astType: node.type };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extract schema from an object expression
|
|
157
|
+
*/
|
|
158
|
+
function extractObjectSchema(node, variableScopes) {
|
|
159
|
+
const properties = {};
|
|
160
|
+
|
|
161
|
+
for (const prop of node.properties) {
|
|
162
|
+
if (prop.type === 'SpreadElement') {
|
|
163
|
+
// Handle spread: { ...variable } or { ...(condition && { prop }) }
|
|
164
|
+
const spreadSchema = extractSpreadSchema(prop.argument, variableScopes);
|
|
165
|
+
if (spreadSchema.properties) {
|
|
166
|
+
for (const [key, value] of Object.entries(spreadSchema.properties)) {
|
|
167
|
+
properties[key] = { ...value, optional: true };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else if (prop.type === 'Property') {
|
|
171
|
+
const key = getPropertyKey(prop);
|
|
172
|
+
if (key) {
|
|
173
|
+
const valueSchema = extractSchemaFromNode(prop.value, variableScopes);
|
|
174
|
+
properties[key] = valueSchema;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Extract schema from spread expressions
|
|
187
|
+
*/
|
|
188
|
+
function extractSpreadSchema(node, variableScopes) {
|
|
189
|
+
// Handle: ...(condition && { prop: value })
|
|
190
|
+
if (node.type === 'LogicalExpression' && node.operator === '&&') {
|
|
191
|
+
return extractSchemaFromNode(node.right, variableScopes);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Handle: ...variable
|
|
195
|
+
if (node.type === 'Identifier') {
|
|
196
|
+
return resolveIdentifier(node.name, variableScopes);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Handle: ...obj.property
|
|
200
|
+
if (node.type === 'MemberExpression') {
|
|
201
|
+
return extractMemberSchema(node);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return extractSchemaFromNode(node, variableScopes);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Extract schema from array expression
|
|
209
|
+
*/
|
|
210
|
+
function extractArraySchema(node, variableScopes) {
|
|
211
|
+
if (node.elements.length === 0) {
|
|
212
|
+
return { type: 'array', items: { type: 'unknown' } };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Infer item type from first element
|
|
216
|
+
const firstElement = node.elements[0];
|
|
217
|
+
if (firstElement) {
|
|
218
|
+
const itemSchema = extractSchemaFromNode(firstElement, variableScopes);
|
|
219
|
+
return { type: 'array', items: itemSchema };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { type: 'array', items: { type: 'unknown' } };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Extract schema from literal values
|
|
227
|
+
*/
|
|
228
|
+
function extractLiteralSchema(node) {
|
|
229
|
+
const value = node.value;
|
|
230
|
+
|
|
231
|
+
if (value === null) return { type: 'null' };
|
|
232
|
+
if (typeof value === 'boolean') return { type: 'boolean', value };
|
|
233
|
+
if (typeof value === 'number') return { type: 'number' };
|
|
234
|
+
if (typeof value === 'string') return { type: 'string' };
|
|
235
|
+
|
|
236
|
+
return { type: 'unknown' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolve an identifier to its schema
|
|
241
|
+
*/
|
|
242
|
+
function resolveIdentifier(name, variableScopes) {
|
|
243
|
+
// Check if we have the variable's init expression
|
|
244
|
+
const initNode = variableScopes.get(name);
|
|
245
|
+
if (initNode) {
|
|
246
|
+
return extractSchemaFromNode(initNode, variableScopes);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Common known identifiers
|
|
250
|
+
const knownTypes = {
|
|
251
|
+
'true': { type: 'boolean', value: true },
|
|
252
|
+
'false': { type: 'boolean', value: false },
|
|
253
|
+
'null': { type: 'null' },
|
|
254
|
+
'undefined': { type: 'undefined' }
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
if (knownTypes[name]) {
|
|
258
|
+
return knownTypes[name];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Return reference type with the variable name for context
|
|
262
|
+
return { type: 'reference', name, label: camelToTitle(name) };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Extract schema from member expression (e.g., obj.prop)
|
|
267
|
+
*/
|
|
268
|
+
function extractMemberSchema(node) {
|
|
269
|
+
const path = getMemberPath(node);
|
|
270
|
+
const lastPart = path[path.length - 1];
|
|
271
|
+
|
|
272
|
+
// Infer type from common property names
|
|
273
|
+
const typeHints = {
|
|
274
|
+
'length': { type: 'number' },
|
|
275
|
+
'trim': { type: 'string' },
|
|
276
|
+
'toString': { type: 'string' },
|
|
277
|
+
'toISOString': { type: 'string' },
|
|
278
|
+
'success': { type: 'boolean' },
|
|
279
|
+
'error': { type: 'string' },
|
|
280
|
+
'message': { type: 'string' },
|
|
281
|
+
'url': { type: 'string' },
|
|
282
|
+
'path': { type: 'string' },
|
|
283
|
+
'name': { type: 'string' },
|
|
284
|
+
'id': { type: 'string' },
|
|
285
|
+
'count': { type: 'number' },
|
|
286
|
+
'total': { type: 'number' },
|
|
287
|
+
'timestamp': { type: 'string' }
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return typeHints[lastPart] || {
|
|
291
|
+
type: 'reference',
|
|
292
|
+
path: path.join('.'),
|
|
293
|
+
label: camelToTitle(lastPart)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get the full path of a member expression
|
|
299
|
+
*/
|
|
300
|
+
function getMemberPath(node) {
|
|
301
|
+
const parts = [];
|
|
302
|
+
|
|
303
|
+
function traverse(n) {
|
|
304
|
+
if (n.type === 'MemberExpression') {
|
|
305
|
+
traverse(n.object);
|
|
306
|
+
if (n.property.type === 'Identifier') {
|
|
307
|
+
parts.push(n.property.name);
|
|
308
|
+
} else if (n.property.type === 'Literal') {
|
|
309
|
+
parts.push(String(n.property.value));
|
|
310
|
+
}
|
|
311
|
+
} else if (n.type === 'Identifier') {
|
|
312
|
+
parts.push(n.name);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
traverse(node);
|
|
317
|
+
return parts;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Extract schema from call expressions
|
|
322
|
+
*/
|
|
323
|
+
function extractCallSchema(node) {
|
|
324
|
+
const callee = node.callee;
|
|
325
|
+
|
|
326
|
+
// Handle common method calls
|
|
327
|
+
if (callee.type === 'MemberExpression') {
|
|
328
|
+
const method = callee.property?.name;
|
|
329
|
+
|
|
330
|
+
const methodTypes = {
|
|
331
|
+
'trim': { type: 'string' },
|
|
332
|
+
'toString': { type: 'string' },
|
|
333
|
+
'toISOString': { type: 'string' },
|
|
334
|
+
'toJSON': { type: 'object' },
|
|
335
|
+
'join': { type: 'string' },
|
|
336
|
+
'map': { type: 'array' },
|
|
337
|
+
'filter': { type: 'array' },
|
|
338
|
+
'slice': { type: 'array' },
|
|
339
|
+
'concat': { type: 'array' },
|
|
340
|
+
'split': { type: 'array' },
|
|
341
|
+
'stringify': { type: 'string' },
|
|
342
|
+
'parse': { type: 'object' }
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (methodTypes[method]) {
|
|
346
|
+
return methodTypes[method];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Handle constructor calls
|
|
351
|
+
if (callee.type === 'Identifier') {
|
|
352
|
+
const constructorTypes = {
|
|
353
|
+
'Date': { type: 'object', label: 'Date' },
|
|
354
|
+
'Array': { type: 'array' },
|
|
355
|
+
'Object': { type: 'object' },
|
|
356
|
+
'String': { type: 'string' },
|
|
357
|
+
'Number': { type: 'number' },
|
|
358
|
+
'Boolean': { type: 'boolean' }
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
if (constructorTypes[callee.name]) {
|
|
362
|
+
return constructorTypes[callee.name];
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { type: 'unknown', call: true };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Get property key from a Property node
|
|
371
|
+
*/
|
|
372
|
+
function getPropertyKey(prop) {
|
|
373
|
+
if (prop.key.type === 'Identifier') {
|
|
374
|
+
return prop.key.name;
|
|
375
|
+
}
|
|
376
|
+
if (prop.key.type === 'Literal') {
|
|
377
|
+
return String(prop.key.value);
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Merge multiple schemas (for multiple return paths)
|
|
384
|
+
*/
|
|
385
|
+
function mergeSchemas(schemas) {
|
|
386
|
+
if (schemas.length === 0) {
|
|
387
|
+
return createUnknownSchema('Empty schemas');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (schemas.length === 1) {
|
|
391
|
+
return schemas[0];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// If all schemas are objects, merge their properties
|
|
395
|
+
const objectSchemas = schemas.filter(s => s.type === 'object');
|
|
396
|
+
if (objectSchemas.length === schemas.length) {
|
|
397
|
+
const mergedProperties = {};
|
|
398
|
+
|
|
399
|
+
for (const schema of objectSchemas) {
|
|
400
|
+
if (schema.properties) {
|
|
401
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
402
|
+
if (mergedProperties[key]) {
|
|
403
|
+
// Property exists in multiple returns - mark as potentially optional
|
|
404
|
+
mergedProperties[key] = {
|
|
405
|
+
...mergeSchemas([mergedProperties[key], value]),
|
|
406
|
+
optional: true
|
|
407
|
+
};
|
|
408
|
+
} else {
|
|
409
|
+
mergedProperties[key] = value;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
type: 'object',
|
|
417
|
+
properties: mergedProperties
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Mixed types - return union
|
|
422
|
+
return {
|
|
423
|
+
type: 'union',
|
|
424
|
+
schemas
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Create an unknown schema with reason
|
|
430
|
+
*/
|
|
431
|
+
function createUnknownSchema(reason) {
|
|
432
|
+
return {
|
|
433
|
+
type: 'unknown',
|
|
434
|
+
reason
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Convert camelCase to Title Case
|
|
440
|
+
*/
|
|
441
|
+
function camelToTitle(str) {
|
|
442
|
+
if (!str) return '';
|
|
443
|
+
return str
|
|
444
|
+
.replace(/([A-Z])/g, ' $1')
|
|
445
|
+
.replace(/^./, s => s.toUpperCase())
|
|
446
|
+
.trim();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Parse all nodes from a module and extract their schemas
|
|
451
|
+
* @param {Object} nodeDefinitions - Object mapping node names to definitions
|
|
452
|
+
* @returns {Object} Object mapping node names to schemas
|
|
453
|
+
*/
|
|
454
|
+
export function parseAllNodeSchemas(nodeDefinitions) {
|
|
455
|
+
const schemas = {};
|
|
456
|
+
|
|
457
|
+
for (const [name, definition] of Object.entries(nodeDefinitions)) {
|
|
458
|
+
schemas[name] = {
|
|
459
|
+
name: definition.name || name,
|
|
460
|
+
hasOutputSchema: !!definition.outputSchema,
|
|
461
|
+
stateSchema: parseNodeSchema(definition)
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return schemas;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Convert parsed schema to display-friendly format for Variable Inspector
|
|
470
|
+
* @param {Object} schema - Parsed schema
|
|
471
|
+
* @param {string} prefix - Path prefix
|
|
472
|
+
* @returns {Array} Array of { path, type, label, optional } objects
|
|
473
|
+
*/
|
|
474
|
+
export function schemaToVariables(schema, prefix = '') {
|
|
475
|
+
const variables = [];
|
|
476
|
+
|
|
477
|
+
// Handle union types by merging all object schemas
|
|
478
|
+
if (schema.type === 'union' && schema.schemas) {
|
|
479
|
+
const mergedProps = {};
|
|
480
|
+
for (const subSchema of schema.schemas) {
|
|
481
|
+
if (subSchema.type === 'object' && subSchema.properties) {
|
|
482
|
+
for (const [key, propSchema] of Object.entries(subSchema.properties)) {
|
|
483
|
+
if (!mergedProps[key]) {
|
|
484
|
+
mergedProps[key] = { ...propSchema, optional: true };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return schemaToVariables({ type: 'object', properties: mergedProps }, prefix);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (schema.type === 'object' && schema.properties) {
|
|
493
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
494
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
495
|
+
const label = propSchema.label || camelToTitle(key);
|
|
496
|
+
|
|
497
|
+
variables.push({
|
|
498
|
+
path,
|
|
499
|
+
type: propSchema.type,
|
|
500
|
+
label,
|
|
501
|
+
optional: propSchema.optional || false
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Recurse into nested objects
|
|
505
|
+
if (propSchema.type === 'object' && propSchema.properties) {
|
|
506
|
+
variables.push(...schemaToVariables(propSchema, path));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Recurse into union types within properties
|
|
510
|
+
if (propSchema.type === 'union' && propSchema.schemas) {
|
|
511
|
+
const nestedVars = schemaToVariables(propSchema, path);
|
|
512
|
+
for (const v of nestedVars) {
|
|
513
|
+
if (!variables.find(existing => existing.path === v.path)) {
|
|
514
|
+
variables.push(v);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return variables;
|
|
522
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-process events.json to enrich with trace data
|
|
3
|
+
* This extracts ACTUAL element text from trace for multi-language support
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { TraceTextEnricher } from '../enrichment/trace-text-enricher.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Enrich events.json with actual text from trace
|
|
11
|
+
* @param {string} sessionPath - Path to session directory (e.g., .../execute_live)
|
|
12
|
+
* @returns {Promise<{enriched: number, failed: number}>}
|
|
13
|
+
*/
|
|
14
|
+
export async function postProcessEvents(sessionPath) {
|
|
15
|
+
const eventsPath = join(sessionPath, 'events.json');
|
|
16
|
+
const enrichedPath = join(sessionPath, 'events-enriched.json');
|
|
17
|
+
|
|
18
|
+
if (!existsSync(eventsPath)) {
|
|
19
|
+
console.log('[PostProcess] No events.json found');
|
|
20
|
+
return { enriched: 0, failed: 0 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
|
|
25
|
+
const enricher = new TraceTextEnricher();
|
|
26
|
+
|
|
27
|
+
let enriched = 0;
|
|
28
|
+
let failed = 0;
|
|
29
|
+
|
|
30
|
+
for (const event of events) {
|
|
31
|
+
try {
|
|
32
|
+
const enrichedData = await enricher.enrich(event, { sessionPath });
|
|
33
|
+
if (enrichedData) {
|
|
34
|
+
event.enrichedData = { ...(event.enrichedData || {}), ...enrichedData };
|
|
35
|
+
enriched++;
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.log(`[PostProcess] Failed to enrich event ${event.id}: ${e.message}`);
|
|
39
|
+
failed++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Save enriched events (backup original)
|
|
44
|
+
if (enriched > 0) {
|
|
45
|
+
writeFileSync(enrichedPath, JSON.stringify(events, null, 2));
|
|
46
|
+
writeFileSync(eventsPath, JSON.stringify(events, null, 2));
|
|
47
|
+
console.log(`[PostProcess] ✅ Enriched ${enriched} events (${failed} failed)`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { enriched, failed };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.log(`[PostProcess] ❌ Failed to post-process events: ${e.message}`);
|
|
53
|
+
return { enriched: 0, failed: 0 };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultHandler - Generic post-execution logic for any workflow.
|
|
3
|
+
* Saves results, logs outcomes. Subclass and override onNodeSaved()
|
|
4
|
+
* for workflow-specific post-processing (event enrichment, video, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { logger } from './logger.js';
|
|
10
|
+
|
|
11
|
+
export class ResultHandler {
|
|
12
|
+
/**
|
|
13
|
+
* Save execution title to session.
|
|
14
|
+
* Searches state for the first node output containing a 'title' or 'result' string field.
|
|
15
|
+
*/
|
|
16
|
+
static saveTitle(result, _cwd) {
|
|
17
|
+
const sessionPath = result.state.sessionPath;
|
|
18
|
+
if (!sessionPath) return;
|
|
19
|
+
|
|
20
|
+
const title = ResultHandler._findInState(result.state, 'title')
|
|
21
|
+
|| ResultHandler._findInState(result.state, 'result');
|
|
22
|
+
if (!title || typeof title !== 'string') return;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const titlePath = join(sessionPath, 'title.txt');
|
|
26
|
+
writeFileSync(titlePath, title, 'utf-8');
|
|
27
|
+
logger.info(`Saved title to session: "${title}"`);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.warn('⚠️ Could not save title file:', err.message);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static _findInState(state, key) {
|
|
34
|
+
for (const [, value] of Object.entries(state)) {
|
|
35
|
+
if (value && typeof value === 'object' && value[key] !== undefined) {
|
|
36
|
+
return value[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Save execution data for each node that produced actions.
|
|
44
|
+
* Writes result.json then calls this.onNodeSaved() for subclass processing.
|
|
45
|
+
*/
|
|
46
|
+
static async saveExecutionData(result) {
|
|
47
|
+
const sessionPath = result.state.sessionPath;
|
|
48
|
+
if (!sessionPath) return;
|
|
49
|
+
|
|
50
|
+
for (const [nodeName, output] of Object.entries(result.state)) {
|
|
51
|
+
if (!output || typeof output !== 'object' || !output.actions) continue;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const nodeFolder = join(sessionPath, nodeName);
|
|
55
|
+
if (!existsSync(nodeFolder)) continue;
|
|
56
|
+
|
|
57
|
+
const executionDataPath = join(nodeFolder, 'result.json');
|
|
58
|
+
writeFileSync(executionDataPath, JSON.stringify(output, null, 2), 'utf-8');
|
|
59
|
+
logger.info(`Saved execution data to ${nodeName} folder`);
|
|
60
|
+
|
|
61
|
+
await this.onNodeSaved(nodeFolder, output);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.warn(`⚠️ Could not save execution data for ${nodeName}:`, err.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook for subclasses -- called after result.json is written for each node.
|
|
70
|
+
* Override to add enrichment, video processing, etc.
|
|
71
|
+
*/
|
|
72
|
+
static async onNodeSaved(_nodeFolder, _executionData) {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Log workflow result. Inspects all node outputs for success/failure indicators.
|
|
76
|
+
*/
|
|
77
|
+
static logResult(result, outputPath) {
|
|
78
|
+
const allOutputs = Object.entries(result.state)
|
|
79
|
+
.filter(([, v]) => v && typeof v === 'object' && v.success !== undefined);
|
|
80
|
+
|
|
81
|
+
const allPassed = allOutputs.length > 0 && allOutputs.every(([, v]) => v.success);
|
|
82
|
+
const anyFailed = allOutputs.some(([, v]) => v.success === false);
|
|
83
|
+
|
|
84
|
+
if (allPassed) {
|
|
85
|
+
logger.info('Workflow completed successfully.');
|
|
86
|
+
if (outputPath) logger.info(`Output: ${outputPath}`);
|
|
87
|
+
} else if (anyFailed) {
|
|
88
|
+
const failedNodes = allOutputs.filter(([, v]) => !v.success).map(([k]) => k);
|
|
89
|
+
logger.info(`Workflow completed with failures in: ${failedNodes.join(', ')}`);
|
|
90
|
+
if (outputPath) logger.info(`Output: ${outputPath}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return allPassed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static handle(result, cwd, outputPath) {
|
|
97
|
+
this.saveTitle(result, cwd);
|
|
98
|
+
this.saveExecutionData(result);
|
|
99
|
+
const success = this.logResult(result, outputPath);
|
|
100
|
+
return success;
|
|
101
|
+
}
|
|
102
|
+
}
|