@ryanfw/prompt-orchestration-pipeline 0.0.1 → 0.3.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 (76) hide show
  1. package/README.md +415 -24
  2. package/package.json +45 -8
  3. package/src/api/files.js +48 -0
  4. package/src/api/index.js +149 -53
  5. package/src/api/validators/seed.js +141 -0
  6. package/src/cli/index.js +456 -29
  7. package/src/cli/run-orchestrator.js +39 -0
  8. package/src/cli/update-pipeline-json.js +47 -0
  9. package/src/components/DAGGrid.jsx +649 -0
  10. package/src/components/JobCard.jsx +96 -0
  11. package/src/components/JobDetail.jsx +159 -0
  12. package/src/components/JobTable.jsx +202 -0
  13. package/src/components/Layout.jsx +134 -0
  14. package/src/components/TaskFilePane.jsx +570 -0
  15. package/src/components/UploadSeed.jsx +239 -0
  16. package/src/components/ui/badge.jsx +20 -0
  17. package/src/components/ui/button.jsx +43 -0
  18. package/src/components/ui/card.jsx +20 -0
  19. package/src/components/ui/focus-styles.css +60 -0
  20. package/src/components/ui/progress.jsx +26 -0
  21. package/src/components/ui/select.jsx +27 -0
  22. package/src/components/ui/separator.jsx +6 -0
  23. package/src/config/paths.js +99 -0
  24. package/src/core/config.js +270 -9
  25. package/src/core/file-io.js +202 -0
  26. package/src/core/module-loader.js +157 -0
  27. package/src/core/orchestrator.js +275 -294
  28. package/src/core/pipeline-runner.js +95 -41
  29. package/src/core/progress.js +66 -0
  30. package/src/core/status-writer.js +331 -0
  31. package/src/core/task-runner.js +719 -73
  32. package/src/core/validation.js +120 -1
  33. package/src/lib/utils.js +6 -0
  34. package/src/llm/README.md +139 -30
  35. package/src/llm/index.js +222 -72
  36. package/src/pages/PipelineDetail.jsx +111 -0
  37. package/src/pages/PromptPipelineDashboard.jsx +223 -0
  38. package/src/providers/deepseek.js +3 -15
  39. package/src/ui/client/adapters/job-adapter.js +258 -0
  40. package/src/ui/client/bootstrap.js +120 -0
  41. package/src/ui/client/hooks/useJobDetailWithUpdates.js +619 -0
  42. package/src/ui/client/hooks/useJobList.js +50 -0
  43. package/src/ui/client/hooks/useJobListWithUpdates.js +335 -0
  44. package/src/ui/client/hooks/useTicker.js +26 -0
  45. package/src/ui/client/index.css +31 -0
  46. package/src/ui/client/index.html +18 -0
  47. package/src/ui/client/main.jsx +38 -0
  48. package/src/ui/config-bridge.browser.js +149 -0
  49. package/src/ui/config-bridge.js +149 -0
  50. package/src/ui/config-bridge.node.js +310 -0
  51. package/src/ui/dist/assets/index-BDABnI-4.js +33399 -0
  52. package/src/ui/dist/assets/style-Ks8LY8gB.css +28496 -0
  53. package/src/ui/dist/index.html +19 -0
  54. package/src/ui/endpoints/job-endpoints.js +300 -0
  55. package/src/ui/file-reader.js +216 -0
  56. package/src/ui/job-change-detector.js +83 -0
  57. package/src/ui/job-index.js +231 -0
  58. package/src/ui/job-reader.js +274 -0
  59. package/src/ui/job-scanner.js +188 -0
  60. package/src/ui/public/app.js +3 -1
  61. package/src/ui/server.js +1636 -59
  62. package/src/ui/sse-enhancer.js +149 -0
  63. package/src/ui/sse.js +204 -0
  64. package/src/ui/state-snapshot.js +252 -0
  65. package/src/ui/transformers/list-transformer.js +347 -0
  66. package/src/ui/transformers/status-transformer.js +307 -0
  67. package/src/ui/watcher.js +61 -7
  68. package/src/utils/dag.js +101 -0
  69. package/src/utils/duration.js +126 -0
  70. package/src/utils/id-generator.js +30 -0
  71. package/src/utils/jobs.js +7 -0
  72. package/src/utils/pipelines.js +44 -0
  73. package/src/utils/task-files.js +271 -0
  74. package/src/utils/ui.jsx +76 -0
  75. package/src/ui/public/index.html +0 -53
  76. package/src/ui/public/style.css +0 -341
