@ryanfw/prompt-orchestration-pipeline 0.15.0 → 0.16.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.
@@ -105,21 +105,36 @@ export default function PromptPipelineDashboard() {
105
105
  }
106
106
  };
107
107
 
108
- // Header actions for Layout
109
- const headerActions = runningJobs.length > 0 && (
110
- <Flex align="center" gap="2" className="text-gray-11">
111
- <Text size="1" weight="medium">
112
- Overall Progress
113
- </Text>
114
- <Progress value={aggregateProgress} className="w-20" />
115
- <Text size="1" className="text-gray-9">
116
- {aggregateProgress}%
117
- </Text>
118
- </Flex>
119
- );
108
+ const progressBanner =
109
+ runningJobs.length > 0 ? (
110
+ <Box
111
+ role="status"
112
+ aria-live="polite"
113
+ className="bg-blue-50 border-b border-blue-200"
114
+ >
115
+ <Flex
116
+ align="center"
117
+ gap="4"
118
+ className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-3"
119
+ >
120
+ <Text size="2">
121
+ {runningJobs.length} job{runningJobs.length !== 1 ? "s" : ""}{" "}
122
+ running
123
+ </Text>
124
+ <Progress
125
+ value={aggregateProgress}
126
+ variant="running"
127
+ className="flex-1"
128
+ />
129
+ <Text size="2" weight="medium">
130
+ {aggregateProgress}%
131
+ </Text>
132
+ </Flex>
133
+ </Box>
134
+ ) : null;
120
135
 
