@ryanfw/prompt-orchestration-pipeline 0.15.1 → 0.16.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.
@@ -11,8 +11,8 @@
11
11
  />
12
12
  <title>Prompt Pipeline Dashboard</title>
13
13
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
14
- <script type="module" crossorigin src="/assets/index-B5HMRkR9.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/style-CoM9SoQF.css">
14
+ <script type="module" crossorigin src="/assets/index-DI_nRqVI.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/style-CVd3RRU2.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
@@ -7,6 +7,7 @@ import { analyzeTask } from "../../task-analysis/index.js";
7
7
  import { writeAnalysisFile } from "../../task-analysis/enrichers/analysis-writer.js";
8
8
  import { deduceArtifactSchema } from "../../task-analysis/enrichers/schema-deducer.js";
9
9
  import { writeSchemaFiles } from "../../task-analysis/enrichers/schema-writer.js";
10
+ import { resolveArtifactReference } from "../../task-analysis/enrichers/artifact-resolver.js";
10
11
 
11
12
  /**
12
13
  * Handle pipeline analysis endpoint.
@@ -134,6 +135,11 @@ export async function handlePipelineAnalysis(req, res) {
134
135
  }
135
136
  }
136
137
 
138
+ // Collect all known artifact filenames from writes for LLM resolution
139
+ const allKnownArtifacts = taskAnalyses.flatMap((t) =>
140
+ t.analysis.artifacts.writes.map((w) => w.fileName)
141
+ );
142
+
137
143
  // Send started event
138
144
  stream.send("started", {
139
145
  pipelineSlug: slug,
@@ -167,6 +173,59 @@ export async function handlePipelineAnalysis(req, res) {
167
173
  return;
168
174
  }
169
175
 
176
+ // Resolve unresolved artifact references using LLM
177
+ const unresolvedReads = analysis.artifacts.unresolvedReads || [];
178
+ const unresolvedWrites = analysis.artifacts.unresolvedWrites || [];
179
+
180
+ for (const unresolved of unresolvedReads) {
181
+ try {
182
+ const resolution = await resolveArtifactReference(
183
+ taskCode,
184
+ unresolved,
185
+ allKnownArtifacts
186
+ );
187
+ if (resolution.confidence >= 0.7 && resolution.resolvedFileName) {
188
+ // Check if this fileName already exists in reads array
189
+ const alreadyExists = analysis.artifacts.reads.some(
190
+ (artifact) => artifact.fileName === resolution.resolvedFileName
191
+ );
192
+ if (!alreadyExists) {
193
+ analysis.artifacts.reads.push({
194
+ fileName: resolution.resolvedFileName,
195
+ stage: unresolved.stage,
196
+ required: unresolved.required,
197
+ });
198
+ }
199
+ }
200
+ } catch {
201
+ // Silently skip failed resolutions
202
+ }
203
+ }
204
+
205
+ for (const unresolved of unresolvedWrites) {
206
+ try {
207
+ const resolution = await resolveArtifactReference(
208
+ taskCode,
209
+ unresolved,
210
+ allKnownArtifacts
211
+ );
212
+ if (resolution.confidence >= 0.7 && resolution.resolvedFileName) {
213
+ // Check if this fileName already exists in writes array
214
+ const alreadyExists = analysis.artifacts.writes.some(
215
+ (artifact) => artifact.fileName === resolution.resolvedFileName
216
+ );
217
+ if (!alreadyExists) {
218
+ analysis.artifacts.writes.push({
219
+ fileName: resolution.resolvedFileName,
220
+ stage: unresolved.stage,
221
+ });
222
+ }
223
+ }
224
+ } catch {
225
+ // Silently skip failed resolutions
226
+ }
227
+ }
228
+
170
229
  // Process each artifact write
171
230
  const artifacts = analysis.artifacts.writes;
172
231
  let jsonArtifactIndex = 0;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Pipeline artifacts endpoint
3
+ *
4
+ * Reads all *.analysis.json files, aggregates artifacts.writes,
5
+ * returns de-duplicated list.
6
+ *
7
+ * Response format: { ok: true, artifacts: [{ fileName, sources: [{ taskName, stage }] }] }
8
+ */
9
+
10
+ import { getPipelineConfig } from "../../core/config.js";
11
+ import { createErrorResponse, Constants } from "../config-bridge.js";
12
+ import { promises as fs } from "node:fs";
13
+ import path from "node:path";
14
+
15
+ /**
16
+ * HTTP handler for GET /api/pipelines/:slug/artifacts
17
+ *
18
+ * @param {http.IncomingMessage} req - HTTP request object
19
+ * @param {http.ServerResponse} res - HTTP response object
20
+ */
21
+ export async function handlePipelineArtifacts(req, res) {
22
+ const { slug } = req.params;
23
+
24
+ // Validate slug parameter
25
+ if (!slug || typeof slug !== "string") {
26
+ res
27
+ .status(400)
28
+ .json(
29
+ createErrorResponse(
30
+ Constants.ERROR_CODES.BAD_REQUEST,
31
+ "Invalid slug parameter"
32
+ )
33
+ );
34
+ return;
35
+ }
36
+
37
+ // Enforce safe characters in slug
38
+ const slugIsValid = /^[A-Za-z0-9_-]+$/.test(slug);
39
+ if (!slugIsValid) {
40
+ res
41
+ .status(400)
42
+ .json(
43
+ createErrorResponse(
44
+ Constants.ERROR_CODES.BAD_REQUEST,
45
+ "Invalid slug: only letters, numbers, hyphens, and underscores allowed"
46
+ )
47
+ );
48
+ return;
49
+ }
50
+
51
+ // Get pipeline config
52
+ let pipelineConfig;
53
+ try {
54
+ pipelineConfig = getPipelineConfig(slug);
55
+ } catch {
56
+ res
57
+ .status(404)
58
+ .json(
59
+ createErrorResponse(
60
+ Constants.ERROR_CODES.NOT_FOUND,
61
+ `Pipeline '${slug}' not found`
62
+ )
63
+ );
64
+ return;
65
+ }
66
+
67
+ // Determine analysis directory path
68
+ const pipelineDir = path.dirname(pipelineConfig.pipelineJsonPath);
69
+ const analysisDir = path.join(pipelineDir, "analysis");
70
+
71
+ // Check if analysis directory exists
72
+ try {
73
+ await fs.access(analysisDir);
74
+ } catch {
75
+ // No analysis directory - return empty artifacts
76
+ res.status(200).json({ ok: true, artifacts: [] });
77
+ return;
78
+ }
79
+
80
+ // Read all *.analysis.json files
81
+ const files = await fs.readdir(analysisDir);
82
+ const analysisFiles = files.filter((f) => f.endsWith(".analysis.json"));
83
+
84
+ // Aggregate artifacts with de-duplication
85
+ const artifactMap = new Map();
86
+
87
+ for (const file of analysisFiles) {
88
+ try {
89
+ const content = await fs.readFile(path.join(analysisDir, file), "utf8");
90
+ const analysis = JSON.parse(content);
91
+ const taskId = analysis.taskId || file.replace(".analysis.json", "");
92
+ const writes = analysis.artifacts?.writes || [];
93
+
94
+ for (const write of writes) {
95
+ const { fileName, stage } = write;
96
+ if (!artifactMap.has(fileName)) {
97
+ artifactMap.set(fileName, { fileName, sources: [] });
98
+ }
99
+ artifactMap.get(fileName).sources.push({ taskName: taskId, stage });
100
+ }
101
+ } catch {
102
+ // Skip malformed files
103
+ continue;
104
+ }
105
+ }
106
+
107
+ const artifacts = Array.from(artifactMap.values());
108
+ res.status(200).json({ ok: true, artifacts });
109
+ }
@@ -24,6 +24,7 @@ import { handlePipelineTypeDetailRequest } from "./endpoints/pipeline-type-detai
24
24
  import { handlePipelineAnalysis } from "./endpoints/pipeline-analysis-endpoint.js";