package/src/api/index.js CHANGED
@@ -1,28 +1,31 @@
1
- import { Orchestrator } from "../core/orchestrator.js";
1
+ import { startOrchestrator } from "../core/orchestrator.js";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs/promises";
4
- import { validateSeedOrThrow } from "../core/validation.js";
4
+ import { validateSeed } from "./validators/seed.js";
5
+ import { atomicWrite, cleanupOnFailure } from "./files.js";
6
+ import { getPipelineConfig } from "../core/config.js";
7
+ import {
8
+ getPendingSeedPath,
9
+ resolvePipelinePaths,
10
+ getJobDirectoryPath,
11
+ getJobMetadataPath,
12
+ getJobPipelinePath,
13
+ } from "../config/paths.js";
14
+ import { generateJobId } from "../utils/id-generator.js";
5
15
 
6
16
  // Pure functional utilities
7
17
  const createPaths = (config) => {
8
- const {
9
- rootDir,
10
- dataDir = "pipeline-data",
11
- configDir = "pipeline-config",
12
- } = config;
18
+ const { rootDir, dataDir = "pipeline-data" } = config;
13
19
  return {
14
20
  pending: path.join(rootDir, dataDir, "pending"),
15
21
  current: path.join(rootDir, dataDir, "current"),
16
22
  complete: path.join(rootDir, dataDir, "complete"),
17
- pipeline: path.join(rootDir, configDir, "pipeline.json"),
18
- tasks: path.join(rootDir, configDir, "tasks"),
19
23
  };
20
24
  };
21
25
 
