@forgehive/forge-cli 0.2.14 → 0.3.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.
Files changed (67) hide show
  1. package/dist/runner.js +3 -1
  2. package/dist/tasks/auth/add.js +23 -19
  3. package/dist/tasks/auth/list.js +20 -16
  4. package/dist/tasks/auth/load.js +19 -15
  5. package/dist/tasks/auth/loadCurrent.js +13 -9
  6. package/dist/tasks/auth/remove.js +30 -26
  7. package/dist/tasks/auth/switch.js +19 -15
  8. package/dist/tasks/bundle/create.js +16 -12
  9. package/dist/tasks/bundle/fingerprint.d.ts +36 -0
  10. package/dist/tasks/bundle/fingerprint.js +164 -0
  11. package/dist/tasks/bundle/load.js +9 -5
  12. package/dist/tasks/bundle/zip.js +49 -45
  13. package/dist/tasks/conf/info.js +23 -19
  14. package/dist/tasks/conf/load.js +8 -4
  15. package/dist/tasks/fixture/download.js +40 -36
  16. package/dist/tasks/init.js +35 -31
  17. package/dist/tasks/runner/bundle.js +34 -30
  18. package/dist/tasks/runner/create.js +28 -24
  19. package/dist/tasks/runner/remove.js +22 -18
  20. package/dist/tasks/task/createTask.js +35 -28
  21. package/dist/tasks/task/describe.js +85 -81
  22. package/dist/tasks/task/download.js +63 -59
  23. package/dist/tasks/task/fingerprint.d.ts +26 -0
  24. package/dist/tasks/task/fingerprint.js +87 -0
  25. package/dist/tasks/task/list.js +27 -23
  26. package/dist/tasks/task/publish.js +72 -68
  27. package/dist/tasks/task/remove.js +24 -20
  28. package/dist/tasks/task/replay.js +94 -90
  29. package/dist/tasks/task/run.js +84 -79
  30. package/dist/test/tasks/create.test.js +6 -5
  31. package/dist/utils/taskAnalysis.d.ts +21 -0
  32. package/dist/utils/taskAnalysis.js +380 -0
  33. package/forge.json +12 -0
  34. package/logs/task:fingerprint.log +10 -0
  35. package/package.json +7 -7
  36. package/specs/fingerprint.md +380 -0
  37. package/src/runner.ts +3 -1
  38. package/src/tasks/README.md +13 -13
  39. package/src/tasks/auth/add.ts +3 -3
  40. package/src/tasks/auth/list.ts +3 -3
  41. package/src/tasks/auth/load.ts +3 -3
  42. package/src/tasks/auth/loadCurrent.ts +3 -3
  43. package/src/tasks/auth/remove.ts +3 -3
  44. package/src/tasks/auth/switch.ts +3 -3
  45. package/src/tasks/bundle/README.md +7 -7
  46. package/src/tasks/bundle/create.ts +4 -4
  47. package/src/tasks/bundle/fingerprint.ts +218 -0
  48. package/src/tasks/bundle/load.ts +4 -4
  49. package/src/tasks/bundle/zip.ts +3 -3
  50. package/src/tasks/conf/info.ts +3 -3
  51. package/src/tasks/conf/load.ts +3 -3
  52. package/src/tasks/fixture/download.ts +3 -3
  53. package/src/tasks/init.ts +3 -3
  54. package/src/tasks/runner/bundle.ts +3 -3
  55. package/src/tasks/runner/create.ts +3 -3
  56. package/src/tasks/runner/remove.ts +3 -3
  57. package/src/tasks/task/createTask.ts +10 -7
  58. package/src/tasks/task/describe.ts +3 -3
  59. package/src/tasks/task/download.ts +3 -3
  60. package/src/tasks/task/fingerprint.ts +107 -0
  61. package/src/tasks/task/list.ts +3 -3
  62. package/src/tasks/task/publish.ts +3 -3
  63. package/src/tasks/task/remove.ts +3 -3
  64. package/src/tasks/task/replay.ts +3 -3
  65. package/src/tasks/task/run.ts +5 -4
  66. package/src/test/tasks/create.test.ts +9 -9
  67. package/src/utils/taskAnalysis.ts +419 -0
