@ryanfw/prompt-orchestration-pipeline 0.12.0 → 0.13.1

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 (60) hide show
  1. package/package.json +10 -1
  2. package/src/cli/analyze-task.js +51 -0
  3. package/src/cli/index.js +8 -0
  4. package/src/components/AddPipelineSidebar.jsx +144 -0
  5. package/src/components/AnalysisProgressTray.jsx +87 -0
  6. package/src/components/JobTable.jsx +4 -3
  7. package/src/components/Layout.jsx +142 -139
  8. package/src/components/MarkdownRenderer.jsx +149 -0
  9. package/src/components/PipelineDAGGrid.jsx +404 -0
  10. package/src/components/PipelineTypeTaskSidebar.jsx +96 -0
  11. package/src/components/SchemaPreviewPanel.jsx +97 -0
  12. package/src/components/StageTimeline.jsx +36 -0
  13. package/src/components/TaskAnalysisDisplay.jsx +227 -0
  14. package/src/components/TaskCreationSidebar.jsx +447 -0
  15. package/src/components/TaskDetailSidebar.jsx +119 -117
  16. package/src/components/TaskFilePane.jsx +94 -39
  17. package/src/components/ui/button.jsx +59 -27
  18. package/src/components/ui/sidebar.jsx +118 -0
  19. package/src/config/models.js +99 -67
  20. package/src/core/config.js +4 -1
  21. package/src/llm/index.js +129 -9
  22. package/src/pages/PipelineDetail.jsx +6 -6
  23. package/src/pages/PipelineList.jsx +214 -0
  24. package/src/pages/PipelineTypeDetail.jsx +234 -0
  25. package/src/providers/deepseek.js +76 -16
  26. package/src/providers/openai.js +61 -34
  27. package/src/task-analysis/enrichers/analysis-writer.js +62 -0
  28. package/src/task-analysis/enrichers/schema-deducer.js +145 -0
  29. package/src/task-analysis/enrichers/schema-writer.js +74 -0
  30. package/src/task-analysis/extractors/artifacts.js +137 -0
  31. package/src/task-analysis/extractors/llm-calls.js +176 -0
  32. package/src/task-analysis/extractors/stages.js +51 -0
  33. package/src/task-analysis/index.js +103 -0
  34. package/src/task-analysis/parser.js +28 -0
  35. package/src/task-analysis/utils/ast.js +43 -0
  36. package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
  37. package/src/ui/client/index.css +64 -0
  38. package/src/ui/client/main.jsx +4 -0
  39. package/src/ui/client/sse-fetch.js +120 -0
  40. package/src/ui/dist/assets/index-cjHV9mYW.js +82578 -0
  41. package/src/ui/dist/assets/index-cjHV9mYW.js.map +1 -0
  42. package/src/ui/dist/assets/style-CoM9SoQF.css +180 -0
  43. package/src/ui/dist/index.html +2 -2
  44. package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
  45. package/src/ui/endpoints/pipeline-analysis-endpoint.js +246 -0
  46. package/src/ui/endpoints/pipeline-type-detail-endpoint.js +181 -0
  47. package/src/ui/endpoints/pipelines-endpoint.js +133 -0
  48. package/src/ui/endpoints/schema-file-endpoint.js +105 -0
  49. package/src/ui/endpoints/task-analysis-endpoint.js +104 -0
  50. package/src/ui/endpoints/task-creation-endpoint.js +114 -0
  51. package/src/ui/endpoints/task-save-endpoint.js +101 -0
  52. package/src/ui/express-app.js +45 -0
  53. package/src/ui/lib/analysis-lock.js +67 -0
  54. package/src/ui/lib/sse.js +30 -0
  55. package/src/ui/server.js +4 -0
  56. package/src/ui/utils/slug.js +31 -0
  57. package/src/ui/watcher.js +28 -2
  58. package/src/ui/dist/assets/index-B320avRx.js +0 -26613
  59. package/src/ui/dist/assets/index-B320avRx.js.map +0 -1
  60. package/src/ui/dist/assets/style-BYCoLBnK.css +0 -62