25
25
  import { handleTaskAnalysisRequest } from "./endpoints/task-analysis-endpoint.js";
26
26
  import { handleSchemaFileRequest } from "./endpoints/schema-file-endpoint.js";
27
+ import { handlePipelineArtifacts } from "./endpoints/pipeline-artifacts-endpoint.js";
27
28
  import { handleTaskPlan } from "./endpoints/task-creation-endpoint.js";
28
29
  import { handleTaskSave } from "./endpoints/task-save-endpoint.js";
29
30
  import { sendJson } from "./utils/http-utils.js";
@@ -130,6 +131,9 @@ export function buildExpressApp({ dataDir, viteServer }) {
130
131
  await handlePipelineTypeDetailRequest(req, res);
131
132
  });
132
133
 
134
+ // GET /api/pipelines/:slug/artifacts
135
+ app.get("/api/pipelines/:slug/artifacts", handlePipelineArtifacts);
136
+
133
137
  // POST /api/pipelines/:slug/analyze
134
138
  app.post("/api/pipelines/:slug/analyze", handlePipelineAnalysis);
135
139
 
package/src/ui/watcher.js CHANGED
@@ -7,6 +7,9 @@ import chokidar from "chokidar";
7
7
  import path from "node:path";
8
8
  import { detectJobChange } from "./job-change-detector.js";