@@ -96,10 +96,10 @@ const boundaries = {
96
96
  }
97
97
  }
98
98
 
99
- export const replay = createTask(
99
+ export const replay = createTask({
100
100
  schema,
101
101
  boundaries,
102
- async function ({ descriptorName, path: fixturePath, cache }, { readFixture, loadConf, loadCurrentProfile, bundleCreate, bundleLoad, ensureBuildsFolder, verifyLogFolder, sendLogToAPI }) {
102
+ fn: async function ({ descriptorName, path: fixturePath, cache }, { readFixture, loadConf, loadCurrentProfile, bundleCreate, bundleLoad, ensureBuildsFolder, verifyLogFolder, sendLogToAPI }) {
103
103
  console.log('Input descriptorName:', descriptorName)
104
104
  console.log('Input path:', fixturePath)
105
105
  console.log('Input cache:', cache)
@@ -215,6 +215,6 @@ export const replay = createTask(
215
215
 
216
216
  return result
217
217
  }
218
- )
218
+ })
219
219
 
220
220
  replay.setDescription(description)
@@ -1,6 +1,7 @@
1
1
  // TASK: run
2
2
  // Run this task with:
3
- // shadow-cli task:run
3
+ // most recursive call on the project
4
+ // forge task:run task:run
4
5
 
5
6
  import path from 'path'
6
7
  import fs from 'fs/promises'
@@ -78,10 +79,10 @@ const boundaries = {
78
79
  }
79
80
  }
80
81
 
81
- export const run = createTask(
82
+ export const run = createTask({
82
83
  schema,
83
84
  boundaries,
84
- async function ({ descriptorName, args }, {
85
+ fn: async function ({ descriptorName, args }, {
85
86
  loadConf,
86
87
  bundleCreate,
87
88
  bundleLoad,
@@ -187,4 +188,4 @@ export const run = createTask(
187
188
 
188
189
  return result
189
190
  }
190
- )
191
+ })
@@ -1,5 +1,5 @@
1
1
  import { createTaskCommand } from '../../tasks/task/createTask'
2
- import { createFsFromVolume, Volume } from 'memfs'
2
+ import { createFsFromVolume, Volume, type IFs } from 'memfs'
3
3
  import path from 'path'
4
4
  import { createMockBoundary } from '../testUtils'
5
5
  import { ForgeConf } from '../../tasks/types'
@@ -12,6 +12,7 @@ const expectedContent = `// TASK: newTask
12
12
  import { createTask } from '@forgehive/task'
13
13
  import { Schema } from '@forgehive/schema'
14
14
 
15
+ const name = 'sample:newTask'
15
16
  const description = 'Add task description here'
16
17
 
17
18
  const schema = new Schema({
@@ -24,10 +25,12 @@ const boundaries = {
24
25
  // example: readFile: async (path: string) => fs.readFile(path, 'utf-8')
25
26
  }
26
27
 
27
- export const newTask = createTask(
28
+ export const newTask = createTask({
29
+ name,
30
+ description,
28
31
  schema,
29
32
  boundaries,
30
- async function (argv, boundaries) {
33
+ fn: async function (argv, boundaries) {
31
34
  console.log('input:', argv)
32
35
  console.log('boundaries:', boundaries)
33
36
  // Your task implementation goes here
@@ -35,15 +38,13 @@ export const newTask = createTask(
35
38
 
36
39
  return status
37
40
  }
38
- )
41
+ })
39
42
 
40
- newTask.setDescription(description)
41
43
  `
42
44
 
43
45
  describe('Create task', () => {
44
46
  let volume: InstanceType<typeof Volume>
45
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
- let fs: any
47
+ let fs: IFs
47
48
  let rootDir: string
48
49
 
49
50
  beforeEach(() => {
@@ -96,11 +97,10 @@ describe('Create task', () => {
96
97
 
97
98
  // Read the created task file
98
99
  const fileContent = await fs.promises.readFile(path.join(rootDir, 'src/tasks/sample', 'newTask.ts'), 'utf-8')
99
-
100
100
  expect(fileContent).toBe(expectedContent)
101
101
 
102
102
  // Read the updated forge.json
103
- const forgeContent = await fs.promises.readFile(path.join(rootDir, 'forge.json'), 'utf-8')
103
+ const forgeContent = await fs.promises.readFile(path.join(rootDir, 'forge.json'), 'utf-8') as string
104
104
  const forgeConf = JSON.parse(forgeContent)
105
105
  expect(forgeConf.tasks['sample:newTask']).toBeDefined()
106
106
  })
@@ -0,0 +1,419 @@
1
+ import * as ts from 'typescript'
2
+
3
+ interface TaskLocation {
4
+ file: string
5
+ line: number
6
+ column: number
7
+ }
8
+
9
+ interface SchemaProperty {
10
+ type: string
11
+ optional?: boolean
12
+ default?: string
13
+ }
14
+
15
+ interface InputSchema {
16
+ type: string
17
+ properties: Record<string, SchemaProperty>
18
+ }
19
+
20
+ interface OutputType {
21
+ type: string
22
+ properties?: Record<string, SchemaProperty>
23
+ }
24
+
25
+ interface TaskFingerprint {
26
+ name: string
27
+ description?: string
28
+ location: TaskLocation
29
+ inputSchema: InputSchema
30
+ outputType: OutputType
31
+ boundaries: string[]
32
+ hash: string
33
+ }
34
+
35
+ // Simplified interface for filesystem output (excludes name, location, hash)
36
+ export interface TaskFingerprintOutput {
37
+ description?: string
38
+ inputSchema: InputSchema
39
+ outputType: OutputType
40
+ boundaries: string[]
41
+ }
42
+
43
+ // Hash generation function
44
+ function generateHash(input: string): string {
45
+ let hash = 0
46
+ for (let i = 0; i < input.length; i++) {
47
+ const char = input.charCodeAt(i)
48
+ hash = ((hash << 5) - hash) + char
49
+ hash = hash & hash // Convert to 32-bit integer
50
+ }
51
+ return Math.abs(hash).toString(36)
52
+ }
53
+
54
+ // TypeScript AST analysis function
55
+ function extractTaskFingerprints(sourceCode: string, filePath: string): TaskFingerprint[] {
56
+ const sourceFile = ts.createSourceFile(
57
+ filePath,
58
+ sourceCode,
59
+ ts.ScriptTarget.Latest,
60
+ true
61
+ )
62
+
63
+ const fingerprints: TaskFingerprint[] = []
64
+ let schemaNode: ts.Expression | null = null
65
+ let boundariesNode: ts.Expression | null = null
66
+
67
+ // First pass: find schema and boundaries variable declarations
68
+ function findVariables(node: ts.Node): void {
69
+ if (ts.isVariableStatement(node)) {
70
+ node.declarationList.declarations.forEach(decl => {
71
+ if (ts.isIdentifier(decl.name)) {
72
+ if (decl.name.text === 'schema' && decl.initializer) {
73
+ schemaNode = decl.initializer
74
+ } else if (decl.name.text === 'boundaries' && decl.initializer) {
75
+ boundariesNode = decl.initializer
76
+ }
77
+ }
78
+ })
79
+ }
80
+ ts.forEachChild(node, findVariables)
81
+ }
82
+
83
+ // Second pass: find createTask calls
84
+ function findCreateTask(node: ts.Node): void {
85
+ // Look for createTask calls
86
+ if (ts.isCallExpression(node) &&
87
+ ts.isIdentifier(node.expression) &&
88
+ node.expression.text === 'createTask') {
89
+
90
+ const taskName = extractTaskName(node, sourceFile)
91
+ if (taskName) {
92
+ const fingerprint = analyzeCreateTaskCall(node, sourceFile, filePath, taskName, schemaNode, boundariesNode)
93
+ if (fingerprint) {
94
+ fingerprints.push(fingerprint)
95
+ }
96
+ }
97
+ }
98
+
99
+ // Look for exported createTask assignments
100
+ if (ts.isVariableStatement(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
101
+ node.declarationList.declarations.forEach(decl => {
102
+ if (ts.isVariableDeclaration(decl) &&
103
+ decl.initializer &&
104
+ ts.isCallExpression(decl.initializer) &&
105
+ ts.isIdentifier(decl.initializer.expression) &&
106
+ decl.initializer.expression.text === 'createTask') {
107
+
108
+ const taskName = ts.isIdentifier(decl.name) ? decl.name.text : 'unknown'
109
+ const fingerprint = analyzeCreateTaskCall(decl.initializer, sourceFile, filePath, taskName, schemaNode, boundariesNode)
110
+ if (fingerprint) {
111
+ fingerprints.push(fingerprint)
112
+ }
113
+ }
114
+ })
115
+ }
116
+
117
+ ts.forEachChild(node, findCreateTask)
118
+ }
119
+
120
+ // Execute both passes
121
+ findVariables(sourceFile)
122
+ findCreateTask(sourceFile)
123
+
124
+ return fingerprints
125
+ }
126
+
127
+ function extractTaskName(node: ts.CallExpression, _sourceFile: ts.SourceFile): string | null {
128
+ // Try to find the task name from variable assignment or export
129
+ let parent = node.parent
130
+ while (parent) {
131
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
132
+ return parent.name.text
133
+ }
134
+ parent = parent.parent
135
+ }
136
+ return null
137
+ }
138
+
139
+ function analyzeCreateTaskCall(
140
+ node: ts.CallExpression,
141
+ sourceFile: ts.SourceFile,
142
+ filePath: string,
143
+ taskName: string,
144
+ schemaNode: ts.Expression | null = null,
145
+ boundariesNode: ts.Expression | null = null
146
+ ): TaskFingerprint | null {
147
+ try {
148
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart())
149
+ const args = node.arguments
150
+
151
+ // Use pre-found schema or fall back to argument analysis
152
+ let inputSchema: InputSchema = { type: 'object', properties: {} }
153
+ if (schemaNode) {
154
+ inputSchema = analyzeSchemaArg(schemaNode, sourceFile)
155
+ } else if (args[0]) {
156
+ inputSchema = analyzeSchemaArg(args[0], sourceFile)
157
+ }
158
+
159
+ // Use pre-found boundaries or fall back to argument analysis - simplified to just names
160
+ let boundaries: string[] = []
161
+ if (boundariesNode) {
162
+ boundaries = analyzeBoundariesArg(boundariesNode, sourceFile)
163
+ } else if (args[1]) {
164
+ boundaries = analyzeBoundariesArg(args[1], sourceFile)
165
+ }
166
+
167
+ // Extract function output type with better detection
168
+ let outputType: OutputType = { type: 'unknown' }
169
+ const functionArg = args[2]
170
+
171
+ if (functionArg) {
172
+ if (ts.isFunctionExpression(functionArg) || ts.isArrowFunction(functionArg)) {
173
+ // Better return type extraction
174
+ if (functionArg.type) {
175
+ const typeString = cleanTypeString(functionArg.type.getText(sourceFile))
176
+ outputType = { type: typeString }
177
+ } else {
178
+ // Try to infer from return statements with better object analysis
179
+ outputType = inferDetailedReturnType(functionArg, sourceFile)
180
+ }
181
+ }
182
+ }
183
+
184
+ // Generate hash from task signature
185
+ const hashInput = `${taskName}:${JSON.stringify(inputSchema)}:${JSON.stringify(boundaries)}`
186
+ const hash = generateHash(hashInput)
187
+
188
+ return {
189
+ name: taskName,
190
+ location: {
191
+ file: filePath,
192
+ line: position.line + 1,
193
+ column: position.character + 1
194
+ },
195
+ inputSchema,
196
+ outputType,
197
+ boundaries,
198
+ hash
199
+ }
200
+ } catch (error) {
201
+ console.warn(`Failed to analyze createTask call for ${taskName}:`, error)
202
+ return null
203
+ }
204
+ }
205
+
206
+ // Enhanced return type inference with detailed object analysis
207
+ function inferDetailedReturnType(func: ts.FunctionExpression | ts.ArrowFunction, _sourceFile: ts.SourceFile): OutputType {
208
+ let returnType: OutputType = { type: 'unknown' }
209
+
210
+ function visitReturnStatements(node: ts.Node): void {
211
+ if (ts.isReturnStatement(node) && node.expression) {
212
+ if (ts.isObjectLiteralExpression(node.expression)) {
213
+ // Analyze object literal properties
214
+ const properties: Record<string, SchemaProperty> = {}
215
+ node.expression.properties.forEach(prop => {
216
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
217
+ // Handle explicit property assignments: { propName: value }
218
+ const propName = prop.name.text
219
+ let propType = 'unknown'
220
+
221
+ // Try to infer property type from the initializer
222
+ if (ts.isStringLiteral(prop.initializer)) {
223
+ propType = 'string'
224
+ } else if (ts.isNumericLiteral(prop.initializer)) {
225
+ propType = 'number'
226
+ } else if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
227
+ prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
228
+ propType = 'boolean'
229
+ } else if (ts.isIdentifier(prop.initializer)) {
230
+ // Variable reference - try to infer from name
231
+ const varName = prop.initializer.text
232
+ propType = inferTypeFromVariableName(varName)
233
+ } else if (ts.isPropertyAccessExpression(prop.initializer)) {
234
+ // Property access like response.handler
235
+ propType = 'unknown'
236
+ }
237
+
238
+ properties[propName] = { type: propType }
239
+ } else if (ts.isShorthandPropertyAssignment(prop)) {
240
+ // Handle shorthand properties: { propName } (equivalent to { propName: propName })
241
+ const propName = prop.name.text
242
+ const propType = inferTypeFromVariableName(propName)
243
+ properties[propName] = { type: propType }
244
+ }
245
+ })
246
+
247
+ if (Object.keys(properties).length > 0) {
248
+ returnType = {
249
+ type: 'object',
250
+ properties
251
+ }
252
+ } else {
253
+ returnType = { type: 'object' }
254
+ }
255
+ } else if (ts.isStringLiteral(node.expression)) {
256
+ returnType = { type: 'string' }
257
+ } else if (ts.isNumericLiteral(node.expression)) {
258
+ returnType = { type: 'number' }
259
+ } else if (node.expression.kind === ts.SyntaxKind.TrueKeyword ||
260
+ node.expression.kind === ts.SyntaxKind.FalseKeyword) {
261
+ returnType = { type: 'boolean' }
262
+ } else if (ts.isIdentifier(node.expression)) {
263
+ // Single variable return
264
+ const varType = inferTypeFromVariableName(node.expression.text)
265
+ returnType = { type: varType }
266
+ }
267
+ }
268
+ ts.forEachChild(node, visitReturnStatements)
269
+ }
270
+
271
+ if (func.body) {
272
+ visitReturnStatements(func.body)
273
+ }
274
+
275
+ return returnType
276
+ }
277
+
278
+ // Helper function to infer types from variable names
279
+ function inferTypeFromVariableName(varName: string): string {
280
+ // Common patterns for type inference based on variable names
281
+ if (varName.includes('path') || varName.includes('Path') ||
282
+ varName.includes('name') || varName.includes('Name') ||
283
+ varName.includes('descriptor') || varName.includes('Descriptor') ||
284
+ varName.includes('fileName') || varName.includes('handler') ||
285
+ varName.includes('url') || varName.includes('id') ||
286
+ varName.includes('uuid') || varName.includes('token')) {
287
+ return 'string'
288
+ } else if (varName.includes('count') || varName.includes('Count') ||
289
+ varName.includes('size') || varName.includes('Size') ||
290
+ varName.includes('length') || varName.includes('Length') ||
291
+ varName.includes('index') || varName.includes('Index')) {
292
+ return 'number'
293
+ } else if (varName.includes('is') || varName.includes('has') ||
294
+ varName.includes('can') || varName.includes('should') ||
295
+ varName.includes('enabled') || varName.includes('success')) {
296
+ return 'boolean'
297
+ } else if (varName.includes('config') || varName.includes('Config') ||
298
+ varName.includes('options') || varName.includes('Options') ||
299
+ varName.includes('data') || varName.includes('result') ||
300
+ varName.includes('response') || varName.includes('error')) {
301
+ return 'unknown'
302
+ }
303
+
304
+ return 'unknown'
305
+ }
306
+
307
+ // Clean up type strings (remove Promise wrappers for boundaries)
308
+ function cleanTypeString(typeString: string): string {
309
+ // Remove Promise wrapper for boundary functions
310
+ const promiseMatch = typeString.match(/Promise<(.+)>/)
311
+ if (promiseMatch) {
312
+ return promiseMatch[1]
313
+ }
314
+ return typeString
315
+ }
316
+
317
+ function analyzeSchemaArg(node: ts.Expression, sourceFile: ts.SourceFile): InputSchema {
318
+ // Handle variable references (e.g., when schema is defined as const schema = ...)
319
+ if (ts.isIdentifier(node) && node.text === 'schema') {
320
+ // This case is now handled by pre-finding the schema node
321
+ return { type: 'object', properties: {} }
322
+ }
323
+
324
+ // Handle direct Schema constructor calls
325
+ if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'Schema') {
326
+ const arg = node.arguments?.[0]
327
+ if (arg && ts.isObjectLiteralExpression(arg)) {
328
+ const properties: Record<string, SchemaProperty> = {}
329
+ arg.properties.forEach(prop => {
330
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
331
+ const propName = prop.name.text
332
+ const propValue = analyzeSchemaProp(prop.initializer, sourceFile)
333
+ properties[propName] = propValue
334
+ }
335
+ })
336
+ return { type: 'object', properties }
337
+ }
338
+ }
339
+ return { type: 'object', properties: {} }
340
+ }
341
+
342
+ // Enhanced schema property analysis
343
+ function analyzeSchemaProp(node: ts.Expression, _sourceFile: ts.SourceFile): SchemaProperty {
344
+ // Analyze Schema.string(), Schema.number(), etc.
345
+ if (ts.isCallExpression(node)) {
346
+ if (ts.isPropertyAccessExpression(node.expression) &&
347
+ ts.isIdentifier(node.expression.expression) &&
348
+ node.expression.expression.text === 'Schema') {
349
+
350
+ const methodName = node.expression.name.text
351
+ let baseType: SchemaProperty = { type: getSchemaTypeFromMethod(methodName) }
352
+
353
+ // Check for chained methods like .optional() or .default()
354
+ let current = node
355
+ while (current.parent && ts.isCallExpression(current.parent)) {
356
+ current = current.parent
357
+ if (ts.isPropertyAccessExpression(current.expression)) {
358
+ const chainedMethod = current.expression.name.text
359
+ if (chainedMethod === 'optional') {
360
+ baseType = { ...baseType, optional: true }
361
+ } else if (chainedMethod === 'default' && current.arguments[0]) {
362
+ baseType = { ...baseType, default: current.arguments[0].getText() }
363
+ }
364
+ }
365
+ }
366
+
367
+ return baseType
368
+ }
369
+ }
370
+ return { type: 'unknown' }
371
+ }
372
+
373
+ function getSchemaTypeFromMethod(methodName: string): string {
374
+ const typeMap: Record<string, string> = {
375
+ string: 'string',
376
+ number: 'number',
377
+ boolean: 'boolean',
378
+ array: 'array',
379
+ object: 'object'
380
+ }
381
+ return typeMap[methodName] || 'unknown'
382
+ }
383
+
384
+ function analyzeBoundariesArg(node: ts.Expression, _sourceFile: ts.SourceFile): string[] {
385
+ const boundaries: string[] = []
386
+
387
+ // Handle variable references (e.g., when boundaries is defined as const boundaries = ...)
388
+ if (ts.isIdentifier(node) && node.text === 'boundaries') {
389
+ // This case is now handled by pre-finding the boundaries node
390
+ return []
391
+ }
392
+
393
+ if (ts.isObjectLiteralExpression(node)) {
394
+ node.properties.forEach(prop => {
395
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
396
+ const boundaryName = prop.name.text
397
+ boundaries.push(boundaryName)
398
+ }
399
+ })
400
+ }
401
+
402
+ return boundaries
403
+ }
404
+
405
+ // Export the core analysis function for reuse
406
+ export function analyzeTaskFile(sourceCode: string, filePath: string, _expectedTaskName?: string): TaskFingerprintOutput | null {
407
+ const taskFingerprint = extractTaskFingerprints(sourceCode, filePath)[0]
408
+ if (!taskFingerprint) {
409
+ return null
410
+ }
411
+
412
+ // Return simplified output without name, location, hash
413
+ return {
414
+ description: taskFingerprint.description,
415
+ inputSchema: taskFingerprint.inputSchema,
416
+ outputType: taskFingerprint.outputType,
417
+ boundaries: taskFingerprint.boundaries
418
+ }
419
+ }