@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.
- package/package.json +11 -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/DAGGrid.jsx +157 -47
- 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/RestartJobModal.jsx +26 -6
- package/src/components/ui/StopJobModal.jsx +183 -0
- 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 +11 -4
- package/src/core/lifecycle-policy.js +62 -0
- package/src/core/pipeline-runner.js +312 -217
- package/src/core/status-writer.js +84 -0
- package/src/llm/index.js +129 -9
- package/src/pages/Code.jsx +8 -1
- package/src/pages/PipelineDetail.jsx +84 -2
- package/src/pages/PipelineList.jsx +214 -0
- package/src/pages/PipelineTypeDetail.jsx +234 -0
- package/src/pages/PromptPipelineDashboard.jsx +10 -11
- 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/adapters/job-adapter.js +60 -0
- package/src/ui/client/api.js +233 -8
- package/src/ui/client/hooks/useAnalysisProgress.js +145 -0
- package/src/ui/client/hooks/useJobList.js +14 -1
- 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/app.js +262 -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/favicon.svg +12 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/endpoints/create-pipeline-endpoint.js +194 -0
- package/src/ui/endpoints/file-endpoints.js +330 -0
- package/src/ui/endpoints/job-control-endpoints.js +1001 -0
- package/src/ui/endpoints/job-endpoints.js +62 -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/sse-endpoints.js +223 -0
- package/src/ui/endpoints/state-endpoint.js +85 -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/endpoints/upload-endpoints.js +406 -0
- package/src/ui/express-app.js +227 -0
- package/src/ui/lib/analysis-lock.js +67 -0
- package/src/ui/lib/sse.js +30 -0
- package/src/ui/server.js +42 -1880
- package/src/ui/sse-broadcast.js +93 -0
- package/src/ui/utils/http-utils.js +139 -0
- package/src/ui/utils/mime-types.js +196 -0
- package/src/ui/utils/slug.js +31 -0
- package/src/ui/vite.config.js +22 -0
- package/src/ui/watcher.js +28 -2
- package/src/utils/jobs.js +39 -0
- package/src/ui/dist/assets/index-DeDzq-Kk.js +0 -23863
- 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
|
+
}
|