9
9
  import { sseEnhancer } from "./sse-enhancer.js";
10
+ import { createLogger } from "../core/logger.js";
11
+
12
+ const logger = createLogger("Watcher");
10
13
 
11
14
  /**
12
15
  * Normalize path separators to forward slash and trim
@@ -69,17 +72,17 @@ export function start(paths, onChange, options = {}) {
69
72
  // Always use relative path for consistency with tests
70
73
  const normalizedPath = rel;
71
74
 
72
- console.debug("[Watcher] File added:", normalizedPath);
75
+ logger.debug("File added:", normalizedPath);
73
76
 
74
77
  // Detect registry.json changes and reload config
75
78
  if (normalizedPath === "pipeline-config/registry.json") {
76
- console.log("[Watcher] registry.json added, reloading config...");
79
+ logger.log("registry.json added, reloading config...");
77
80
  try {
78
81
  const { resetConfig } = await import("../core/config.js");
79
82
  resetConfig();
80
- console.log("[Watcher] Config cache invalidated successfully");
83
+ logger.log("Config cache invalidated successfully");
81
84
  } catch (error) {
82
- console.error("[Watcher] Failed to reload config:", error);
85
+ logger.error("Failed to reload config:", error);
83
86
  }
84
87
  }
85
88
 
@@ -89,7 +92,7 @@ export function start(paths, onChange, options = {}) {
89
92
  // Check for job-specific changes with normalized path
90
93
  const jobChange = detectJobChange(normalizedPath);
91
94
  if (jobChange) {
92
- console.debug("[Watcher] Job change detected:", jobChange);
95
+ logger.debug("Job change detected:", jobChange);
93
96
  sseEnhancer.handleJobChange(jobChange);
94
97
  }
95
98
  });
@@ -100,17 +103,24 @@ export function start(paths, onChange, options = {}) {
100
103
  // Always use relative path for consistency with tests
101
104
  const normalizedPath = rel;
102
105
 
103
- console.debug("[Watcher] File changed:", normalizedPath);
106
+ // Skip "modified" events for files under pipeline-data/.../files/
107
+ // (logs etc. are frequently updated but frontend only cares about creation)
108
+ if (/pipeline-data\/[^/]+\/[^/]+\/files\//.test(normalizedPath)) {
109
+ logger.debug("Skipping files/ modification:", normalizedPath);
110
+ return;
111
+ }
112
+
113
+ logger.debug("File changed:", normalizedPath);
104
114
 
105
115
  // Detect registry.json changes and reload config
106
116
  if (normalizedPath === "pipeline-config/registry.json") {
107
- console.log("[Watcher] registry.json modified, reloading config...");
117
+ logger.log("registry.json modified, reloading config...");
108
118
  try {
109
119
  const { resetConfig } = await import("../core/config.js");
110
120
  resetConfig();
111
- console.log("[Watcher] Config cache invalidated successfully");
121
+ logger.log("Config cache invalidated successfully");
112
122
  } catch (error) {
113
- console.error("[Watcher] Failed to reload config:", error);
123
+ logger.error("Failed to reload config:", error);
114
124
  }
115
125
  }
116
126
 
@@ -120,7 +130,7 @@ export function start(paths, onChange, options = {}) {
120
130
  // Check for job-specific changes with normalized path
121
131
  const jobChange = detectJobChange(normalizedPath);
122
132
  if (jobChange) {
123
- console.debug("[Watcher] Job change detected:", jobChange);
133
+ logger.debug("Job change detected:", jobChange);
124
134
  sseEnhancer.handleJobChange(jobChange);
125
135
  }
126
136
  });