@forgehive/forge-cli 0.3.6 → 0.3.7

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.
@@ -45,10 +45,26 @@ function generateHash(input) {
45
45
  }
46
46
  return Math.abs(hash).toString(36);
47
47
  }
48
- // TypeScript AST analysis function
49
- function extractTaskFingerprints(sourceCode, filePath) {
48
+ // TypeScript AST analysis function with error collection
49
+ function extractTaskFingerprintsWithErrors(sourceCode, filePath, errors) {
50
+ try {
51
+ return extractTaskFingerprintsInternal(sourceCode, filePath, errors);
52
+ }
53
+ catch (error) {
54
+ errors.push({
55
+ type: 'parsing',
56
+ message: error instanceof Error ? error.message : 'TypeScript parsing failed',
57
+ location: { file: filePath },
58
+ details: { error: error instanceof Error ? error.stack : String(error) }
59
+ });
60
+ return [];
61
+ }
62
+ }
63
+ // TypeScript AST analysis function with error collection
64
+ function extractTaskFingerprintsInternal(sourceCode, filePath, errors) {
50
65
  const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true);
51
66
  const fingerprints = [];
67
+ const processedNodes = new Set(); // Track processed createTask nodes to prevent duplicates
52
68
  let schemaNode = null;
53
69
  let boundariesNode = null;
54
70
  // First pass: find schema and boundaries variable declarations
@@ -73,11 +89,14 @@ function extractTaskFingerprints(sourceCode, filePath) {
73
89
  if (ts.isCallExpression(node) &&
74
90
  ts.isIdentifier(node.expression) &&
75
91
  node.expression.text === 'createTask') {
76
- const taskName = extractTaskName(node, sourceFile);
77
- if (taskName) {
78
- const fingerprint = analyzeCreateTaskCall(node, sourceFile, filePath, taskName, schemaNode, boundariesNode);
79
- if (fingerprint) {
80
- fingerprints.push(fingerprint);
92
+ if (!processedNodes.has(node)) {
93
+ processedNodes.add(node);
94
+ const taskName = extractTaskName(node, sourceFile);
95
+ if (taskName) {
96
+ const fingerprint = analyzeCreateTaskCall(node, sourceFile, filePath, taskName, schemaNode, boundariesNode, errors);
97
+ if (fingerprint) {
98
+ fingerprints.push(fingerprint);
99
+ }
81
100
  }
82
101
  }
83
102
  }
@@ -89,10 +108,13 @@ function extractTaskFingerprints(sourceCode, filePath) {
89
108
  ts.isCallExpression(decl.initializer) &&
90
109
  ts.isIdentifier(decl.initializer.expression) &&
91
110
  decl.initializer.expression.text === 'createTask') {
92
- const taskName = ts.isIdentifier(decl.name) ? decl.name.text : 'unknown';
93
- const fingerprint = analyzeCreateTaskCall(decl.initializer, sourceFile, filePath, taskName, schemaNode, boundariesNode);
94
- if (fingerprint) {
95
- fingerprints.push(fingerprint);
111
+ if (!processedNodes.has(decl.initializer)) {
112
+ processedNodes.add(decl.initializer);
113
+ const taskName = ts.isIdentifier(decl.name) ? decl.name.text : 'unknown';
114
+ const fingerprint = analyzeCreateTaskCall(decl.initializer, sourceFile, filePath, taskName, schemaNode, boundariesNode, errors);
115
+ if (fingerprint) {
116
+ fingerprints.push(fingerprint);
117
+ }
96
118
  }
97
119
  }
98
120
  });
@@ -115,44 +137,109 @@ function extractTaskName(node, _sourceFile) {
115
137
  }
116
138
  return null;
117
139
  }
118
- function analyzeCreateTaskCall(node, sourceFile, filePath, taskName, schemaNode = null, boundariesNode = null) {
140
+ function analyzeCreateTaskCall(node, sourceFile, filePath, taskName, schemaNode = null, boundariesNode = null, errors = []) {
119
141
  try {
120
142
  const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
121
143
  const args = node.arguments;
122
- // Use pre-found schema or fall back to argument analysis
144
+ // Analyze createTask({ schema, boundaries, fn }) structure
123
145
  let inputSchema = { type: 'object', properties: {} };
124
- if (schemaNode) {
125
- inputSchema = analyzeSchemaArg(schemaNode, sourceFile);
126
- }
127
- else if (args[0]) {
128
- inputSchema = analyzeSchemaArg(args[0], sourceFile);
129
- }
130
- // Use pre-found boundaries or fall back to argument analysis - simplified to just names
131
146
  let boundaries = [];
132
- if (boundariesNode) {
133
- boundaries = analyzeBoundariesArg(boundariesNode, sourceFile);
134
- }
135
- else if (args[1]) {
136
- boundaries = analyzeBoundariesArg(args[1], sourceFile);
147
+ let boundaryTypes = new Map();
148
+ if (args[0] && ts.isObjectLiteralExpression(args[0])) {
149
+ const schemaProperty = args[0].properties.find(prop => ts.isPropertyAssignment(prop) &&
150
+ ts.isIdentifier(prop.name) &&
151
+ prop.name.text === 'schema');
152
+ const boundariesProperty = args[0].properties.find(prop => ts.isPropertyAssignment(prop) &&
153
+ ts.isIdentifier(prop.name) &&
154
+ prop.name.text === 'boundaries');
155
+ if (schemaNode) {
156
+ try {
157
+ inputSchema = analyzeSchemaArg(schemaNode, sourceFile);
158
+ }
159
+ catch (error) {
160
+ errors.push({
161
+ type: 'schema',
162
+ message: error instanceof Error ? error.message : 'Schema analysis failed',
163
+ location: { file: filePath, line: position.line + 1, column: position.character + 1 },
164
+ details: { taskName, schemaSource: 'variable' }
165
+ });
166
+ }
167
+ }
168
+ else if (schemaProperty && ts.isPropertyAssignment(schemaProperty)) {
169
+ try {
170
+ inputSchema = analyzeSchemaArg(schemaProperty.initializer, sourceFile);
171
+ }
172
+ catch (error) {
173
+ errors.push({
174
+ type: 'schema',
175
+ message: error instanceof Error ? error.message : 'Schema analysis failed',
176
+ location: { file: filePath, line: position.line + 1, column: position.character + 1 },
177
+ details: { taskName, schemaSource: 'property' }
178
+ });
179
+ }
180
+ }
181
+ if (boundariesNode) {
182
+ try {
183
+ const boundaryInfo = analyzeBoundariesWithTypes(boundariesNode, sourceFile);
184
+ boundaries = boundaryInfo.boundaries;
185
+ boundaryTypes = boundaryInfo.types;
186
+ }
187
+ catch (error) {
188
+ errors.push({
189
+ type: 'boundary',
190
+ message: error instanceof Error ? error.message : 'Boundary analysis failed',
191
+ location: { file: filePath, line: position.line + 1, column: position.character + 1 },
192
+ details: { taskName, boundarySource: 'variable' }
193
+ });
194
+ }
195
+ }
196
+ else if (boundariesProperty && ts.isPropertyAssignment(boundariesProperty)) {
197
+ try {
198
+ const boundaryInfo = analyzeBoundariesWithTypes(boundariesProperty.initializer, sourceFile);
199
+ boundaries = boundaryInfo.boundaries;
200
+ boundaryTypes = boundaryInfo.types;
201
+ }
202
+ catch (error) {
203
+ errors.push({
204
+ type: 'boundary',
205
+ message: error instanceof Error ? error.message : 'Boundary analysis failed',
206
+ location: { file: filePath, line: position.line + 1, column: position.character + 1 },
207
+ details: { taskName, boundarySource: 'property' }
208
+ });
209
+ }
210
+ }
137
211
  }
138
212
  // Extract function output type with better detection
139
213
  let outputType = { type: 'unknown' };
140
- const functionArg = args[2];
214
+ let functionArg;
215
+ // Extract function from createTask({ fn }) structure
216
+ if (args[0] && ts.isObjectLiteralExpression(args[0])) {
217
+ const fnProperty = args[0].properties.find(prop => ts.isPropertyAssignment(prop) &&
218
+ ts.isIdentifier(prop.name) &&
219
+ prop.name.text === 'fn');
220
+ if (fnProperty && ts.isPropertyAssignment(fnProperty)) {
221
+ functionArg = fnProperty.initializer;
222
+ }
223
+ }
141
224
  if (functionArg) {
142
225
  if (ts.isFunctionExpression(functionArg) || ts.isArrowFunction(functionArg)) {
226
+ // Collect errors from main task function
227
+ const mainFunctionErrors = analyzeMainTaskFunctionErrors(functionArg, sourceFile, taskName);
228
+ errors.push(...mainFunctionErrors);
143
229
  // Better return type extraction
144
230
  if (functionArg.type) {
145
231
  const typeString = cleanTypeString(functionArg.type.getText(sourceFile));
146
232
  outputType = { type: typeString };
147
233
  }
148
234
  else {
149
- // Try to infer from return statements with better object analysis
150
- outputType = inferDetailedReturnType(functionArg, sourceFile);
235
+ // Try to infer from return statements with boundary type information
236
+ outputType = inferDetailedReturnType(functionArg, sourceFile, boundaryTypes);
151
237
  }
152
238
  }
153
239
  }
154
240
  // Generate hash from task signature
155
- const hashInput = `${taskName}:${JSON.stringify(inputSchema)}:${JSON.stringify(boundaries)}`;
241
+ const boundaryNames = boundaries.map(b => b.name);
242
+ const hashInput = `${taskName}:${JSON.stringify(inputSchema)}:${JSON.stringify(boundaryNames)}`;
156
243
  const hash = generateHash(hashInput);
157
244
  return {
158
245
  name: taskName,
@@ -173,8 +260,35 @@ function analyzeCreateTaskCall(node, sourceFile, filePath, taskName, schemaNode
173
260
  }
174
261
  }
175
262
  // Enhanced return type inference with detailed object analysis
176
- function inferDetailedReturnType(func, _sourceFile) {
263
+ function inferDetailedReturnType(func, sourceFile, boundaryTypes = new Map()) {
177
264
  let returnType = { type: 'unknown' };
265
+ // First, collect variable declarations and their types within the function
266
+ const variableTypes = new Map();
267
+ function collectVariableDeclarations(node) {
268
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
269
+ const varName = node.name.text;
270
+ // Handle await expressions specially for boundary calls
271
+ if (ts.isAwaitExpression(node.initializer) &&
272
+ ts.isCallExpression(node.initializer.expression) &&
273
+ ts.isIdentifier(node.initializer.expression.expression)) {
274
+ const boundaryName = node.initializer.expression.expression.text;
275
+ const boundaryType = boundaryTypes.get(boundaryName);
276
+ if (boundaryType && typeof boundaryType === 'object') {
277
+ // Store the detailed type information
278
+ variableTypes.set(varName, boundaryType);
279
+ }
280
+ else {
281
+ const varType = inferTypeFromExpression(node.initializer, sourceFile, variableTypes, boundaryTypes);
282
+ variableTypes.set(varName, { type: varType });
283
+ }
284
+ }
285
+ else {
286
+ const varType = inferTypeFromExpression(node.initializer, sourceFile, variableTypes, boundaryTypes);
287
+ variableTypes.set(varName, { type: varType });
288
+ }
289
+ }
290
+ ts.forEachChild(node, collectVariableDeclarations);
291
+ }
178
292
  function visitReturnStatements(node) {
179
293
  if (ts.isReturnStatement(node) && node.expression) {
180
294
  if (ts.isObjectLiteralExpression(node.expression)) {
@@ -184,34 +298,67 @@ function inferDetailedReturnType(func, _sourceFile) {
184
298
  if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
185
299
  // Handle explicit property assignments: { propName: value }
186
300
  const propName = prop.name.text;
187
- let propType = 'unknown';
188
- // Try to infer property type from the initializer
189
- if (ts.isStringLiteral(prop.initializer)) {
190
- propType = 'string';
191
- }
192
- else if (ts.isNumericLiteral(prop.initializer)) {
193
- propType = 'number';
194
- }
195
- else if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
196
- prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
197
- propType = 'boolean';
301
+ // Check if it's a property access like result1.result
302
+ if (ts.isPropertyAccessExpression(prop.initializer)) {
303
+ const baseExpr = prop.initializer.expression;
304
+ const propertyName = prop.initializer.name.text;
305
+ if (ts.isIdentifier(baseExpr)) {
306
+ const baseVarType = variableTypes.get(baseExpr.text);
307
+ if (baseVarType && typeof baseVarType === 'object' && baseVarType.properties) {
308
+ const propertyType = baseVarType.properties[propertyName];
309
+ if (propertyType) {
310
+ properties[propName] = propertyType;
311
+ }
312
+ else {
313
+ properties[propName] = { type: 'unknown' };
314
+ }
315
+ }
316
+ else {
317
+ properties[propName] = { type: 'unknown' };
318
+ }
319
+ }
320
+ else {
321
+ properties[propName] = { type: 'unknown' };
322
+ }
198
323
  }
199
- else if (ts.isIdentifier(prop.initializer)) {
200
- // Variable reference - try to infer from name
201
- const varName = prop.initializer.text;
202
- propType = inferTypeFromVariableName(varName);
324
+ else {
325
+ const propType = inferTypeFromExpression(prop.initializer, sourceFile, variableTypes, boundaryTypes);
326
+ // Handle identifiers that might reference boundary call results
327
+ if (ts.isIdentifier(prop.initializer)) {
328
+ const varType = variableTypes.get(prop.initializer.text);
329
+ if (varType && typeof varType === 'object' && varType.type) {
330
+ properties[propName] = { type: varType.type };
331
+ }
332
+ else {
333
+ properties[propName] = { type: propType };
334
+ }
335
+ }
336
+ else {
337
+ properties[propName] = { type: propType };
338
+ }
203
339
  }
204
- else if (ts.isPropertyAccessExpression(prop.initializer)) {
205
- // Property access like response.handler
206
- propType = 'unknown';
207
- }
208
- properties[propName] = { type: propType };
209
340
  }
210
341
  else if (ts.isShorthandPropertyAssignment(prop)) {
211
342
  // Handle shorthand properties: { propName } (equivalent to { propName: propName })
212
343
  const propName = prop.name.text;
213
- const propType = inferTypeFromVariableName(propName);
214
- properties[propName] = { type: propType };
344
+ const varType = variableTypes.get(propName);
345
+ if (varType && typeof varType === 'object' && varType.type) {
346
+ // If we have detailed type information from boundary calls
347
+ if (varType.type === 'object' && varType.properties) {
348
+ properties[propName] = varType;
349
+ }
350
+ else if (varType.type === 'array' && varType.elementType) {
351
+ // Handle arrays with elementType information
352
+ properties[propName] = varType;
353
+ }
354
+ else {
355
+ properties[propName] = { type: varType.type };
356
+ }
357
+ }
358
+ else {
359
+ const propType = varType ? (varType.type || 'unknown') : inferTypeFromIdentifier(prop.name.text, boundaryTypes);
360
+ properties[propName] = { type: propType };
361
+ }
215
362
  }
216
363
  });
217
364
  if (Object.keys(properties).length > 0) {
@@ -236,43 +383,101 @@ function inferDetailedReturnType(func, _sourceFile) {
236
383
  }
237
384
  else if (ts.isIdentifier(node.expression)) {
238
385
  // Single variable return
239
- const varType = inferTypeFromVariableName(node.expression.text);
240
- returnType = { type: varType };
386
+ const varType = variableTypes.get(node.expression.text);
387
+ if (varType) {
388
+ returnType = varType;
389
+ }
390
+ else {
391
+ const inferredType = inferTypeFromIdentifier(node.expression.text, boundaryTypes);
392
+ returnType = { type: inferredType };
393
+ }
241
394
  }
242
395
  }
243
396
  ts.forEachChild(node, visitReturnStatements);
244
397
  }
245
398
  if (func.body) {
399
+ // First pass: collect variable declarations
400
+ collectVariableDeclarations(func.body);
401
+ // Second pass: analyze return statements
246
402
  visitReturnStatements(func.body);
247
403
  }
248
404
  return returnType;
249
405
  }
250
- // Helper function to infer types from variable names
251
- function inferTypeFromVariableName(varName) {
252
- // Common patterns for type inference based on variable names
253
- if (varName.includes('path') || varName.includes('Path') ||
254
- varName.includes('name') || varName.includes('Name') ||
255
- varName.includes('descriptor') || varName.includes('Descriptor') ||
256
- varName.includes('fileName') || varName.includes('handler') ||
257
- varName.includes('url') || varName.includes('id') ||
258
- varName.includes('uuid') || varName.includes('token')) {
406
+ // Helper function to infer type from any expression
407
+ function inferTypeFromExpression(expr, sourceFile, variableTypes, boundaryTypes = new Map()) {
408
+ if (ts.isStringLiteral(expr)) {
259
409
  return 'string';
260
410
  }
261
- else if (varName.includes('count') || varName.includes('Count') ||
262
- varName.includes('size') || varName.includes('Size') ||
263
- varName.includes('length') || varName.includes('Length') ||
264
- varName.includes('index') || varName.includes('Index')) {
411
+ else if (ts.isNumericLiteral(expr)) {
265
412
  return 'number';
266
413
  }
267
- else if (varName.includes('is') || varName.includes('has') ||
268
- varName.includes('can') || varName.includes('should') ||
269
- varName.includes('enabled') || varName.includes('success')) {
414
+ else if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword) {
270
415
  return 'boolean';
271
416
  }
272
- else if (varName.includes('config') || varName.includes('Config') ||
273
- varName.includes('options') || varName.includes('Options') ||
274
- varName.includes('data') || varName.includes('result') ||
275
- varName.includes('response') || varName.includes('error')) {
417
+ else if (ts.isArrayLiteralExpression(expr)) {
418
+ return 'array';
419
+ }
420
+ else if (ts.isObjectLiteralExpression(expr)) {
421
+ return 'object';
422
+ }
423
+ else if (ts.isIdentifier(expr)) {
424
+ // Check if we know the type from variable declarations
425
+ const varType = variableTypes.get(expr.text);
426
+ if (varType) {
427
+ return varType.type || 'unknown';
428
+ }
429
+ return inferTypeFromIdentifier(expr.text, boundaryTypes);
430
+ }
431
+ else if (ts.isCallExpression(expr)) {
432
+ // Handle method calls like array.reduce(), boundary calls, etc.
433
+ if (ts.isPropertyAccessExpression(expr.expression)) {
434
+ const methodName = expr.expression.name.text;
435
+ if (methodName === 'reduce') {
436
+ // For reduce, infer type from initial value (second argument)
437
+ if (expr.arguments.length > 1) {
438
+ const initialValue = expr.arguments[1];
439
+ return inferTypeFromExpression(initialValue, sourceFile, variableTypes, boundaryTypes);
440
+ }
441
+ return 'number'; // Common case for reduce operations
442
+ }
443
+ else if (methodName === 'map' || methodName === 'filter') {
444
+ // These typically return arrays
445
+ return 'array';
446
+ }
447
+ else if (methodName === 'join' || methodName === 'toString') {
448
+ // These return strings
449
+ return 'string';
450
+ }
451
+ else if (methodName === 'length' || methodName === 'indexOf' || methodName === 'findIndex') {
452
+ // These return numbers
453
+ return 'number';
454
+ }
455
+ }
456
+ else if (ts.isIdentifier(expr.expression)) {
457
+ // Direct function calls - could be boundary functions
458
+ const functionName = expr.expression.text;
459
+ const boundaryType = boundaryTypes.get(functionName);
460
+ if (boundaryType) {
461
+ // If boundaryType is an object with type info, return the type
462
+ if (typeof boundaryType === 'object' && boundaryType.type) {
463
+ return boundaryType.type;
464
+ }
465
+ return typeof boundaryType === 'string' ? boundaryType : 'unknown';
466
+ }
467
+ return 'unknown';
468
+ }
469
+ return 'unknown';
470
+ }
471
+ else if (ts.isAwaitExpression(expr)) {
472
+ // Handle await expressions - analyze the awaited expression
473
+ return inferTypeFromExpression(expr.expression, sourceFile, variableTypes, boundaryTypes);
474
+ }
475
+ else if (ts.isPropertyAccessExpression(expr)) {
476
+ // Handle property access like obj.prop - try to infer from base object
477
+ const baseType = inferTypeFromExpression(expr.expression, sourceFile, variableTypes, boundaryTypes);
478
+ if (baseType === 'object') {
479
+ return 'unknown'; // Could be any property type
480
+ }
276
481
  return 'unknown';
277
482
  }
278
483
  return 'unknown';
@@ -310,29 +515,38 @@ function analyzeSchemaArg(node, sourceFile) {
310
515
  return { type: 'object', properties: {} };
311
516
  }
312
517
  // Enhanced schema property analysis
313
- function analyzeSchemaProp(node, _sourceFile) {
518
+ function analyzeSchemaProp(node, sourceFile) {
314
519
  // Analyze Schema.string(), Schema.number(), etc.
315
520
  if (ts.isCallExpression(node)) {
316
521
  if (ts.isPropertyAccessExpression(node.expression) &&
317
522
  ts.isIdentifier(node.expression.expression) &&
318
523
  node.expression.expression.text === 'Schema') {
319
524
  const methodName = node.expression.name.text;
320
- let baseType = { type: getSchemaTypeFromMethod(methodName) };
321
- // Check for chained methods like .optional() or .default()
322
- let current = node;
323
- while (current.parent && ts.isCallExpression(current.parent)) {
324
- current = current.parent;
325
- if (ts.isPropertyAccessExpression(current.expression)) {
326
- const chainedMethod = current.expression.name.text;
327
- if (chainedMethod === 'optional') {
328
- baseType = { ...baseType, optional: true };
329
- }
330
- else if (chainedMethod === 'default' && current.arguments[0]) {
331
- baseType = { ...baseType, default: current.arguments[0].getText() };
332
- }
525
+ const baseType = { type: getSchemaTypeFromMethod(methodName) };
526
+ return baseType;
527
+ }
528
+ }
529
+ // Handle chained calls like Schema.number().optional()
530
+ if (ts.isCallExpression(node)) {
531
+ if (ts.isPropertyAccessExpression(node.expression)) {
532
+ const chainedMethod = node.expression.name.text;
533
+ if (chainedMethod === 'optional') {
534
+ // This is a .optional() call, get the base type
535
+ const baseCall = node.expression.expression;
536
+ if (ts.isCallExpression(baseCall)) {
537
+ const baseType = analyzeSchemaProp(baseCall, sourceFile);
538
+ return { ...baseType, optional: true };
539
+ }
540
+ }
541
+ else if (chainedMethod === 'default') {
542
+ // This is a .default() call, get the base type
543
+ const baseCall = node.expression.expression;
544
+ if (ts.isCallExpression(baseCall)) {
545
+ const baseType = analyzeSchemaProp(baseCall, sourceFile);
546
+ const defaultValue = node.arguments[0]?.getText() || 'undefined';
547
+ return { ...baseType, default: defaultValue };
333
548
  }
334
549
  }
335
- return baseType;
336
550
  }
337
551
  }
338
552
  return { type: 'unknown' };
@@ -347,34 +561,473 @@ function getSchemaTypeFromMethod(methodName) {
347
561
  };
348
562
  return typeMap[methodName] || 'unknown';
349
563
  }
350
- function analyzeBoundariesArg(node, _sourceFile) {
564
+ // Enhanced boundary analysis that extracts detailed boundary information
565
+ function analyzeBoundariesWithTypes(node, sourceFile) {
566
+ const names = [];
567
+ const types = new Map();
351
568
  const boundaries = [];
352
- // Handle variable references (e.g., when boundaries is defined as const boundaries = ...)
353
- if (ts.isIdentifier(node) && node.text === 'boundaries') {
354
- // This case is now handled by pre-finding the boundaries node
355
- return [];
356
- }
357
569
  if (ts.isObjectLiteralExpression(node)) {
358
570
  node.properties.forEach(prop => {
359
571
  if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
360
572
  const boundaryName = prop.name.text;
361
- boundaries.push(boundaryName);
573
+ names.push(boundaryName);
574
+ const boundaryErrors = [];
575
+ // Try to analyze the boundary function to extract input and output types
576
+ if (ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer)) {
577
+ try {
578
+ // Analyze input parameters with error collection
579
+ const inputAnalysis = analyzeBoundaryInputTypesWithErrors(prop.initializer, sourceFile, boundaryName);
580
+ boundaryErrors.push(...inputAnalysis.errors);
581
+ // Analyze return type with error collection
582
+ const returnAnalysis = analyzeBoundaryReturnTypeWithErrors(prop.initializer, sourceFile, boundaryName);
583
+ boundaryErrors.push(...returnAnalysis.errors);
584
+ types.set(boundaryName, returnAnalysis.returnType);
585
+ // Validate boundary structure and collect structural errors
586
+ const structuralErrors = validateBoundaryStructure(prop.initializer, sourceFile, boundaryName);
587
+ boundaryErrors.push(...structuralErrors);
588
+ // Create boundary fingerprint
589
+ const boundaryFingerprint = {
590
+ name: boundaryName,
591
+ input: inputAnalysis.inputTypes,
592
+ output: returnAnalysis.returnType,
593
+ errors: boundaryErrors
594
+ };
595
+ boundaries.push(boundaryFingerprint);
596
+ }
597
+ catch (error) {
598
+ boundaryErrors.push({
599
+ type: 'boundary',
600
+ message: error instanceof Error ? error.message : 'Boundary analysis failed',
601
+ location: { file: sourceFile.fileName },
602
+ details: { boundaryName, errorType: 'critical_analysis_failure' }
603
+ });
604
+ // Create boundary fingerprint with error
605
+ const boundaryFingerprint = {
606
+ name: boundaryName,
607
+ input: [],
608
+ output: { type: 'unknown' },
609
+ errors: boundaryErrors
610
+ };
611
+ boundaries.push(boundaryFingerprint);
612
+ }
613
+ }
614
+ else {
615
+ // Not a function - this is an error
616
+ boundaryErrors.push({
617
+ type: 'boundary',
618
+ message: 'Boundary is not a function',
619
+ location: { file: sourceFile.fileName },
620
+ details: { boundaryName, nodeType: ts.SyntaxKind[prop.initializer.kind] }
621
+ });
622
+ const boundaryFingerprint = {
623
+ name: boundaryName,
624
+ input: [],
625
+ output: { type: 'unknown' },
626
+ errors: boundaryErrors
627
+ };
628
+ boundaries.push(boundaryFingerprint);
629
+ }
362
630
  }
363
631
  });
364
632
  }
365
- return boundaries;
633
+ return { names, types, boundaries };
634
+ }
635
+ // Analyze boundary function input parameter types with error collection
636
+ function analyzeBoundaryInputTypesWithErrors(func, sourceFile, boundaryName) {
637
+ const inputTypes = [];
638
+ const errors = [];
639
+ if (func.parameters) {
640
+ func.parameters.forEach((param, index) => {
641
+ if (ts.isIdentifier(param.name)) {
642
+ const paramName = param.name.text;
643
+ if (param.type) {
644
+ try {
645
+ const typeText = param.type.getText(sourceFile);
646
+ const schemaProperty = parseTypeToSchemaProperty(typeText);
647
+ inputTypes.push({ ...schemaProperty, name: paramName });
648
+ }
649
+ catch (error) {
650
+ errors.push({
651
+ type: 'boundary',
652
+ message: `Failed to parse parameter type for '${paramName}': ${error instanceof Error ? error.message : 'Unknown error'}`,
653
+ location: { file: sourceFile.fileName },
654
+ details: { boundaryName, parameterName: paramName, parameterIndex: index }
655
+ });
656
+ inputTypes.push({ name: paramName, type: 'unknown' });
657
+ }
658
+ }
659
+ else {
660
+ // No type annotation - just use unknown
661
+ inputTypes.push({ name: paramName, type: 'unknown' });
662
+ }
663
+ }
664
+ else {
665
+ // Complex parameter patterns (destructuring, etc.) - just use unknown
666
+ inputTypes.push({ name: `param${index}`, type: 'unknown' });
667
+ }
668
+ });
669
+ // Only check for obvious parameter issues
670
+ if (func.parameters.length === 0) {
671
+ errors.push({
672
+ type: 'boundary',
673
+ message: 'Boundary function has no parameters',
674
+ location: { file: sourceFile.fileName },
675
+ details: { boundaryName, issue: 'no_parameters' }
676
+ });
677
+ }
678
+ }
679
+ return { inputTypes, errors };
680
+ }
681
+ // Helper function to convert TypeScript type text to SchemaProperty
682
+ function parseTypeToSchemaProperty(typeText) {
683
+ // Remove whitespace
684
+ const cleanType = typeText.trim();
685
+ if (cleanType === 'string') {
686
+ return { type: 'string' };
687
+ }
688
+ else if (cleanType === 'number') {
689
+ return { type: 'number' };
690
+ }
691
+ else if (cleanType === 'boolean') {
692
+ return { type: 'boolean' };
693
+ }
694
+ else if (cleanType.includes('[]') || cleanType.includes('Array<')) {
695
+ return { type: 'array' };
696
+ }
697
+ else if (cleanType.includes('{') && cleanType.includes('}')) {
698
+ // Parse object type structure
699
+ const objectMatch = cleanType.match(/^\s*\{\s*(.+)\s*\}\s*$/);
700
+ if (objectMatch) {
701
+ const properties = {};
702
+ const propsString = objectMatch[1];
703
+ // Split by commas and semicolons
704
+ const propPairs = propsString.split(/[,;]/).map(s => s.trim());
705
+ for (const propPair of propPairs) {
706
+ const colonIndex = propPair.indexOf(':');
707
+ if (colonIndex > 0) {
708
+ const propName = propPair.substring(0, colonIndex).trim();
709
+ const propType = propPair.substring(colonIndex + 1).trim();
710
+ properties[propName] = parseTypeToSchemaProperty(propType);
711
+ }
712
+ }
713
+ return {
714
+ type: 'object',
715
+ properties
716
+ };
717
+ }
718
+ return { type: 'object' };
719
+ }
720
+ else {
721
+ return { type: cleanType };
722
+ }
723
+ }
724
+ // Helper function to infer type from variable names using TypeScript compiler analysis
725
+ function inferTypeFromIdentifier(identifierText, boundaryTypes) {
726
+ // Check boundary types first
727
+ if (boundaryTypes.has(identifierText)) {
728
+ const boundaryType = boundaryTypes.get(identifierText);
729
+ if (typeof boundaryType === 'object' && boundaryType.type) {
730
+ return boundaryType.type;
731
+ }
732
+ return (typeof boundaryType === 'object' && boundaryType?.type) ? boundaryType.type : 'unknown';
733
+ }
734
+ // Use TypeScript's built-in type inference instead of hardcoded patterns
735
+ // For now, return unknown and let the compiler handle it
736
+ return 'unknown';
737
+ }
738
+ // Analyze boundary function return type with error collection
739
+ function analyzeBoundaryReturnTypeWithErrors(func, sourceFile, boundaryName) {
740
+ const errors = [];
741
+ // Check if function has explicit return type annotation
742
+ if (func.type) {
743
+ try {
744
+ const typeText = func.type.getText(sourceFile);
745
+ // Handle Promise<T> types - extract T
746
+ const promiseMatch = typeText.match(/Promise<(.+)>/);
747
+ if (promiseMatch) {
748
+ const innerType = promiseMatch[1];
749
+ // Just process the type without validation
750
+ // Parse detailed type patterns
751
+ if (innerType.includes('[]') || innerType.includes('Array<')) {
752
+ // Extract array element type
753
+ let elementType = 'unknown';
754
+ if (innerType.includes('[]')) {
755
+ // Handle T[] format
756
+ const elementTypeMatch = innerType.match(/^(.+)\[\]$/);
757
+ if (elementTypeMatch) {
758
+ const rawElementType = elementTypeMatch[1].trim();
759
+ // Parse object element types
760
+ if (rawElementType.includes('{') && rawElementType.includes('}')) {
761
+ const objectType = parseObjectTypeFromString(rawElementType);
762
+ if (objectType && objectType.properties) {
763
+ return {
764
+ returnType: {
765
+ type: 'array',
766
+ elementType: {
767
+ type: 'object',
768
+ properties: objectType.properties
769
+ }
770
+ },
771
+ errors
772
+ };
773
+ }
774
+ }
775
+ elementType = rawElementType;
776
+ }
777
+ }
778
+ else if (innerType.includes('Array<')) {
779
+ // Handle Array<T> format
780
+ const elementTypeMatch = innerType.match(/Array<(.+)>/);
781
+ if (elementTypeMatch) {
782
+ elementType = elementTypeMatch[1].trim();
783
+ }
784
+ }
785
+ return {
786
+ returnType: {
787
+ type: 'array',
788
+ elementType: elementType === 'unknown' ? undefined : { type: elementType }
789
+ },
790
+ errors
791
+ };
792
+ }
793
+ else if (innerType === 'string') {
794
+ return { returnType: { type: 'string' }, errors };
795
+ }
796
+ else if (innerType === 'number') {
797
+ return { returnType: { type: 'number' }, errors };
798
+ }
799
+ else if (innerType === 'boolean') {
800
+ return { returnType: { type: 'boolean' }, errors };
801
+ }
802
+ else if (innerType.includes('{') && innerType.includes('}')) {
803
+ // Try to parse object type structure from string
804
+ try {
805
+ const parsedType = parseObjectTypeFromString(innerType);
806
+ return { returnType: parsedType, errors };
807
+ }
808
+ catch (parseError) {
809
+ errors.push({
810
+ type: 'boundary',
811
+ message: `Failed to parse return type object structure: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`,
812
+ location: { file: sourceFile.fileName },
813
+ details: { boundaryName, returnType: innerType, issue: 'object_parse_failure' }
814
+ });
815
+ return { returnType: { type: 'object' }, errors };
816
+ }
817
+ }
818
+ }
819
+ else {
820
+ // Not a Promise type - just use the type as-is
821
+ }
822
+ return { returnType: { type: cleanTypeString(typeText) }, errors };
823
+ }
824
+ catch (error) {
825
+ errors.push({
826
+ type: 'boundary',
827
+ message: `Failed to analyze return type: ${error instanceof Error ? error.message : 'Unknown error'}`,
828
+ location: { file: sourceFile.fileName },
829
+ details: { boundaryName, issue: 'return_type_analysis_failure' }
830
+ });
831
+ return { returnType: { type: 'unknown' }, errors };
832
+ }
833
+ }
834
+ else {
835
+ // No return type annotation - will try to infer
836
+ }
837
+ // If no explicit type, try to infer from return statements
838
+ if (func.body) {
839
+ try {
840
+ const returnType = inferDetailedReturnType(func, sourceFile, new Map());
841
+ return { returnType, errors };
842
+ }
843
+ catch (error) {
844
+ errors.push({
845
+ type: 'boundary',
846
+ message: `Failed to infer return type from function body: ${error instanceof Error ? error.message : 'Unknown error'}`,
847
+ location: { file: sourceFile.fileName },
848
+ details: { boundaryName, issue: 'return_type_inference_failure' }
849
+ });
850
+ }
851
+ }
852
+ return { returnType: { type: 'unknown' }, errors };
853
+ }
854
+ // Helper function to check for throw statements in boundary functions
855
+ function checkForBoundaryThrowStatements(node, sourceFile, errors) {
856
+ if (ts.isThrowStatement(node)) {
857
+ let errorMessage = 'Boundary function contains throw statement';
858
+ // Try to extract error message if it's a simple throw new Error('message')
859
+ if (node.expression && ts.isNewExpression(node.expression)) {
860
+ if (ts.isIdentifier(node.expression.expression) &&
861
+ node.expression.expression.text === 'Error' &&
862
+ node.expression.arguments &&
863
+ node.expression.arguments.length > 0) {
864
+ const errorArg = node.expression.arguments[0];
865
+ if (ts.isStringLiteral(errorArg)) {
866
+ errorMessage = `Boundary function throws: ${errorArg.text}`;
867
+ }
868
+ }
869
+ }
870
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
871
+ errors.push({
872
+ type: 'boundary',
873
+ message: errorMessage,
874
+ location: {
875
+ file: sourceFile.fileName,
876
+ line: position.line + 1,
877
+ column: position.character + 1
878
+ }
879
+ });
880
+ }
881
+ ts.forEachChild(node, (childNode) => checkForBoundaryThrowStatements(childNode, sourceFile, errors));
882
+ }
883
+ // Validate boundary structure and collect structural errors (simplified to focus on runtime errors)
884
+ function validateBoundaryStructure(func, sourceFile, _boundaryName) {
885
+ const errors = [];
886
+ // Check function body for potential runtime errors
887
+ if (func.body) {
888
+ if (ts.isBlock(func.body)) {
889
+ checkForBoundaryThrowStatements(func.body, sourceFile, errors);
890
+ }
891
+ }
892
+ return errors;
893
+ }
894
+ // Helper function to check for throw statements in main task functions
895
+ function checkForMainTaskThrowStatements(node, sourceFile, errors) {
896
+ if (ts.isThrowStatement(node)) {
897
+ let errorMessage = 'Main task function contains throw statement';
898
+ // Try to extract error message if it's a simple throw new Error('message')
899
+ if (node.expression && ts.isNewExpression(node.expression)) {
900
+ if (ts.isIdentifier(node.expression.expression) &&
901
+ node.expression.expression.text === 'Error' &&
902
+ node.expression.arguments &&
903
+ node.expression.arguments.length > 0) {
904
+ const errorArg = node.expression.arguments[0];
905
+ if (ts.isStringLiteral(errorArg)) {
906
+ errorMessage = `Main task function throws: ${errorArg.text}`;
907
+ }
908
+ else if (ts.isTemplateExpression(errorArg)) {
909
+ // Handle template literals like `User with ID ${userId} not found`
910
+ const templateText = errorArg.getText(sourceFile);
911
+ errorMessage = `Main task function throws: ${templateText.replace(/`/g, '')}`;
912
+ }
913
+ }
914
+ }
915
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
916
+ errors.push({
917
+ type: 'analysis',
918
+ message: errorMessage,
919
+ location: {
920
+ file: sourceFile.fileName,
921
+ line: position.line + 1,
922
+ column: position.character + 1
923
+ }
924
+ });
925
+ }
926
+ ts.forEachChild(node, (childNode) => checkForMainTaskThrowStatements(childNode, sourceFile, errors));
927
+ }
928
+ // Analyze main task function for throw statements and runtime errors
929
+ function analyzeMainTaskFunctionErrors(func, sourceFile, _taskName) {
930
+ const errors = [];
931
+ // Check function body for potential runtime errors
932
+ if (func.body) {
933
+ if (ts.isBlock(func.body)) {
934
+ checkForMainTaskThrowStatements(func.body, sourceFile, errors);
935
+ }
936
+ }
937
+ return errors;
938
+ }
939
+ // Helper function to parse object type structure from type string
940
+ function parseObjectTypeFromString(typeString) {
941
+ // Simple parsing for common object patterns like "{ result: string }"
942
+ const objectMatch = typeString.match(/^\s*\{\s*(.+)\s*\}\s*$/);
943
+ if (objectMatch) {
944
+ const properties = {};
945
+ const propsString = objectMatch[1];
946
+ // Split by commas (simple approach - doesn't handle nested objects)
947
+ const propPairs = propsString.split(',').map(s => s.trim());
948
+ for (const propPair of propPairs) {
949
+ const colonIndex = propPair.indexOf(':');
950
+ if (colonIndex > 0) {
951
+ const propName = propPair.substring(0, colonIndex).trim();
952
+ const propType = propPair.substring(colonIndex + 1).trim();
953
+ if (propType === 'string') {
954
+ properties[propName] = { type: 'string' };
955
+ }
956
+ else if (propType === 'number') {
957
+ properties[propName] = { type: 'number' };
958
+ }
959
+ else if (propType === 'boolean') {
960
+ properties[propName] = { type: 'boolean' };
961
+ }
962
+ else if (propType.includes('[]')) {
963
+ properties[propName] = { type: 'array' };
964
+ }
965
+ else {
966
+ properties[propName] = { type: propType };
967
+ }
968
+ }
969
+ }
970
+ return {
971
+ type: 'object',
972
+ properties
973
+ };
974
+ }
975
+ return { type: 'object' };
366
976
  }
367
977
  // Export the core analysis function for reuse
368
978
  function analyzeTaskFile(sourceCode, filePath, _expectedTaskName) {
369
- const taskFingerprint = extractTaskFingerprints(sourceCode, filePath)[0];
370
- if (!taskFingerprint) {
371
- return null;
372
- }
373
- // Return simplified output without name, location, hash
374
- return {
375
- description: taskFingerprint.description,
376
- inputSchema: taskFingerprint.inputSchema,
377
- outputType: taskFingerprint.outputType,
378
- boundaries: taskFingerprint.boundaries
979
+ const errors = [];
980
+ const analysisMetadata = {
981
+ timestamp: new Date().toISOString(),
982
+ filePath,
983
+ success: true,
984
+ analysisVersion: '1.0.0'
379
985
  };
986
+ try {
987
+ const taskFingerprints = extractTaskFingerprintsWithErrors(sourceCode, filePath, errors);
988
+ const taskFingerprint = taskFingerprints[0];
989
+ if (!taskFingerprint) {
990
+ errors.push({
991
+ type: 'analysis',
992
+ message: 'No task fingerprint found in file',
993
+ location: { file: filePath },
994
+ details: { reason: 'No createTask calls detected' }
995
+ });
996
+ analysisMetadata.success = false;
997
+ return {
998
+ description: undefined,
999
+ inputSchema: { type: 'object', properties: {} },
1000
+ outputType: { type: 'unknown' },
1001
+ boundaries: [],
1002
+ errors,
1003
+ analysisMetadata
1004
+ };
1005
+ }
1006
+ // Return simplified output without name, location, hash
1007
+ return {
1008
+ description: taskFingerprint.description,
1009
+ inputSchema: taskFingerprint.inputSchema,
1010
+ outputType: taskFingerprint.outputType,
1011
+ boundaries: taskFingerprint.boundaries,
1012
+ errors,
1013
+ analysisMetadata
1014
+ };
1015
+ }
1016
+ catch (error) {
1017
+ errors.push({
1018
+ type: 'parsing',
1019
+ message: error instanceof Error ? error.message : 'Unknown parsing error',
1020
+ location: { file: filePath },
1021
+ details: { error: error instanceof Error ? error.stack : String(error) }
1022
+ });
1023
+ analysisMetadata.success = false;
1024
+ return {
1025
+ description: undefined,
1026
+ inputSchema: { type: 'object', properties: {} },
1027
+ outputType: { type: 'unknown' },
1028
+ boundaries: [],
1029
+ errors,
1030
+ analysisMetadata
1031
+ };
1032
+ }
380
1033
  }