@ryanfw/prompt-orchestration-pipeline 0.11.0 → 0.13.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 (83) hide show
  1. package/package.json +11 -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/DAGGrid.jsx +157 -47
  7. package/src/components/JobTable.jsx +4 -3
  8. package/src/components/Layout.jsx +142 -139
  9. package/src/components/MarkdownRenderer.jsx +149 -0
  10. package/src/components/PipelineDAGGrid.jsx +404 -0
  11. package/src/components/PipelineTypeTaskSidebar.jsx +96 -0
  12. package/src/components/SchemaPreviewPanel.jsx +97 -0
  13. package/src/components/StageTimeline.jsx +36 -0
  14. package/src/components/TaskAnalysisDisplay.jsx +227 -0
  15. package/src/components/TaskCreationSidebar.jsx +447 -0
  16. package/src/components/TaskDetailSidebar.jsx +119 -117
  17. package/src/components/TaskFilePane.jsx +94 -39
  18. package/src/components/ui/RestartJobModal.jsx +26 -6
  19. package/src/components/ui/StopJobModal.jsx +183 -0
  20. package/src/components/ui/button.jsx +59 -27
  21. package/src/components/ui/sidebar.jsx +118 -0
  22. package/src/config/models.js +99 -67
  23. package/src/core/config.js +11 -4
  24. package/src/core/lifecycle-policy.js +62 -0
  25. package/src/core/pipeline-runner.js +312 -217
  26. package/src/core/status-writer.js +84 -0
  27. package/src/llm/index.js +129 -9
  28. package/src/pages/Code.jsx +8 -1
  29. package/src/pages/PipelineDetail.jsx +84 -2
  30. package/src/pages/PipelineList.jsx +214 -0
  31. package/src/pages/PipelineTypeDetail.jsx +234 -0
  32. package/src/pages/PromptPipelineDashboard.jsx +10 -11
  33. package/src/providers/deepseek.js +76 -16
  34. package/src/providers/openai.js +61 -34
  35. package/src/task-analysis/enrichers/analysis-writer.js +62 -0
  36. package/src/task-analysis/enrichers/schema-deducer.js +145 -0
  37. package/src/task-analysis/enrichers/schema-writer.js +74 -0
  38. package/src/task-analysis/extractors/artifacts.js +137 -0
  39. package/src/task-analysis/extractors/llm-calls.js +176 -0
  40. package/src/task-analysis/extractors/stages.js +51 -0
  41. package/src/task-analysis/index.js +103 -0
  42. package/src/task-analysis/parser.js +28 -0
  43. package/src/task-analysis/utils/ast.js +43 -0
  44. package/src/ui/client/adapters/job-adapter.js +60 -0
  45. package/src/ui/client/api.js +233 -8
  46. package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
  47. package/src/ui/client/hooks/useJobList.js +14 -1
  48. package/src/ui/client/index.css +64 -0
  49. package/src/ui/client/main.jsx +4 -0
  50. package/src/ui/client/sse-fetch.js +120 -0
  51. package/src/ui/dist/app.js +262 -0
  52. package/src/ui/dist/assets/index-cjHV9mYW.js +82578 -0
  53. package/src/ui/dist/assets/index-cjHV9mYW.js.map +1 -0
  54. package/src/ui/dist/assets/style-CoM9SoQF.css +180 -0
  55. package/src/ui/dist/favicon.svg +12 -0
  56. package/src/ui/dist/index.html +2 -2
  57. package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
  58. package/src/ui/endpoints/file-endpoints.js +330 -0
  59. package/src/ui/endpoints/job-control-endpoints.js +1001 -0
  60. package/src/ui/endpoints/job-endpoints.js +62 -0
  61. package/src/ui/endpoints/pipeline-analysis-endpoint.js +246 -0
  62. package/src/ui/endpoints/pipeline-type-detail-endpoint.js +181 -0
  63. package/src/ui/endpoints/pipelines-endpoint.js +133 -0
  64. package/src/ui/endpoints/schema-file-endpoint.js +105 -0
  65. package/src/ui/endpoints/sse-endpoints.js +223 -0
  66. package/src/ui/endpoints/state-endpoint.js +85 -0
  67. package/src/ui/endpoints/task-analysis-endpoint.js +104 -0
  68. package/src/ui/endpoints/task-creation-endpoint.js +114 -0
  69. package/src/ui/endpoints/task-save-endpoint.js +101 -0
  70. package/src/ui/endpoints/upload-endpoints.js +406 -0
  71. package/src/ui/express-app.js +227 -0
  72. package/src/ui/lib/analysis-lock.js +67 -0
  73. package/src/ui/lib/sse.js +30 -0
  74. package/src/ui/server.js +42 -1880
  75. package/src/ui/sse-broadcast.js +93 -0
  76. package/src/ui/utils/http-utils.js +139 -0
  77. package/src/ui/utils/mime-types.js +196 -0
  78. package/src/ui/utils/slug.js +31 -0
  79. package/src/ui/vite.config.js +22 -0
  80. package/src/ui/watcher.js +28 -2
  81. package/src/utils/jobs.js +39 -0
  82. package/src/ui/dist/assets/index-DeDzq-Kk.js +0 -23863
  83. package/src/ui/dist/assets/style-aBtD_Yrs.css +0 -62
