@ryanfw/prompt-orchestration-pipeline 1.2.5 → 1.2.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryanfw/prompt-orchestration-pipeline",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "A Prompt-orchestration pipeline (POP) is a framework for building, running, and experimenting with complex chains of LLM tasks.",
5
5
  "type": "module",
6
6
  "main": "src/ui/server/index.ts",
@@ -212,13 +212,10 @@ export function resetJobFromTask(jobDir: string, fromTask: string, options?: Res
212
212
 
213
213
  return writeJobStatus(jobDir, (snapshot) => {
214
214
  const taskKeys = Object.keys(snapshot.tasks);
215
- const totalCount = taskKeys.length;
216
- const doneCount = taskKeys.filter((k) => snapshot.tasks[k]!.state === "done").length;
217
215
 
218
216
  snapshot.state = "pending";
219
217
  snapshot.current = null;
220
218
  snapshot.currentStage = null;
221
- snapshot.progress = totalCount > 0 ? (doneCount / totalCount) * 100 : 0;
222
219
 
223
220
  const fromIndex = taskKeys.indexOf(fromTask);
224
221
  const resetKeys = fromIndex === -1 ? [] : taskKeys.slice(fromIndex);
@@ -795,7 +795,17 @@ export async function runPipeline(
795
795
 
796
796
  // Write done status (best-effort)
797
797
  try {
798
+ const lastStage = KNOWN_STAGES[KNOWN_STAGES.length - 1];
799
+ const doneProgress = computeDeterministicProgress(
800
+ pipelineTasks ?? [taskName],
801
+ taskName,
802
+ lastStage,
803
+ );
798
804
  await writeJobStatus(jobDir, (snapshot: StatusSnapshot) => {
805
+ snapshot.state = TaskState.DONE;
806
+ snapshot.progress = doneProgress;
807
+ snapshot.current = null;
808
+ snapshot.currentStage = null;
799
809
  if (!snapshot.tasks[taskName]) snapshot.tasks[taskName] = {};
800
810
  snapshot.tasks[taskName]!.state = TaskState.DONE;
801
811
  snapshot.tasks[taskName]!.currentStage = null;
@@ -102,6 +102,32 @@ describe("job adapter", () => {
102
102
  expect(job.doneCount).toBe(1);
103
103
  });
104
104
 
105
+ it("computes progress from pipelineConfig taskCount, ignoring api progress", () => {
106
+ const job = adaptJobSummary({
107
+ jobId: "job-1",
108
+ progress: 100,
109
+ tasks: {
110
+ build: { state: "done" },
111
+ test: { state: "done" },
112
+ lint: { state: "done" },
113
+ },
114
+ pipelineConfig: {
115
+ tasks: [
116
+ { name: "build" },
117
+ { name: "test" },
118
+ { name: "lint" },
119
+ { name: "deploy" },
120
+ { name: "verify" },
121
+ { name: "publish" },
122
+ ],
123
+ },
124
+ });
125
+
126
+ expect(job.taskCount).toBe(6);
127
+ expect(job.doneCount).toBe(3);
128
+ expect(job.progress).toBe(50);
129
+ });
130
+
105
131
  it("falls back to taskList length when pipelineConfig is absent", () => {
106
132
  const job = adaptJobSummary({
107
133
  jobId: "job-1",
@@ -68,6 +68,49 @@ describe("useJobDetailWithUpdates helpers", () => {
68
68
  expect(extractJobDetail({ ok: true, data: { jobId: "job-1" } })).toEqual({ jobId: "job-1" });
69
69
  });
70
70
 
71
+ it("uses pipelineConfig.tasks.length as authoritative denominator", () => {
72
+ const detail: NormalizedJobDetail = {
73
+ ...makeDetail("job-1"),
74
+ pipelineConfig: {
75
+ tasks: ["a", "b", "c", "d", "e", "f"],
76
+ },
77
+ taskCount: 6,
78
+ tasks: {
79
+ a: { name: "a", state: "done", startedAt: null, endedAt: null, files: { artifacts: [], logs: [], tmp: [] } },
80
+ b: { name: "b", state: "done", startedAt: null, endedAt: null, files: { artifacts: [], logs: [], tmp: [] } },
81
+ c: { name: "c", state: "done", startedAt: null, endedAt: null, files: { artifacts: [], logs: [], tmp: [] } },
82
+ d: { name: "d", state: "running", startedAt: null, endedAt: null, files: { artifacts: [], logs: [], tmp: [] } },
83
+ },
84
+ };
85
+
86
+ const next = applyDetailEvent(detail, {
87
+ type: "task:updated",
88
+ data: { jobId: "job-1", taskName: "d", task: { state: "running" } },
89
+ });
90
+
91
+ expect(next.taskCount).toBe(6);
92
+ expect(next.doneCount).toBe(3);
93
+ expect(next.progress).toBe(50);
94
+ });
95
+
96
+ it("falls back to local task list length without pipelineConfig", () => {
97
+ const detail: NormalizedJobDetail = {
98
+ ...makeDetail("job-1"),
99
+ pipelineConfig: undefined,
100
+ };
101
+
102
+ const next = applyDetailEvent(detail, {
103
+ type: "task:updated",
104
+ data: { jobId: "job-1", taskName: "build", task: { state: "done" } },
105
+ });
106
+
107
+ expect(next.taskCount).toBe(2);
108
+ expect(next.doneCount).toBe(1);
109
+ expect(next.progress).toBe(50);
110
+ expect(next.progress).toBeGreaterThanOrEqual(0);
111
+ expect(next.progress).toBeLessThanOrEqual(100);
112
+ });
113
+
71
114
  it("exports the detail debounce constant", () => {
72
115
  expect(REFRESH_DEBOUNCE_MS).toBe(200);
73
116
  });
@@ -139,9 +139,11 @@ function adaptBaseJob(apiJob: Record<string, unknown>): NormalizedJobSummary {
139
139
  const taskCount = pipelineTaskArray ? pipelineTaskArray.length : taskList.length;
140
140
  const inferredStatus = deriveJobStatusFromTasks(taskList);
141
141
  const status = normalizeJobStatus(apiJob["status"] ?? inferredStatus);
142
- const progress = typeof apiJob["progress"] === "number"
143
- ? apiJob["progress"]
144
- : taskCount === 0 ? 0 : Math.floor((doneCount / taskCount) * 100);
142
+ const progress = pipelineTaskArray
143
+ ? (taskCount === 0 ? 0 : Math.min(100, Math.floor((doneCount / taskCount) * 100)))
144
+ : typeof apiJob["progress"] === "number"
145
+ ? apiJob["progress"]
146
+ : taskCount === 0 ? 0 : Math.min(100, Math.floor((doneCount / taskCount) * 100));
145
147
 
146
148
  return {
147
149
  id: typeof apiJob["id"] === "string" ? apiJob["id"] : String(apiJob["jobId"] ?? ""),
@@ -22,15 +22,21 @@ export function extractJobDetail(payload: unknown): Record<string, unknown> | nu
22
22
  return null;
23
23
  }
24
24
 
25
+ function getPipelineTaskCount(detail: NormalizedJobDetail): number | null {
26
+ const config = detail.pipelineConfig;
27
+ if (config && Array.isArray(config["tasks"])) return config["tasks"].length;
28
+ return null;
29
+ }
30
+
25
31
  function recomputeProgress(detail: NormalizedJobDetail): NormalizedJobDetail {
26
32
  const tasks = Object.values(detail.tasks);
27
33
  const doneCount = tasks.filter((task) => task.state === "done").length;
28
- const taskCount = tasks.length;
34
+ const taskCount = getPipelineTaskCount(detail) ?? tasks.length;
29
35
  return {
30
36
  ...detail,
31
37
  doneCount,
32
38
  taskCount,
33
- progress: taskCount === 0 ? 0 : Math.floor((doneCount / taskCount) * 100),
39
+ progress: taskCount === 0 ? 0 : Math.min(100, Math.floor((doneCount / taskCount) * 100)),
34
40
  updatedAt: new Date().toISOString(),
35
41
  };
36
42
  }
@@ -21910,7 +21910,7 @@ function adaptBaseJob(apiJob) {
21910
21910
  const taskCount = pipelineTaskArray ? pipelineTaskArray.length : taskList.length;
21911
21911
  const inferredStatus = deriveJobStatusFromTasks(taskList);
21912
21912
  const status = normalizeJobStatus(apiJob["status"] ?? inferredStatus);
21913
- const progress = typeof apiJob["progress"] === "number" ? apiJob["progress"] : taskCount === 0 ? 0 : Math.floor(doneCount / taskCount * 100);
21913
+ const progress = pipelineTaskArray ? taskCount === 0 ? 0 : Math.min(100, Math.floor(doneCount / taskCount * 100)) : typeof apiJob["progress"] === "number" ? apiJob["progress"] : taskCount === 0 ? 0 : Math.min(100, Math.floor(doneCount / taskCount * 100));
21914
21914
  return {
21915
21915
  id: typeof apiJob["id"] === "string" ? apiJob["id"] : String(apiJob["jobId"] ?? ""),
21916
21916
  jobId: typeof apiJob["jobId"] === "string" ? apiJob["jobId"] : String(apiJob["id"] ?? ""),
@@ -21968,15 +21968,20 @@ function extractJobDetail(payload) {
21968
21968
  if (isRecord$2(payload)) return payload;
21969
21969
  return null;
21970
21970
  }
21971
+ function getPipelineTaskCount(detail) {
21972
+ const config = detail.pipelineConfig;
21973
+ if (config && Array.isArray(config["tasks"])) return config["tasks"].length;
21974
+ return null;
21975
+ }
21971
21976
  function recomputeProgress(detail) {
21972
21977
  const tasks = Object.values(detail.tasks);
21973
21978
  const doneCount = tasks.filter((task) => task.state === "done").length;
21974
- const taskCount = tasks.length;
21979
+ const taskCount = getPipelineTaskCount(detail) ?? tasks.length;
21975
21980
  return {
21976
21981
  ...detail,
21977
21982
  doneCount,
21978
21983
  taskCount,
21979
- progress: taskCount === 0 ? 0 : Math.floor(doneCount / taskCount * 100),
21984
+ progress: taskCount === 0 ? 0 : Math.min(100, Math.floor(doneCount / taskCount * 100)),
21980
21985
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21981
21986
  };
21982
21987
  }