@@ -0,0 +1,145 @@
1
+ import { chat } from "../../llm/index.js";
2
+ import Ajv from "ajv";
3
+
4
+ const ajv = new Ajv();
5
+
6
+ /**
7
+ * Deduce JSON schema for an artifact using LLM with structured output.
8
+ *
9
+ * @param {string} taskCode - Full source code of the task file
10
+ * @param {object} artifact - Artifact info { fileName, stage }
11
+ * @returns {Promise<{ schema, example, reasoning }>}
12
+ */
13
+ export async function deduceArtifactSchema(taskCode, artifact) {
14
+ const response = await chat({
15
+ provider: "deepseek",
16
+ model: "deepseek-chat",
17
+ temperature: 0,
18
+ responseFormat: { type: "json_object" },
19
+ messages: [
20
+ { role: "system", content: buildSystemPrompt() },
21
+ { role: "user", content: buildUserPrompt(taskCode, artifact) },
22
+ ],
23
+ });
24
+
25
+ if (!response || typeof response !== "object") {
26
+ throw new Error(
27
+ `LLM response is missing or not an object when deducing artifact schema for "${artifact.fileName}".`
28
+ );
29
+ }
30
+
31
+ const result = response.content;
32
+
33
+ if (!result || typeof result !== "object") {
34
+ throw new Error(
35
+ `LLM response.content is missing or not an object when deducing artifact schema for "${artifact.fileName}".`
36
+ );
37
+ }
38
+
39
+ const { schema, example, reasoning } = result;
40
+
41
+ if (
42
+ !schema ||
43
+ typeof schema !== "object" ||
44
+ !example ||
45
+ typeof example !== "object" ||
46
+ typeof reasoning !== "string"
47
+ ) {
48
+ throw new Error(
49
+ `LLM returned invalid structured output when deducing artifact schema for "${artifact.fileName}". ` +
50
+ `Expected properties: { schema: object, example: object, reasoning: string }.`
51
+ );
52
+ }
53
+
54
+ // Validate the generated example against the generated schema
55
+ const validate = ajv.compile(schema);
56
+ if (!validate(example)) {
57
+ throw new Error(
58
+ `Generated example does not validate against schema: ${JSON.stringify(validate.errors)}`
59
+ );
60
+ }
61
+
62
+ return {
63
+ schema,
64
+ example,
65
+ reasoning,
66
+ };
67
+ }
68
+
69
+ function buildSystemPrompt() {
70
+ return `You are a code analysis expert that deduces JSON schemas from JavaScript source code.
71
+
72
+ Your task: Given a pipeline task's source code and a target artifact filename, extract the JSON schema that describes that artifact's structure.
73
+
74
+ ANALYSIS STRATEGY (follow this order):
75
+ 1. FIRST: Look for an exported schema constant (e.g. \`export const <name>Schema = {...}\`) that matches the artifact
76
+ 2. SECOND: Find the io.writeArtifact() call for this artifact and trace the data being written
77
+ 3. THIRD: Look for JSON structure hints in LLM prompts, JSON.parse usage, or data transformations
78
+ 4. FOURTH: If the artifact is read and validated elsewhere, check validation code for schema hints
79
+
80
+ OUTPUT REQUIREMENTS:
81
+ - Schema must be valid JSON Schema Draft-07
82
+ - Schema must include "$schema": "http://json-schema.org/draft-07/schema#"
83
+ - Schema must include "type", "properties", and "required" fields
84
+ - Example must be realistic data that validates against the schema
85
+ - Reasoning must explain your analysis steps
86
+
87
+ You must respond with a JSON object matching this exact structure:
88
+ {
89
+ "schema": { <valid JSON Schema Draft-07> },
90
+ "example": { <realistic example data> },
91
+ "reasoning": "<step-by-step explanation of how you determined the schema>"
92
+ }`;
93
+ }
94
+
95
+ function buildUserPrompt(taskCode, artifact) {
96
+ return `## Task Source Code
97
+
98
+ \`\`\`javascript
99
+ ${taskCode}
100
+ \`\`\`
101
+
102
+ ## Target Artifact
103
+ - Filename: ${artifact.fileName}
104
+ - Written in stage: ${artifact.stage}
105
+
106
+ ## Few-Shot Example
107
+
108
+ For a task that writes "user-profile.json" with code like:
109
+ \`\`\`javascript
110
+ await io.writeArtifact("user-profile.json", JSON.stringify({
111
+ name: user.name,
112
+ email: user.email,
113
+ preferences: { theme: "dark" }
114
+ }));
115
+ \`\`\`
116
+
117
+ The correct output would be:
118
+ {
119
+ "schema": {
120
+ "$schema": "http://json-schema.org/draft-07/schema#",
121
+ "type": "object",
122
+ "required": ["name", "email", "preferences"],
123
+ "properties": {
124
+ "name": { "type": "string" },
125
+ "email": { "type": "string", "format": "email" },
126
+ "preferences": {
127
+ "type": "object",
128
+ "properties": {
129
+ "theme": { "type": "string", "enum": ["light", "dark"] }
130
+ }
131
+ }
132
+ }
133
+ },
134
+ "example": {
135
+ "name": "Jane Doe",
136
+ "email": "jane@example.com",
137
+ "preferences": { "theme": "dark" }
138
+ },
139
+ "reasoning": "Found io.writeArtifact call with inline object literal. Traced property types from the object structure. Added format:email based on property name convention."
140
+ }
141
+
142
+ ## Your Task
143
+
144
+ Analyze the source code and produce the schema, example, and reasoning for the artifact "${artifact.fileName}" written in stage "${artifact.stage}".`;
145
+ }
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Write schema, sample, and meta files for an artifact.
6
+ *
7
+ * Key design: Schema files are PURE JSON Schema with no extra keys.
8
+ * Metadata is stored in a separate .meta.json file.
9
+ *
10
+ * @param {string} pipelinePath - Path to pipeline directory
11
+ * @param {string} artifactName - Artifact filename (e.g., "output.json")
12
+ * @param {object} deducedData - Object containing { schema, example, reasoning }
13
+ */
14
+ export async function writeSchemaFiles(
15
+ pipelinePath,
16
+ artifactName,
17
+ deducedData
18
+ ) {
19
+ // Validate that deducedData contains all required properties
20
+ if (!deducedData || typeof deducedData !== "object") {
21
+ throw new Error(
22
+ `Invalid deducedData: expected an object but got ${typeof deducedData}`
23
+ );
24
+ }
25
+
26
+ if (!deducedData.schema || typeof deducedData.schema !== "object") {
27
+ throw new Error(
28
+ `Invalid deducedData.schema: expected an object but got ${typeof deducedData.schema}`
29
+ );
30
+ }
31
+
32
+ if (deducedData.example === undefined || deducedData.example === null) {
33
+ throw new Error(
34
+ `Invalid deducedData.example: expected a value but got ${deducedData.example}`
35
+ );
36
+ }
37
+
38
+ if (typeof deducedData.reasoning !== "string") {
39
+ throw new Error(
40
+ `Invalid deducedData.reasoning: expected a string but got ${typeof deducedData.reasoning}`
41
+ );
42
+ }
43
+
44
+ const schemasDir = path.join(pipelinePath, "schemas");
45
+ await fs.mkdir(schemasDir, { recursive: true });
46
+
47
+ const baseName = path.parse(artifactName).name;
48
+
49
+ // 1. Write pure schema (valid JSON Schema Draft-07)
50
+ await fs.writeFile(
51
+ path.join(schemasDir, `${baseName}.schema.json`),
52
+ JSON.stringify(deducedData.schema, null, 2)
53
+ );
54
+
55
+ // 2. Write sample data (plain JSON)
56
+ await fs.writeFile(
57
+ path.join(schemasDir, `${baseName}.sample.json`),
58
+ JSON.stringify(deducedData.example, null, 2)
59
+ );
60
+
61
+ // 3. Write metadata separately (doesn't pollute schema/example)
62
+ await fs.writeFile(
63
+ path.join(schemasDir, `${baseName}.meta.json`),
64
+ JSON.stringify(
65
+ {
66
+ source: "llm-deduction",
67
+ generatedAt: new Date().toISOString(),
68
+ reasoning: deducedData.reasoning,
69
+ },
70
+ null,
71
+ 2
72
+ )
73
+ );
74
+ }
@@ -0,0 +1,137 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const traverse =
4
+ require("@babel/traverse").default ?? require("@babel/traverse");
5
+ const generate =
6
+ require("@babel/generator").default ?? require("@babel/generator");
7
+ import * as t from "@babel/types";
8
+ import { isInsideTryCatch, getStageName } from "../utils/ast.js";
9
+
10
+ /**
11
+ * Extract io.readArtifact calls from the AST.
12
+ *
13
+ * @param {import("@babel/types").File} ast - The AST to analyze
14
+ * @returns {Array<{fileName: string, stage: string, required: boolean}>} Array of artifact read references
15
+ * @throws {Error} If io.readArtifact call is found outside an exported function
16
+ */
17
+ export function extractArtifactReads(ast) {
18
+ const reads = [];
19
+
20
+ traverse(ast, {
21
+ CallExpression(path) {
22
+ const { callee } = path.node;
23
+
24
+ // Match: io.readArtifact("file.json") or io.readArtifact`file.json`
25
+ if (
26
+ t.isMemberExpression(callee) &&
27
+ t.isIdentifier(callee.object, { name: "io" }) &&
28
+ t.isIdentifier(callee.property, { name: "readArtifact" })
29
+ ) {
30
+ // Extract fileName from first argument
31
+ const fileName = extractFileName(path.node.arguments[0]);
32
+
33
+ if (!fileName) {
34
+ throw new Error(
35
+ `io.readArtifact requires a string literal or template literal argument at ${path.node.loc?.start?.line}:${path.node.loc?.start?.column}`
36
+ );
37
+ }
38
+
39
+ // Get the stage name (must be in an exported function)
40
+ const stage = getStageName(path);
41
+
42
+ if (!stage) {
43
+ throw new Error(
44
+ `io.readArtifact call found outside an exported function at ${path.node.loc?.start?.line}:${path.node.loc?.start?.column}`
45
+ );
46
+ }
47
+
48
+ // Check if inside try/catch to determine if required
49
+ const required = !isInsideTryCatch(path);
50
+
51
+ reads.push({ fileName, stage, required });
52
+ }
53
+ },
54
+ });
55
+
56
+ return reads;
57
+ }
58
+
59
+ /**
60
+ * Extract io.writeArtifact calls from the AST.
61
+ *
62
+ * @param {import("@babel/types").File} ast - The AST to analyze
63
+ * @returns {Array<{fileName: string, stage: string}>} Array of artifact write references
64
+ * @throws {Error} If io.writeArtifact call is found outside an exported function
65
+ */
66
+ export function extractArtifactWrites(ast) {
67
+ const writes = [];
68
+
69
+ traverse(ast, {
70
+ CallExpression(path) {
71
+ const { callee } = path.node;
72
+
73
+ // Match: io.writeArtifact("file.json", content)
74
+ if (
75
+ t.isMemberExpression(callee) &&
76
+ t.isIdentifier(callee.object, { name: "io" }) &&
77
+ t.isIdentifier(callee.property, { name: "writeArtifact" })
78
+ ) {
79
+ // Extract fileName from first argument
80
+ const fileName = extractFileName(path.node.arguments[0]);
81
+
82
+ if (!fileName) {
83
+ throw new Error(
84
+ `io.writeArtifact requires a string literal or template literal argument at ${path.node.loc?.start?.line}:${path.node.loc?.start?.column}`
85
+ );
86
+ }
87
+
88
+ // Get the stage name (must be in an exported function)
89
+ const stage = getStageName(path);
90
+
91
+ if (!stage) {
92
+ throw new Error(
93
+ `io.writeArtifact call found outside an exported function at ${path.node.loc?.start?.line}:${path.node.loc?.start?.column}`
94
+ );
95
+ }
96
+
97
+ writes.push({ fileName, stage });
98
+ }
99
+ },
100
+ });
101
+
102
+ return writes;
103
+ }
104
+
105
+ /**
106
+ * Extract filename from a string literal or template literal.
107
+ *
108
+ * @param {import("@babel/types").Node} node - The argument node
109
+ * @returns {string | null} The extracted filename or null if not a string/template literal
110
+ */
111
+ function extractFileName(node) {
112
+ // Handle string literals: "file.json"
113
+ if (t.isStringLiteral(node)) {
114
+ return node.value;
115
+ }
116
+
117
+ // Handle template literals: `file.json` or `file-${name}.json`
118
+ if (t.isTemplateLiteral(node)) {
119
+ // If there are no expressions, use the simple approach
120
+ if (!node.expressions || node.expressions.length === 0) {
121
+ return node.quasis.map((q) => q.value.cooked).join("");
122
+ }
123
+
124
+ // For template literals with expressions, use @babel/generator to preserve them
125
+ // This ensures dynamic filenames like `file-${name}.json` are preserved as-is
126
+ const generated = generate(node, { concise: true });
127
+ // Remove the backticks from the generated code
128
+ const code = generated.code;
129
+ if (code.startsWith("`") && code.endsWith("`")) {
130
+ return code.slice(1, -1);
131
+ }
132
+ // Fallback in case the generated code doesn't have backticks
133
+ return code;
134
+ }
135
+
136
+ return null;
137
+ }
@@ -0,0 +1,176 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const traverse =
4
+ require("@babel/traverse").default ?? require("@babel/traverse");
5
+ import * as t from "@babel/types";
6
+ import { getStageName } from "../utils/ast.js";
7
+
8
+ /**
9
+ * Extract LLM method calls from the AST.
10
+ *
11
+ * Matches direct calls like:
12
+ * - llm.deepseek.chat(...)
13
+ * - llm.openai.gpt5Mini(...)
14
+ * - llm.anthropic.sonnet45(...)
15
+ * - llm.gemini.flash25(...)
16
+ *
17
+ * And destructured calls like:
18
+ * - const { deepseek } = llm; deepseek.chat(...)
19
+ *
20
+ * @param {import("@babel/types").File} ast - The AST to analyze
21
+ * @returns {Array<{provider: string, method: string, stage: string}>} Array of LLM call references
22
+ * @throws {Error} If LLM call is found outside an exported function
23
+ */
24
+ export function extractLLMCalls(ast) {
25
+ const calls = [];
26
+
27
+ traverse(ast, {
28
+ CallExpression(path) {
29
+ const { callee } = path.node;
30
+
31
+ // Match: llm.provider.method(...)
32
+ if (isDirectLLMCall(callee)) {
33
+ const provider = callee.object.property.name;
34
+ const method = callee.property.name;
35
+ const stage = getStageName(path);
36
+
37
+ if (!stage) {
38
+ throw new Error(
39
+ `LLM call found outside an exported function at ${path.node.loc?.start?.line}:${path.node.loc?.start?.column}`
40
+ );
41
+ }
42
+
43
+ calls.push({ provider, method, stage });
44
+ }
45
+
46
+ // Match: provider.method(...) where provider was destructured from llm
47
+ if (isDestructuredLLMCall(callee, path)) {
48
+ const provider = callee.object.name;
49
+ const method = callee.property.name;
50
+ const stage = getStageName(path);
51
+
52
+ if (!stage) {
53
+ throw new Error(
54
+ `LLM call found outside an exported function at ${path.node.loc?.start?.line}:${path.node.loc?.start?.column}`
55
+ );
56
+ }
57
+
58
+ calls.push({ provider, method, stage });
59
+ }
60
+ },
61
+ });
62
+
63
+ return calls;
64
+ }
65
+
66
+ /**
67
+ * Check if a callee node is a direct LLM call pattern.
68
+ *
69
+ * Matches nested member expressions: llm.provider.method
70
+ *
71
+ * @param {import("@babel/types").Node} callee - The callee node to check
72
+ * @returns {boolean} True if the callee is a direct LLM call pattern
73
+ */
74
+ function isDirectLLMCall(callee) {
75
+ // Must be a member expression (e.g., llm.provider.method)
76
+ if (!t.isMemberExpression(callee)) {
77
+ return false;
78
+ }
79
+
80
+ // The object must also be a member expression (e.g., llm.provider)
81
+ if (!t.isMemberExpression(callee.object)) {
82
+ return false;
83
+ }
84
+
85
+ // The root object must be identifier "llm"
86
+ if (!t.isIdentifier(callee.object.object, { name: "llm" })) {
87
+ return false;
88
+ }
89
+
90
+ // The object property must be an identifier (the provider)
91
+ if (!t.isIdentifier(callee.object.property)) {
92
+ return false;
93
+ }
94
+
95
+ // The callee property must be an identifier (the method)
96
+ if (!t.isIdentifier(callee.property)) {
97
+ return false;
98
+ }
99
+
100
+ return true;
101
+ }
102
+
103
+ /**
104
+ * Check if a callee node is a destructured LLM call pattern.
105
+ *
106
+ * Matches: provider.method(...) where provider was destructured from llm
107
+ * Uses scope analysis to verify the identifier was actually destructured from llm
108
+ * in the current scope, avoiding false positives from same-named identifiers in different scopes.
109
+ *
110
+ * @param {import("@babel/types").Node} callee - The callee node to check
111
+ * @param {import("@babel/traverse").NodePath} path - The path of the call expression
112
+ * @returns {boolean} True if the callee is a destructured LLM call pattern
113
+ */
114
+ function isDestructuredLLMCall(callee, path) {
115
+ // Must be a member expression (e.g., provider.method)
116
+ if (!t.isMemberExpression(callee)) {
117
+ return false;
118
+ }
119
+
120
+ // The object must be an identifier (the destructured provider)
121
+ if (!t.isIdentifier(callee.object)) {
122
+ return false;
123
+ }
124
+
125
+ // The callee property must be an identifier (the method)
126
+ if (!t.isIdentifier(callee.property)) {
127
+ return false;
128
+ }
129
+
130
+ // Get the binding for this identifier in the current scope
131
+ const binding = path.scope.getBinding(callee.object.name);
132
+ if (!binding) {
133
+ return false;
134
+ }
135
+
136
+ // Check if it was destructured from llm in a variable declaration
137
+ if (t.isVariableDeclarator(binding.path.node)) {
138
+ const { id, init } = binding.path.node;
139
+ // Match: const { provider } = llm
140
+ if (t.isObjectPattern(id) && t.isIdentifier(init, { name: "llm" })) {
141
+ return true;
142
+ }
143
+ }
144
+
145
+ // Check if it was destructured from llm in function parameters
146
+ // Match: ({ llm: { provider } }) => {}
147
+ if (binding.kind === "param" && t.isObjectPattern(binding.path.node)) {
148
+ // The binding points to the entire parameter ObjectPattern
149
+ // Look for a property with key "llm" whose value is an ObjectPattern
150
+ const llmProperty = binding.path.node.properties.find(
151
+ (prop) =>
152
+ t.isObjectProperty(prop) &&
153
+ t.isIdentifier(prop.key, { name: "llm" }) &&
154
+ t.isObjectPattern(prop.value)
155
+ );
156
+
157
+ if (llmProperty) {
158
+ // Check if the provider identifier is in the nested ObjectPattern
159
+ // Handles both shorthand ({ llm: { provider } }) and renamed ({ llm: { provider: alias } }) patterns
160
+ const providerInPattern = llmProperty.value.properties.some((innerProp) =>
161
+ t.isObjectProperty(innerProp)
162
+ ? // Check key for shorthand pattern: { provider }
163
+ t.isIdentifier(innerProp.key, { name: callee.object.name }) ||
164
+ // Check value for renamed pattern: { provider: alias } where we're looking for alias
165
+ t.isIdentifier(innerProp.value, { name: callee.object.name })
166
+ : false
167
+ );
168
+
169
+ if (providerInPattern) {
170
+ return true;
171
+ }
172
+ }
173
+ }
174
+
175
+ return false;
176
+ }
@@ -0,0 +1,51 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const traverse =
4
+ require("@babel/traverse").default ?? require("@babel/traverse");
5
+
6
+ /**
7
+ * Extract exported function stages from an AST.
8
+ *
9
+ * Visits ExportNamedDeclaration nodes and extracts stage information
10
+ * including name, line order, and async status.
11
+ *
12
+ * @param {import("@babel/types").File} ast - The parsed AST
13
+ * @returns {Array<{name: string, order: number, isAsync: boolean}>}
14
+ * Array of stages sorted by order (line number)
15
+ */
16
+ export function extractStages(ast) {
17
+ const stages = [];
18
+
19
+ traverse(ast, {
20
+ ExportNamedDeclaration(path) {
21
+ const declaration = path.node.declaration;
22
+
23
+ // Handle: export function name() {}
24
+ if (declaration?.type === "FunctionDeclaration") {
25
+ stages.push({
26
+ name: declaration.id.name,
27
+ order: path.node.loc?.start.line ?? 0,
28
+ isAsync: declaration.async ?? false,
29
+ });
30
+ return;
31
+ }
32
+
33
+ // Handle: export const name = () => {} or export const name = async () => {}
34
+ // or export const name = function() {}
35
+ if (declaration?.type === "VariableDeclaration") {
36
+ const declarator = declaration.declarations[0];
37
+ const init = declarator?.init;
38
+
39
+ if (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") {
40
+ stages.push({
41
+ name: declarator.id.name,
42
+ order: path.node.loc?.start.line ?? 0,
43
+ isAsync: init.async ?? false,
44
+ });
45
+ }
46
+ }
47
+ },
48
+ });
49
+
50
+ return stages.sort((a, b) => a.order - b.order);
51
+ }
@@ -0,0 +1,103 @@
1
+ import { parseTaskSource } from "./parser.js";
2
+ import { extractStages } from "./extractors/stages.js";
3
+ import {
4
+ extractArtifactReads,
5
+ extractArtifactWrites,
6
+ } from "./extractors/artifacts.js";
7
+ import { extractLLMCalls } from "./extractors/llm-calls.js";
8
+ import { writeAnalysisFile } from "./enrichers/analysis-writer.js";
9
+
10
+ /**
11
+ * Analyze task source code and extract metadata.
12
+ *
13
+ * This is the main entry point for the task analysis library. It parses
14
+ * the source code and extracts:
15
+ * - Stages (exported functions with order and async status)
16
+ * - Artifacts (read/write operations with stage context)
17
+ * - Models (LLM provider and method calls with stage context)
18
+ *
19
+ * @param {string} code - The task source code to analyze
20
+ * @param {string|null} taskFilePath - Path to the task file (optional)
21
+ * @returns {TaskAnalysis} Complete analysis of the task
22
+ * @throws {Error} If parsing or extraction fails
23
+ *
24
+ * @example
25
+ * const code = `
26
+ * export function ingestion({ io, llm, data, flags }) {
27
+ * const content = io.readArtifact("input.json");
28
+ * const result = llm.deepseek.chat(content);
29
+ * io.writeArtifact("output.json", result);
30
+ * }
31
+ * `;
32
+ * const analysis = analyzeTask(code, "/path/to/task.js");
33
+ * // Returns:
34
+ * // {
35
+ * // taskFilePath: "/path/to/task.js",
36
+ * // stages: [{ name: "ingestion", order: 2, isAsync: false }],
37
+ * // artifacts: {
38
+ * // reads: [{ fileName: "input.json", stage: "ingestion", required: true }],
39
+ * // writes: [{ fileName: "output.json", stage: "ingestion" }]
40
+ * // },
41
+ * // models: [{ provider: "deepseek", method: "chat", stage: "ingestion" }]
42
+ * // }
43
+ */
44
+ export { writeAnalysisFile };
45
+
46
+ export function analyzeTask(code, taskFilePath = null) {
47
+ // Parse the source code into an AST
48
+ const ast = parseTaskSource(code);
49
+
50
+ // Extract all metadata from the AST
51
+ const stages = extractStages(ast);
52
+ const reads = extractArtifactReads(ast);
53
+ const writes = extractArtifactWrites(ast);
54
+ const models = extractLLMCalls(ast);
55
+
56
+ // Compose into the TaskAnalysis object
57
+ return {
58
+ taskFilePath,
59
+ stages,
60
+ artifacts: {
61
+ reads,
62
+ writes,
63
+ },
64
+ models,
65
+ };
66
+ }
67
+
68
+ /**
69
+ * @typedef {Object} TaskAnalysis
70
+ * @property {string|null} taskFilePath - Path to the task file
71
+ * @property {Array<Stage>} stages - Array of exported function stages
72
+ * @property {Object} artifacts - Artifact operations
73
+ * @property {Array<ArtifactRead>} artifacts.reads - Artifact read operations
74
+ * @property {Array<ArtifactWrite>} artifacts.writes - Artifact write operations
75
+ * @property {Array<ModelCall>} models - LLM method calls
76
+ */
77
+
78
+ /**
79
+ * @typedef {Object} Stage
80
+ * @property {string} name - Stage function name
81
+ * @property {number} order - Line number for execution order
82
+ * @property {boolean} isAsync - Whether the stage is async
83
+ */
84
+
85
+ /**
86
+ * @typedef {Object} ArtifactRead
87
+ * @property {string} fileName - Name of the artifact file
88
+ * @property {string} stage - Stage name where read occurs
89
+ * @property {boolean} required - Whether the read is required (not wrapped in try/catch)
90
+ */
91
+
92
+ /**
93
+ * @typedef {Object} ArtifactWrite
94
+ * @property {string} fileName - Name of the artifact file
95
+ * @property {string} stage - Stage name where write occurs
96
+ */
97
+
98
+ /**
99
+ * @typedef {Object} ModelCall
100
+ * @property {string} provider - LLM provider name (e.g., "deepseek", "openai")
101
+ * @property {string} method - LLM method name (e.g., "chat", "gpt5Mini")
102
+ * @property {string} stage - Stage name where LLM call occurs
103
+ */