@@ -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
+ */
@@ -0,0 +1,28 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const parser = require("@babel/parser");
4
+
5
+ /**
6
+ * Parse task source code into a Babel AST.
7
+ *
8
+ * @param {string} code - The source code to parse
9
+ * @returns {import("@babel/types").File} The parsed AST
10
+ * @throws {Error} If parsing fails, includes syntax error location and message
11
+ */
12
+ export function parseTaskSource(code) {
13
+ try {
14
+ const ast = parser.parse(code, {
15
+ sourceType: "module",
16
+ plugins: ["jsx"],
17
+ });
18
+ return ast;
19
+ } catch (error) {
20
+ const loc = error.loc
21
+ ? `line ${error.loc.line}, column ${error.loc.column}`
22
+ : "unknown location";
23
+ throw new Error(
24
+ `Failed to parse task source code at ${loc}: ${error.message}`,
25
+ { cause: error }
26
+ );
27
+ }
28
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Check if a path is inside a try/catch block.
3
+ *
4
+ * @param {import("@babel/traverse").NodePath} path - The node path to check
5
+ * @returns {boolean} True if inside try/catch, false otherwise
6
+ */
7
+ export function isInsideTryCatch(path) {
8
+ return path.findParent((p) => p.isTryStatement()) !== null;
9
+ }
10
+
11
+ /**
12
+ * Get the stage name by finding the nearest exported function.
13
+ *
14
+ * Walks up the AST to find the parent ExportNamedDeclaration and returns
15
+ * the exported identifier name.
16
+ *
17
+ * @param {import("@babel/traverse").NodePath} path - The node path to start from
18
+ * @returns {string | null} The stage name or null if not in exported function
19
+ */
20
+ export function getStageName(path) {
21
+ const exportPath = path.findParent((p) => p.isExportNamedDeclaration());
22
+
23
+ if (!exportPath) {
24
+ return null;
25
+ }
26
+
27
+ const declaration = exportPath.node.declaration;
28
+
29
+ // Handle: export function name() {}
30
+ if (declaration?.type === "FunctionDeclaration") {
31
+ return declaration.id?.name ?? null;
32
+ }
33
+
34
+ // Handle: export const name = () => {}
35
+ if (declaration?.type === "VariableDeclaration") {
36
+ const declarator = declaration.declarations[0];
37
+ if (declarator?.id?.type === "Identifier") {
38
+ return declarator.id.name;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ }
@@ -3,6 +3,8 @@ import {
3
3
  normalizeTaskState,
4
4
  deriveJobStatusFromTasks,
5
5
  } from "../../../config/statuses.js";
6
+ import { classifyJobForDisplay } from "../../../utils/jobs.js";
7
+ import { decideTransition } from "../../../core/lifecycle-policy.js";
6
8
 
7
9
  /**
8
10
  * Normalize a raw task state into canonical enum.
@@ -225,6 +227,9 @@ export function adaptJobSummary(apiJob) {
225
227
  job.totalTokens = job.costsSummary.totalTokens;
226
228
  }
227
229
 
230
+ // Compute and attach display category for UI bucketing
231
+ job.displayCategory = classifyJobForDisplay(job);
232
+
228
233
  // Include warnings for debugging
229
234
  if (warnings.length > 0) job.__warnings = warnings;
230
235
 
@@ -284,8 +289,63 @@ export function adaptJobDetail(apiDetail) {
284
289
  detail.costs = apiDetail.costs;
285
290
  }
286
291
 
292
+ // Compute and attach display category for UI bucketing
293
+ detail.displayCategory = classifyJobForDisplay(detail);
294
+
287
295
  // Include warnings for debugging
288
296
  if (warnings.length > 0) detail.__warnings = warnings;
289
297
 
290
298
  return detail;
291
299
  }
300
+
301
+ /**
302
+ * deriveAllowedActions(adaptedJob, pipelineTasks)
303
+ * - adaptedJob: normalized job object from adaptJobSummary/adaptJobDetail
304
+ * - pipelineTasks: array of task names in execution order from pipeline.json
305
+ * Returns { start, restart } boolean flags for UI controls.
306
+ */
307
+ export function deriveAllowedActions(adaptedJob, pipelineTasks) {
308
+ // Check if any task is running
309
+ const hasRunningTask = Object.values(adaptedJob.tasks || {}).some(
310
+ (task) => task.state === "running"
311
+ );
312
+
313
+ // Default to disabled if job state is running or any task is running
314
+ if (adaptedJob.status === "running" || hasRunningTask) {
315
+ return { start: false, restart: false };
316
+ }
317
+
318
+ // Default to enabled for restart if not running
319
+ const restart = true;
320
+
321
+ // Start requires checking if ANY task can be started (not ALL tasks)
322
+ // Edge case: if no pipeline tasks, default to enabled
323
+ if (pipelineTasks.length === 0) {
324
+ return { start: true, restart };
325
+ }
326
+
327
+ const start = pipelineTasks.some((taskName) => {
328
+ const task = adaptedJob.tasks[taskName];
329
+ if (!task) return false; // Task not found, skip
330
+
331
+ const taskState = task.state || "pending";
332
+
333
+ // Check if all upstream tasks are done for dependency evaluation
334
+ const taskIndex = pipelineTasks.indexOf(taskName);
335
+ const upstreamTasks = pipelineTasks.slice(0, taskIndex);
336
+ const dependenciesReady = upstreamTasks.every((upstreamTaskName) => {
337
+ const upstreamTask = adaptedJob.tasks[upstreamTaskName];
338
+ return upstreamTask && upstreamTask.state === "done";
339
+ });
340
+
341
+ const startDecision = decideTransition({
342
+ op: "start",
343
+ taskState,
344
+ dependenciesReady,
345
+ });
346
+
347
+ return startDecision.ok;
348
+ });
349
+
350
+ return { start, restart };
351
+ }