22
26
  const validateConfig = (options = {}) => ({
23
27
  rootDir: options.rootDir || process.cwd(),
24
28
  dataDir: options.dataDir || "pipeline-data",
25
- configDir: options.configDir || "pipeline-config",
26
29
  autoStart: options.autoStart ?? true,
27
30
  ui: options.ui ?? false,
28
31
  uiPort: options.uiPort || 3000,
@@ -36,22 +39,10 @@ const ensureDirectories = async (paths) => {
36
39
  }
37
40
  };
38
41
 
39
- const loadPipelineDefinition = async (pipelinePath) => {
40
- try {
41
- const content = await fs.readFile(pipelinePath, "utf8");
42
- const definition = JSON.parse(content);
43
- definition.__path = pipelinePath;
44
- return definition;
45
- } catch (error) {
46
- if (error.code === "ENOENT") {
47
- throw new Error(`Pipeline definition not found at ${pipelinePath}`);
48
- }
49
- throw error;
50
- }
51
- };
52
-
53
- const createOrchestrator = (paths, pipelineDefinition) =>
54
- new Orchestrator({ paths, pipelineDefinition });
42
+ const createOrchestrator = (paths, pipelineDefinition, rootDir) =>
43
+ // Accept an explicit rootDir (project root) to avoid passing a subpath.
44
+ // Pipeline is mandatory - see docs/plans/multi-pipeline-backend-plan-PR2.md
45
+ startOrchestrator({ dataDir: rootDir || paths.pending });
55
46
 
56
47
  // Main API functions
57
48
  export const createPipelineOrchestrator = async (options = {}) => {
@@ -59,40 +50,39 @@ export const createPipelineOrchestrator = async (options = {}) => {
59
50
  const paths = createPaths(config);
60
51
 
61
52
  await ensureDirectories(paths);
62
- const pipelineDefinition = await loadPipelineDefinition(paths.pipeline);
63
- const orchestrator = createOrchestrator(paths, pipelineDefinition);
53
+
54
+ // Pass config.rootDir as the orchestrator dataDir root so the orchestrator resolves
55
+ // pipeline-data/... correctly (avoids duplicate path segments).
56
+ const orchestrator = await createOrchestrator(
57
+ paths,
58
+ null, // unused - pipeline definition will be loaded per-job
59
+ config.rootDir
60
+ );
64
61
 
65
62
  let uiServer = null;
66
63
 
67
64
  const state = {
68
65
  config,
69
66
  paths,
70
- pipelineDefinition,
67
+ pipelineDefinition: undefined, // TODO: multi-pipeline UI will provide per-slug previews from snapshots
71
68
  orchestrator,
72
69
  uiServer,
73
70
  };
74
71
 
75
- // Auto-start if configured
76
- if (config.autoStart) {
77
- await orchestrator.start();
78
- }
72
+ // Auto-start if configured (startOrchestrator handles this automatically)
73
+ // No need to call orchestrator.start() as startOrchestrator auto-starts when autoStart=true
79
74
 
80
75
  // Start UI if configured
81
76
  if (config.ui) {
82
- const { createUIServer } = await import("../ui/server.js");
77
+ const { startServer } = await import("../ui/server.js");
83
78
 
84
- // Create API object with state injection for UI server
85
- const uiApi = {
86
- submitJob: (seed) => submitJob(state, seed),
87
- getStatus: (jobName) => getStatus(state, jobName),
88
- listJobs: (status) => listJobs(state, status),
89
- };
90
-
91
- uiServer = createUIServer(uiApi);
92
- uiServer.listen(config.uiPort, () => {
93
- console.log(`Pipeline UI available at http://localhost:${config.uiPort}`);
79
+ uiServer = await startServer({
80
+ dataDir: config.rootDir,
81
+ port: config.uiPort,
94
82
  });
83
+
95
84
  state.uiServer = uiServer;
85
+ console.log(`Pipeline UI available at ${uiServer.url}`);
96
86
  }
97
87
 
98
88
  return state;
@@ -100,13 +90,120 @@ export const createPipelineOrchestrator = async (options = {}) => {
100
90
 
101
91
  // Job management functions
102
92
  export const submitJob = async (state, seed) => {
103
- // Validate seed structure before submitting
104
- validateSeedOrThrow(seed);
93
+ throw new Error(
94
+ "submitJob is deprecated. Use submitJobWithValidation instead for ID-only job submission."
95
+ );
96
+ };
97
+
98
+ /**
99
+ * Submit a job with comprehensive validation and atomic writes
100
+ * @param {Object} options - Options object
101
+ * @param {string} options.dataDir - Base data directory
102
+ * @param {Object} options.seedObject - Seed object to submit
103
+ * @returns {Promise<Object>} Result object with success status
104
+ */
105
+ export const submitJobWithValidation = async ({ dataDir, seedObject }) => {
106
+ let partialFiles = [];
107
+
108
+ try {
109
+ // Validate the seed object
110
+ const validatedSeed = await validateSeed(
111
+ JSON.stringify(seedObject),
112
+ dataDir
113
+ );
114
+
115
+ // Generate a random job ID
116
+ const jobId = generateJobId();
117
+
118
+ // Get the paths
119
+ const paths = resolvePipelinePaths(dataDir);
120
+ const pendingPath = getPendingSeedPath(dataDir, jobId);
121
+ const currentJobDir = getJobDirectoryPath(dataDir, jobId, "current");
122
+ const jobMetadataPath = getJobMetadataPath(dataDir, jobId, "current");
123
+ const jobPipelinePath = getJobPipelinePath(dataDir, jobId, "current");
124
+
125
+ // Ensure directories exist
126
+ await fs.mkdir(paths.pending, { recursive: true });
127
+ await fs.mkdir(currentJobDir, { recursive: true });
128
+
129
+ // Create job metadata
130
+ const jobMetadata = {
131
+ id: jobId,
132
+ name: validatedSeed.name,
133
+ pipeline: validatedSeed.pipeline, // Include pipeline slug
134
+ createdAt: new Date().toISOString(),
135
+ status: "pending",
136
+ };
105
137
 
106
- const name = seed.name;
107
- const seedPath = path.join(state.paths.pending, `${name}-seed.json`);
108
- await fs.writeFile(seedPath, JSON.stringify(seed, null, 2));
109
- return { name, seedPath };
138
+ // Read pipeline configuration for snapshot
139
+ let pipelineSnapshot = null;
140
+ try {
141
+ // Compute snapshot path from the seed-derived slug
142
+ const pipelineSlug = validatedSeed.pipeline;
143
+ const { pipelineJsonPath } = getPipelineConfig(pipelineSlug);
144
+ const pipelineContent = await fs.readFile(pipelineJsonPath, "utf8");
145
+ pipelineSnapshot = JSON.parse(pipelineContent);
146
+ } catch (error) {
147
+ // Handle unknown pipeline slug
148
+ if (
149
+ error.message.includes("Unknown pipeline") ||
150
+ error.message.includes("not found")
151
+ ) {
152
+ const errorMessage = "Unknown pipeline slug: " + validatedSeed.pipeline;
153
+ return {
154
+ success: false,
155
+ message: errorMessage,
156
+ };
157
+ }
158
+ // If pipeline config doesn't exist, create a minimal snapshot
159
+ pipelineSnapshot = {
160
+ tasks: [],
161
+ name: validatedSeed.pipeline,
162
+ };
163
+ }
164
+
165
+ // Write files atomically
166
+ partialFiles.push(pendingPath);
167
+ await atomicWrite(pendingPath, JSON.stringify(validatedSeed, null, 2));
168
+
169
+ partialFiles.push(jobMetadataPath);
170
+ await atomicWrite(jobMetadataPath, JSON.stringify(jobMetadata, null, 2));
171
+
172
+ partialFiles.push(jobPipelinePath);
173
+ await atomicWrite(
174
+ jobPipelinePath,
175
+ JSON.stringify(pipelineSnapshot, null, 2)
176
+ );
177
+
178
+ return {
179
+ success: true,
180
+ jobId,
181
+ jobName: validatedSeed.name,
182
+ message: "Seed file uploaded successfully",
183
+ };
184
+ } catch (error) {
185
+ // Clean up any partial files on failure
186
+ for (const filePath of partialFiles) {
187
+ try {
188
+ await cleanupOnFailure(filePath);
189
+ } catch (cleanupError) {
190
+ // Ignore cleanup errors
191
+ }
192
+ }
193
+
194
+ // Map validation errors to appropriate error messages
195
+ let errorMessage = error.message;
196
+ if (error.message.includes("Invalid JSON")) {
197
+ errorMessage = "Invalid JSON";
198
+ } else if (error.message.includes("required")) {
199
+ errorMessage = "Required fields missing";
200
+ }
201
+
202
+ return {
203
+ success: false,
204
+ message: errorMessage,
205
+ };
206
+ }
110
207
  };
111
208
 
112
209
  export const getStatus = async (state, jobName) => {
@@ -166,13 +263,13 @@ export const listJobs = async (state, status = "all") => {
166
263
 
167
264
  // Control functions
168
265
  export const start = async (state) => {
169
- await state.orchestrator.start();
266
+ // startOrchestrator already starts automatically, no need to call start
170
267
  return state;
171
268
  };
172
269
 
173
270
  export const stop = async (state) => {
174
271
  if (state.uiServer) {
175
- await new Promise((resolve) => state.uiServer.close(resolve));
272
+ await state.uiServer.close();
176
273
  }
177
274
  await state.orchestrator.stop();
178
275
  return state;
@@ -215,6 +312,5 @@ export const PipelineOrchestrator = {
215
312
 
216
313
  // Export the original functions for direct functional usage
217
314
  export { runPipeline } from "../core/task-runner.js";
218
- export { selectModel } from "../core/task-runner.js";
219
315
 
220
316
  export default PipelineOrchestrator;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Seed validation utilities
3
+ * @module api/validators/seed
4
+ */
5
+
6
+ import { promises as fs } from "fs";
7
+ import path from "path";
8
+ import { getPipelineConfig } from "../../core/config.js";
9
+
10
+ /**
11
+ * Validate JSON string and parse it
12
+ * @param {string} jsonString - JSON string to validate
13
+ * @returns {Object} Parsed JSON object
14
+ * @throws {Error} With message containing "Invalid JSON" on parse failure
15
+ */
16
+ function validateAndParseJson(jsonString) {
17
+ try {
18
+ return JSON.parse(jsonString);
19
+ } catch (error) {
20
+ throw new Error("Invalid JSON");
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Validate required fields in seed object
26
+ * @param {Object} seedObject - Seed object to validate
27
+ * @returns {Object} Validated seed object
28
+ * @throws {Error} With message containing "required" if fields are missing
29
+ */
30
+ function validateRequiredFields(seedObject) {
31
+ if (
32
+ !seedObject.name ||
33
+ typeof seedObject.name !== "string" ||
34
+ seedObject.name.trim() === ""
35
+ ) {
36
+ throw new Error("name field is required");
37
+ }
38
+
39
+ if (!seedObject.data || typeof seedObject.data !== "object") {
40
+ throw new Error("data field is required");
41
+ }
42
+
43
+ if (
44
+ !seedObject.pipeline ||
45
+ typeof seedObject.pipeline !== "string" ||
46
+ seedObject.pipeline.trim() === ""
47
+ ) {
48
+ throw new Error("pipeline field is required");
49
+ }
50
+
51
+ return seedObject;
52
+ }
53
+
54
+ /**
55
+ * Validate name format (alphanumeric + -/_ allowed)
56
+ * @param {string} name - Name to validate
57
+ * @returns {string} Validated name
58
+ * @throws {Error} With message containing "required" if format is invalid
59
+ */
60
+ function validateNameFormat(name) {
61
+ // Allow display-friendly names: required non-empty string, trim, length cap
62
+ if (!name || typeof name !== "string" || name.trim() === "") {
63
+ throw new Error("name field is required");
64
+ }
65
+ const trimmed = name.trim();
66
+ if (trimmed.length > 120) {
67
+ throw new Error("name must be 120 characters or less");
68
+ }
69
+ // Allow spaces and common punctuation for better UX
70
+ // Still disallow control characters and path traversal patterns
71
+ const dangerousPattern = /[\x00-\x1f\x7f-\x9f]/;
72
+ if (dangerousPattern.test(trimmed)) {
73
+ throw new Error("name must contain only printable characters");
74
+ }
75
+ return trimmed;
76
+ }
77
+
78
+ /**
79
+ * Check if a job with the given name already exists
80
+ * @param {string} baseDir - Base data directory
81
+ * @param {string} jobName - Job name to check
82
+ * @returns {Promise<boolean>} True if duplicate exists
83
+ */
84
+ async function checkDuplicateJob(baseDir, jobName) {
85
+ const { getPendingSeedPath, getCurrentSeedPath, getCompleteSeedPath } =
86
+ await import("../../config/paths.js");
87
+
88
+ const paths = [
89
+ getPendingSeedPath(baseDir, jobName),
90
+ getCurrentSeedPath(baseDir, jobName),
91
+ getCompleteSeedPath(baseDir, jobName),
92
+ ];
93
+
94
+ for (const filePath of paths) {
95
+ try {
96
+ await fs.access(filePath);
97
+ return true; // File exists, duplicate found
98
+ } catch (error) {
99
+ // File doesn't exist, continue checking
100
+ }
101
+ }
102
+
103
+ return false; // No duplicates found
104
+ }
105
+
106
+ /**
107
+ * Comprehensive seed validation
108
+ * @param {string} jsonString - JSON string to validate
109
+ * @param {string} baseDir - Base data directory for duplicate checking
110
+ * @returns {Promise<Object>} Validated seed object
111
+ * @throws {Error} With appropriate error message
112
+ */
113
+ async function validateSeed(jsonString, baseDir) {
114
+ // Step 1: Validate and parse JSON
115
+ const seedObject = validateAndParseJson(jsonString);
116
+
117
+ // Step 2: Validate required fields
118
+ const validatedObject = validateRequiredFields(seedObject);
119
+
120
+ // Step 3: Validate name format
121
+ validateNameFormat(validatedObject.name);
122
+
123
+ // Step 4: Validate pipeline slug against registry
124
+ getPipelineConfig(validatedObject.pipeline);
125
+
126
+ // Step 5: Check for duplicates
127
+ const isDuplicate = await checkDuplicateJob(baseDir, validatedObject.name);
128
+ if (isDuplicate) {
129
+ throw new Error("Job with this name already exists");
130
+ }
131
+
132
+ return validatedObject;
133
+ }
134
+
135
+ export {
136
+ validateAndParseJson,
137
+ validateRequiredFields,
138
+ validateNameFormat,
139
+ checkDuplicateJob,
140
+ validateSeed,
141
+ };