121
136
  return (
122
- <Layout title="Prompt Pipeline" actions={headerActions}>
137
+ <Layout title="Prompt Pipeline" subheader={progressBanner}>
123
138
  {error && (
124
139
  <Box className="mb-4 rounded-md bg-yellow-50 p-3 border border-yellow-200">
125
140
  <Text size="2" className="text-yellow-800">
@@ -41,6 +41,38 @@ export async function writeAnalysisFile(pipelinePath, taskName, analysisData) {
41
41
  );
42
42
  }
43
43
 
44
+ // Validate artifacts.reads and artifacts.writes are arrays
45
+ if (!Array.isArray(analysisData.artifacts.reads)) {
46
+ throw new Error(
47
+ `Invalid analysisData.artifacts.reads: expected an array but got ${typeof analysisData.artifacts.reads}`
48
+ );
49
+ }
50
+
51
+ if (!Array.isArray(analysisData.artifacts.writes)) {
52
+ throw new Error(
53
+ `Invalid analysisData.artifacts.writes: expected an array but got ${typeof analysisData.artifacts.writes}`
54
+ );
55
+ }
56
+
57
+ // Validate unresolvedReads and unresolvedWrites if present (must be arrays)
58
+ if (
59
+ analysisData.artifacts.unresolvedReads !== undefined &&
60
+ !Array.isArray(analysisData.artifacts.unresolvedReads)
61
+ ) {
62
+ throw new Error(
63
+ `Invalid analysisData.artifacts.unresolvedReads: expected an array but got ${typeof analysisData.artifacts.unresolvedReads}`
64
+ );
65
+ }
66
+
67
+ if (
68
+ analysisData.artifacts.unresolvedWrites !== undefined &&
69
+ !Array.isArray(analysisData.artifacts.unresolvedWrites)
70
+ ) {
71
+ throw new Error(
72
+ `Invalid analysisData.artifacts.unresolvedWrites: expected an array but got ${typeof analysisData.artifacts.unresolvedWrites}`
73
+ );
74
+ }
75
+
44
76
  if (!Array.isArray(analysisData.models)) {
45
77
  throw new Error(
46
78
  `Invalid analysisData.models: expected an array but got ${typeof analysisData.models}`
@@ -0,0 +1,98 @@
1
+ import { chat } from "../../llm/index.js";
2
+
3
+ const SYSTEM_PROMPT = `You are an expert code analyzer. Your task is to match a dynamic artifact reference in JavaScript code to one of the known artifact filenames.
4
+
5
+ Given:
6
+ 1. The full task source code
7
+ 2. A dynamic expression used as an argument to io.readArtifact() or io.writeArtifact()
8
+ 3. The surrounding code context
9
+ 4. A list of available artifact filenames
10
+
11
+ Analyze the code to determine what the dynamic expression likely evaluates to, then match it against the available artifacts.
12
+
13
+ Return your response as JSON with this structure:
14
+ {
15
+ "resolvedFileName": "matched-artifact.json" or null if no match,
16
+ "confidence": 0.0 to 1.0,
17
+ "reasoning": "Brief explanation of your analysis"
18
+ }
19
+
20
+ Guidelines:
21
+ - Look at variable assignments, function return values, and naming patterns
22
+ - Consider the stage name and surrounding context for clues
23
+ - Return confidence 0 if no reasonable match exists
24
+ - Only return high confidence (>=0.7) when there's strong evidence
25
+ - If multiple artifacts could match, choose the most likely one but reduce confidence`;
26
+
27
+ /**
28
+ * Resolve a dynamic artifact reference using LLM analysis.
29
+ *
30
+ * @param {string} taskCode - The full task source code
31
+ * @param {object} unresolvedArtifact - The unresolved artifact reference
32
+ * @param {string} unresolvedArtifact.expression - The dynamic expression code
33
+ * @param {string} unresolvedArtifact.codeContext - Surrounding code context
34
+ * @param {string} unresolvedArtifact.stage - Stage name where the call occurs
35
+ * @param {string[]} availableArtifacts - List of known artifact filenames
36
+ * @returns {Promise<{resolvedFileName: string|null, confidence: number, reasoning: string}>}
37
+ */
38
+ export async function resolveArtifactReference(
39
+ taskCode,
40
+ unresolvedArtifact,
41
+ availableArtifacts
42
+ ) {
43
+ const { expression, codeContext, stage } = unresolvedArtifact;
44
+
45
+ const userPrompt = `Task source code:
46
+ \`\`\`javascript
47
+ ${taskCode}
48
+ \`\`\`
49
+
50
+ Dynamic expression: ${expression}
51
+ Stage: ${stage}
52
+ Code context:
53
+ \`\`\`javascript
54
+ ${codeContext}
55
+ \`\`\`
56
+
57
+ Available artifact filenames:
58
+ ${availableArtifacts.map((a) => `- ${a}`).join("\n")}
59
+
60
+ Analyze the code and determine which artifact this expression likely refers to.`;
61
+
62
+ try {
63
+ const response = await chat({
64
+ provider: "deepseek",
65
+ messages: [
66
+ { role: "system", content: SYSTEM_PROMPT },
67
+ { role: "user", content: userPrompt },
68
+ ],
69
+ temperature: 0,
70
+ responseFormat: "json_object",
71
+ });
72
+
73
+ const parsed = JSON.parse(response.content);
74
+
75
+ const rawResolvedFileName =
76
+ typeof parsed.resolvedFileName === "string" ? parsed.resolvedFileName : null;
77
+ const resolvedFileName =
78
+ rawResolvedFileName && availableArtifacts.includes(rawResolvedFileName)
79
+ ? rawResolvedFileName
80
+ : null;
81
+
82
+ const rawConfidence =
83
+ typeof parsed.confidence === "number" ? parsed.confidence : 0;
84
+ const confidence = resolvedFileName === null ? 0 : rawConfidence;
85
+
86
+ return {
87
+ resolvedFileName,
88
+ confidence,
89
+ reasoning: parsed.reasoning ?? "",
90
+ };
91
+ } catch {
92
+ return {
93
+ resolvedFileName: null,
94
+ confidence: 0,
95
+ reasoning: "Failed to analyze artifact reference",
96
+ };
97
+ }
98
+ }
@@ -7,15 +7,39 @@ const generate =
7
7
  import * as t from "@babel/types";
8
8
  import { isInsideTryCatch, getStageName } from "../utils/ast.js";
9
9
 
10
+ /**
11
+ * Extract surrounding code context for a node.
12
+ *
13
+ * @param {import("@babel/traverse").NodePath} path - The Babel path
14
+ * @param {string} sourceCode - The original source code
15
+ * @returns {string} Up to 5 lines of surrounding context (2 lines before and 2 lines after the target line)
16
+ */
17
+ export function extractCodeContext(path, sourceCode) {
18
+ if (!sourceCode || !path.node.loc) {
19
+ return "";
20
+ }
21
+
22
+ const lines = sourceCode.split("\n");
23
+ const nodeLine = path.node.loc.start.line;
24
+
25
+ // Get 2 lines before and 2 lines after (5 lines total, 1-indexed)
26
+ const startLine = Math.max(1, nodeLine - 2);
27
+ const endLine = Math.min(lines.length, nodeLine + 2);
28
+
29
+ return lines.slice(startLine - 1, endLine).join("\n");
30
+ }
31
+
10
32
  /**
11
33
  * Extract io.readArtifact calls from the AST.
12
34
  *
13
35
  * @param {import("@babel/types").File} ast - The AST to analyze
14
- * @returns {Array<{fileName: string, stage: string, required: boolean}>} Array of artifact read references
36
+ * @param {string} [sourceCode] - The original source code (for extracting context)
37
+ * @returns {{reads: Array<{fileName: string, stage: string, required: boolean}>, unresolvedReads: Array<{expression: string, codeContext: string, stage: string, required: boolean, location: {line: number, column: number}}>}} Artifact read references
15
38
  * @throws {Error} If io.readArtifact call is found outside an exported function
16
39
  */
17
- export function extractArtifactReads(ast) {
40
+ export function extractArtifactReads(ast, sourceCode) {
18
41
  const reads = [];
42
+ const unresolvedReads = [];
19
43
 
20
44
  traverse(ast, {
21
45
  CallExpression(path) {
@@ -27,15 +51,6 @@ export function extractArtifactReads(ast) {
27
51
  t.isIdentifier(callee.object, { name: "io" }) &&
28
52
  t.isIdentifier(callee.property, { name: "readArtifact" })
29
53
  ) {
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
54
  // Get the stage name (must be in an exported function)
40
55
  const stage = getStageName(path);
41
56
 
@@ -48,23 +63,46 @@ export function extractArtifactReads(ast) {
48
63
  // Check if inside try/catch to determine if required
49
64
  const required = !isInsideTryCatch(path);
50
65
 
51
- reads.push({ fileName, stage, required });
66
+ // Extract fileName from first argument
67
+ const fileName = extractFileName(path.node.arguments[0]);
68
+
69
+ if (fileName) {
70
+ reads.push({ fileName, stage, required });
71
+ } else if (path.node.arguments[0]) {
72
+ // Capture unresolved reference
73
+ const argNode = path.node.arguments[0];
74
+ const expression = generate(argNode, { concise: true }).code;
75
+ const codeContext = extractCodeContext(path, sourceCode);
76
+ const location = {
77
+ line: argNode.loc?.start?.line ?? 0,
78
+ column: argNode.loc?.start?.column ?? 0,
79
+ };
80
+ unresolvedReads.push({
81
+ expression,
82
+ codeContext,
83
+ stage,
84
+ required,
85
+ location,
86
+ });
87
+ }
52
88
  }
53
89
  },
54
90
  });
55
91
 
56
- return reads;
92
+ return { reads, unresolvedReads };
57
93
  }
58
94
 
59
95
  /**
60
96
  * Extract io.writeArtifact calls from the AST.
61
97
  *
62
98
  * @param {import("@babel/types").File} ast - The AST to analyze
63
- * @returns {Array<{fileName: string, stage: string}>} Array of artifact write references
99
+ * @param {string} [sourceCode] - The original source code (for extracting context)
100
+ * @returns {{writes: Array<{fileName: string, stage: string}>, unresolvedWrites: Array<{expression: string, codeContext: string, stage: string, location: {line: number, column: number}}>}} Artifact write references
64
101
  * @throws {Error} If io.writeArtifact call is found outside an exported function
65
102
  */
66
- export function extractArtifactWrites(ast) {
103
+ export function extractArtifactWrites(ast, sourceCode) {
67
104
  const writes = [];
105
+ const unresolvedWrites = [];
68
106
 
69
107
  traverse(ast, {
70
108
  CallExpression(path) {
@@ -76,15 +114,6 @@ export function extractArtifactWrites(ast) {
76
114
  t.isIdentifier(callee.object, { name: "io" }) &&
77
115
  t.isIdentifier(callee.property, { name: "writeArtifact" })
78
116
  ) {
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
117
  // Get the stage name (must be in an exported function)
89
118
  const stage = getStageName(path);
90
119
 
@@ -94,12 +123,27 @@ export function extractArtifactWrites(ast) {
94
123
  );
95
124
  }
96
125
 
97
- writes.push({ fileName, stage });
126
+ // Extract fileName from first argument
127
+ const fileName = extractFileName(path.node.arguments[0]);
128
+
129
+ if (fileName) {
130
+ writes.push({ fileName, stage });
131
+ } else if (path.node.arguments[0]) {
132
+ // Capture unresolved reference
133
+ const argNode = path.node.arguments[0];
134
+ const expression = generate(argNode, { concise: true }).code;
135
+ const codeContext = extractCodeContext(path, sourceCode);
136
+ const location = {
137
+ line: argNode.loc?.start?.line ?? 0,
138
+ column: argNode.loc?.start?.column ?? 0,
139
+ };
140
+ unresolvedWrites.push({ expression, codeContext, stage, location });
141
+ }
98
142
  }
99
143
  },
100
144
  });
101
145
 
102
- return writes;
146
+ return { writes, unresolvedWrites };
103
147
  }
104
148
 
105
149
  /**
@@ -49,8 +49,8 @@ export function analyzeTask(code, taskFilePath = null) {
49
49
 
50
50
  // Extract all metadata from the AST
51
51
  const stages = extractStages(ast);
52
- const reads = extractArtifactReads(ast);
53
- const writes = extractArtifactWrites(ast);
52
+ const { reads, unresolvedReads } = extractArtifactReads(ast, code);
53
+ const { writes, unresolvedWrites } = extractArtifactWrites(ast, code);
54
54
  const models = extractLLMCalls(ast);
55
55
 
56
56
  // Compose into the TaskAnalysis object
@@ -60,6 +60,8 @@ export function analyzeTask(code, taskFilePath = null) {
60
60
  artifacts: {
61
61
  reads,
62
62
  writes,
63
+ unresolvedReads,
64
+ unresolvedWrites,
63
65
  },
64
66
  models,
65
67
  };