@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.
- package/package.json +10 -1
- package/src/cli/analyze-task.js +51 -0
- package/src/cli/index.js +8 -0
- package/src/components/AddPipelineSidebar.jsx +144 -0
- package/src/components/AnalysisProgressTray.jsx +87 -0
- package/src/components/JobTable.jsx +4 -3
- package/src/components/Layout.jsx +142 -139
- package/src/components/MarkdownRenderer.jsx +149 -0
- package/src/components/PipelineDAGGrid.jsx +404 -0
- package/src/components/PipelineTypeTaskSidebar.jsx +96 -0
- package/src/components/SchemaPreviewPanel.jsx +97 -0
- package/src/components/StageTimeline.jsx +36 -0
- package/src/components/TaskAnalysisDisplay.jsx +227 -0
- package/src/components/TaskCreationSidebar.jsx +447 -0
- package/src/components/TaskDetailSidebar.jsx +119 -117
- package/src/components/TaskFilePane.jsx +94 -39
- package/src/components/ui/button.jsx +59 -27
- package/src/components/ui/sidebar.jsx +118 -0
- package/src/config/models.js +99 -67
- package/src/core/config.js +4 -1
- package/src/llm/index.js +129 -9
- package/src/pages/PipelineDetail.jsx +6 -6
- package/src/pages/PipelineList.jsx +214 -0
- package/src/pages/PipelineTypeDetail.jsx +234 -0
- package/src/providers/deepseek.js +76 -16
- package/src/providers/openai.js +61 -34
- package/src/task-analysis/enrichers/analysis-writer.js +62 -0
- package/src/task-analysis/enrichers/schema-deducer.js +145 -0
- package/src/task-analysis/enrichers/schema-writer.js +74 -0
- package/src/task-analysis/extractors/artifacts.js +137 -0
- package/src/task-analysis/extractors/llm-calls.js +176 -0
- package/src/task-analysis/extractors/stages.js +51 -0
- package/src/task-analysis/index.js +103 -0
- package/src/task-analysis/parser.js +28 -0
- package/src/task-analysis/utils/ast.js +43 -0
- package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
- package/src/ui/client/index.css +64 -0
- package/src/ui/client/main.jsx +4 -0
- package/src/ui/client/sse-fetch.js +120 -0
- package/src/ui/dist/assets/index-cjHV9mYW.js +82578 -0
- package/src/ui/dist/assets/index-cjHV9mYW.js.map +1 -0
- package/src/ui/dist/assets/style-CoM9SoQF.css +180 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
- package/src/ui/endpoints/pipeline-analysis-endpoint.js +246 -0
- package/src/ui/endpoints/pipeline-type-detail-endpoint.js +181 -0
- package/src/ui/endpoints/pipelines-endpoint.js +133 -0
- package/src/ui/endpoints/schema-file-endpoint.js +105 -0
- package/src/ui/endpoints/task-analysis-endpoint.js +104 -0
- package/src/ui/endpoints/task-creation-endpoint.js +114 -0
- package/src/ui/endpoints/task-save-endpoint.js +101 -0
- package/src/ui/express-app.js +45 -0
- package/src/ui/lib/analysis-lock.js +67 -0
- package/src/ui/lib/sse.js +30 -0
- package/src/ui/server.js +4 -0
- package/src/ui/utils/slug.js +31 -0
- package/src/ui/watcher.js +28 -2
- package/src/ui/dist/assets/index-B320avRx.js +0 -26613
- package/src/ui/dist/assets/index-B320avRx.js.map +0 -1
- 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
|
+
*/
|