@ryanfw/prompt-orchestration-pipeline 1.3.3 → 1.3.4

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 (198) hide show
  1. package/docs/http-api.md +14 -4
  2. package/package.json +1 -4
  3. package/src/api/__tests__/index.test.ts +144 -41
  4. package/src/api/index.ts +15 -83
  5. package/src/cli/__tests__/analyze-task.test.ts +40 -7
  6. package/src/cli/__tests__/index.test.ts +337 -17
  7. package/src/cli/__tests__/types.test.ts +0 -18
  8. package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
  9. package/src/cli/analyze-task.ts +3 -14
  10. package/src/cli/index.ts +73 -61
  11. package/src/cli/types.ts +0 -13
  12. package/src/cli/update-pipeline-json.ts +3 -14
  13. package/src/config/__tests__/paths.test.ts +46 -1
  14. package/src/config/__tests__/pipeline-registry.test.ts +767 -0
  15. package/src/config/__tests__/sse-events.test.ts +470 -0
  16. package/src/config/paths.ts +11 -2
  17. package/src/config/pipeline-registry.ts +258 -0
  18. package/src/config/sse-events.ts +122 -0
  19. package/src/core/__tests__/agent-step.test.ts +115 -0
  20. package/src/core/__tests__/config.test.ts +517 -107
  21. package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
  22. package/src/core/__tests__/job-concurrency.test.ts +200 -0
  23. package/src/core/__tests__/job-submission.test.ts +396 -0
  24. package/src/core/__tests__/job-view.test.ts +485 -3
  25. package/src/core/__tests__/json-file.test.ts +111 -0
  26. package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
  27. package/src/core/__tests__/logger.test.ts +22 -0
  28. package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
  29. package/src/core/__tests__/orchestrator.test.ts +373 -2
  30. package/src/core/__tests__/pipeline-runner.test.ts +946 -9
  31. package/src/core/__tests__/redact.test.ts +153 -0
  32. package/src/core/__tests__/runner-liveness.test.ts +45 -0
  33. package/src/core/__tests__/seed-naming.test.ts +57 -0
  34. package/src/core/__tests__/single-derivation.test.ts +1 -1
  35. package/src/core/__tests__/task-runner.test.ts +594 -3
  36. package/src/core/__tests__/task-telemetry.test.ts +241 -0
  37. package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
  38. package/src/core/agent-step.ts +4 -1
  39. package/src/core/agent-types.ts +5 -4
  40. package/src/core/config.ts +127 -234
  41. package/src/core/control.ts +3 -0
  42. package/src/core/job-concurrency.ts +50 -19
  43. package/src/core/job-submission.ts +133 -0
  44. package/src/core/job-view.ts +162 -18
  45. package/src/core/json-file.ts +45 -0
  46. package/src/core/lifecycle-policy.ts +23 -8
  47. package/src/core/logger.ts +0 -29
  48. package/src/core/orchestrator.ts +124 -70
  49. package/src/core/pipeline-runner.ts +85 -53
  50. package/src/core/redact.ts +40 -0
  51. package/src/core/runner-liveness.ts +8 -4
  52. package/src/core/seed-naming.ts +9 -0
  53. package/src/core/status-writer.ts +3 -28
  54. package/src/core/task-runner.ts +356 -319
  55. package/src/core/task-telemetry.ts +107 -0
  56. package/src/harness/__tests__/subprocess.test.ts +112 -1
  57. package/src/harness/subprocess.ts +55 -16
  58. package/src/llm/__tests__/index.test.ts +684 -33
  59. package/src/llm/index.ts +310 -67
  60. package/src/providers/__tests__/alibaba.test.ts +67 -6
  61. package/src/providers/__tests__/anthropic.test.ts +35 -14
  62. package/src/providers/__tests__/base.test.ts +62 -0
  63. package/src/providers/__tests__/claude-code.test.ts +99 -14
  64. package/src/providers/__tests__/deepseek.test.ts +16 -6
  65. package/src/providers/__tests__/gemini.test.ts +47 -25
  66. package/src/providers/__tests__/moonshot.test.ts +27 -0
  67. package/src/providers/__tests__/openai.test.ts +262 -74
  68. package/src/providers/__tests__/opencode.test.ts +77 -0
  69. package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
  70. package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
  71. package/src/providers/__tests__/types.test.ts +85 -0
  72. package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
  73. package/src/providers/__tests__/zhipu.test.ts +27 -14
  74. package/src/providers/alibaba.ts +20 -39
  75. package/src/providers/anthropic.ts +23 -11
  76. package/src/providers/base.ts +19 -0
  77. package/src/providers/claude-code.ts +27 -18
  78. package/src/providers/deepseek.ts +9 -28
  79. package/src/providers/gemini.ts +20 -58
  80. package/src/providers/moonshot.ts +15 -11
  81. package/src/providers/openai.ts +79 -61
  82. package/src/providers/opencode.ts +16 -33
  83. package/src/providers/stream-accumulator.ts +27 -21
  84. package/src/providers/types.ts +29 -4
  85. package/src/providers/zhipu.ts +15 -44
  86. package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
  87. package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
  88. package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
  89. package/src/task-analysis/__tests__/repository.test.ts +65 -0
  90. package/src/task-analysis/__tests__/types.test.ts +63 -130
  91. package/src/task-analysis/analyzer.ts +91 -0
  92. package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
  93. package/src/task-analysis/index.ts +2 -36
  94. package/src/task-analysis/repository.ts +45 -0
  95. package/src/task-analysis/types.ts +42 -58
  96. package/src/ui/client/__tests__/api.test.ts +143 -1
  97. package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
  98. package/src/ui/client/__tests__/job-adapter.test.ts +125 -2
  99. package/src/ui/client/__tests__/load-state.test.ts +70 -0
  100. package/src/ui/client/__tests__/types.test.ts +66 -3
  101. package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
  102. package/src/ui/client/__tests__/useJobList.test.ts +198 -23
  103. package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
  104. package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
  105. package/src/ui/client/adapters/job-adapter.ts +31 -16
  106. package/src/ui/client/api.ts +38 -15
  107. package/src/ui/client/bootstrap.ts +19 -14
  108. package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
  109. package/src/ui/client/hooks/useJobList.ts +26 -31
  110. package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
  111. package/src/ui/client/load-state.ts +20 -0
  112. package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
  113. package/src/ui/client/reducers/job-events.ts +137 -0
  114. package/src/ui/client/types.ts +14 -20
  115. package/src/ui/components/DAGGrid.tsx +6 -1
  116. package/src/ui/components/JobDetail.tsx +12 -4
  117. package/src/ui/components/JobTable.tsx +13 -2
  118. package/src/ui/components/StageTimeline.tsx +8 -20
  119. package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
  120. package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
  121. package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
  122. package/src/ui/components/__tests__/JobTable.test.tsx +83 -0
  123. package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
  124. package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
  125. package/src/ui/components/types.ts +33 -15
  126. package/src/ui/dist/assets/{index-L6cvsCAx.js → index-DN3-zvtP.js} +4299 -251
  127. package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
  128. package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
  129. package/src/ui/dist/index.html +2 -2
  130. package/src/ui/pages/PipelineDetail.tsx +60 -4
  131. package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
  132. package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
  133. package/src/ui/pages/__tests__/pages.test.tsx +236 -42
  134. package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
  135. package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
  136. package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
  137. package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
  138. package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
  139. package/src/ui/server/__tests__/index.test.ts +63 -0
  140. package/src/ui/server/__tests__/job-control-endpoints.test.ts +455 -1
  141. package/src/ui/server/__tests__/job-endpoints.test.ts +43 -1
  142. package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
  143. package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
  144. package/src/ui/server/__tests__/router-threading.test.ts +148 -0
  145. package/src/ui/server/__tests__/router.test.ts +104 -0
  146. package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
  147. package/src/ui/server/__tests__/sse-enhancer.test.ts +39 -0
  148. package/src/ui/server/config-bridge-node.ts +8 -26
  149. package/src/ui/server/embedded-assets-imports.d.ts +44 -0
  150. package/src/ui/server/embedded-assets.ts +13 -2
  151. package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
  152. package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
  153. package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
  154. package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
  155. package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
  156. package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
  157. package/src/ui/server/endpoints/file-endpoints.ts +15 -10
  158. package/src/ui/server/endpoints/job-control-endpoints.ts +122 -73
  159. package/src/ui/server/endpoints/job-endpoints.ts +1 -0
  160. package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
  161. package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
  162. package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
  163. package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
  164. package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
  165. package/src/ui/server/index.ts +19 -14
  166. package/src/ui/server/job-reader.ts +14 -2
  167. package/src/ui/server/router.ts +33 -10
  168. package/src/ui/server/sse-broadcast.ts +14 -3
  169. package/src/ui/server/sse-enhancer.ts +12 -2
  170. package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
  171. package/src/ui/state/__tests__/snapshot.test.ts +70 -15
  172. package/src/ui/state/__tests__/types.test.ts +6 -0
  173. package/src/ui/state/snapshot.ts +1 -1
  174. package/src/ui/state/transformers/__tests__/list-transformer.test.ts +42 -0
  175. package/src/ui/state/transformers/__tests__/status-transformer.test.ts +45 -3
  176. package/src/ui/state/transformers/list-transformer.ts +6 -0
  177. package/src/ui/state/types.ts +6 -1
  178. package/src/utils/__tests__/path-containment.test.ts +160 -0
  179. package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
  180. package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
  181. package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
  182. package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
  183. package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
  184. package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
  185. package/src/task-analysis/__tests__/index.test.ts +0 -143
  186. package/src/task-analysis/__tests__/parser.test.ts +0 -41
  187. package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
  188. package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
  189. package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
  190. package/src/task-analysis/extractors/artifacts.ts +0 -143
  191. package/src/task-analysis/extractors/llm-calls.ts +0 -117
  192. package/src/task-analysis/extractors/stages.ts +0 -45
  193. package/src/task-analysis/parser.ts +0 -20
  194. package/src/task-analysis/utils/ast.ts +0 -45
  195. package/src/ui/dist/assets/index-L6cvsCAx.js.map +0 -1
  196. package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
  197. package/src/ui/embedded-assets.js +0 -12
  198. package/src/ui/server/__tests__/path-containment.test.ts +0 -54
@@ -1,143 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { analyzeTask } from "../index.js";
3
-
4
- const TASK_CODE = `
5
- export async function ingestion({ io, llm }) {
6
- const data = await io.readArtifact("raw-data.json");
7
- await io.writeArtifact("ingested.json", data);
8
- await llm.openai.complete({ prompt: "summarize" });
9
- }
10
-
11
- export function parsing({ io }) {
12
- try {
13
- const extra = io.readArtifact("optional.json");
14
- } catch {}
15
- const result = io.readArtifact("ingested.json");
16
- io.writeArtifact("parsed.json", result);
17
- }
18
-
19
- export const transform = async ({ io, llm }) => {
20
- const input = await io.readArtifact("parsed.json");
21
- await llm.anthropic.chat({ prompt: "transform" });
22
- await io.writeArtifact("transformed.json", input);
23
- };
24
- `.trim();
25
-
26
- describe("analyzeTask", () => {
27
- it("returns null taskFilePath when not provided", () => {
28
- const result = analyzeTask(TASK_CODE);
29
- expect(result.taskFilePath).toBeNull();
30
- });
31
-
32
- it("returns null taskFilePath when explicitly passed null", () => {
33
- const result = analyzeTask(TASK_CODE, null);
34
- expect(result.taskFilePath).toBeNull();
35
- });
36
-
37
- it("returns provided taskFilePath", () => {
38
- const result = analyzeTask(TASK_CODE, "/path/to/task.js");
39
- expect(result.taskFilePath).toBe("/path/to/task.js");
40
- });
41
-
42
- it("extracts all stages in order", () => {
43
- const result = analyzeTask(TASK_CODE);
44
- expect(result.stages).toHaveLength(3);
45
-
46
- const names = result.stages.map((s) => s.name);
47
- expect(names).toEqual(["ingestion", "parsing", "transform"]);
48
-
49
- expect(result.stages[0]?.isAsync).toBe(true);
50
- expect(result.stages[1]?.isAsync).toBe(false);
51
- expect(result.stages[2]?.isAsync).toBe(true);
52
- });
53
-
54
- it("extracts artifact reads", () => {
55
- const result = analyzeTask(TASK_CODE);
56
- const readNames = result.artifacts.reads.map((r) => r.fileName);
57
- expect(readNames).toContain("raw-data.json");
58
- expect(readNames).toContain("ingested.json");
59
- expect(readNames).toContain("parsed.json");
60
- });
61
-
62
- it("marks reads inside try blocks as not required", () => {
63
- const result = analyzeTask(TASK_CODE);
64
- const optional = result.artifacts.reads.find(
65
- (r) => r.fileName === "optional.json"
66
- );
67
- expect(optional).toBeDefined();
68
- expect(optional?.required).toBe(false);
69
- });
70
-
71
- it("marks reads outside try blocks as required", () => {
72
- const result = analyzeTask(TASK_CODE);
73
- const required = result.artifacts.reads.filter((r) => r.required);
74
- const names = required.map((r) => r.fileName);
75
- expect(names).toContain("raw-data.json");
76
- expect(names).toContain("ingested.json");
77
- expect(names).toContain("parsed.json");
78
- });
79
-
80
- it("extracts artifact writes", () => {
81
- const result = analyzeTask(TASK_CODE);
82
- const writeNames = result.artifacts.writes.map((w) => w.fileName);
83
- expect(writeNames).toContain("ingested.json");
84
- expect(writeNames).toContain("parsed.json");
85
- expect(writeNames).toContain("transformed.json");
86
- });
87
-
88
- it("assigns correct stage to reads and writes", () => {
89
- const result = analyzeTask(TASK_CODE);
90
- const rawRead = result.artifacts.reads.find(
91
- (r) => r.fileName === "raw-data.json"
92
- );
93
- expect(rawRead?.stage).toBe("ingestion");
94
-
95
- const transformedWrite = result.artifacts.writes.find(
96
- (w) => w.fileName === "transformed.json"
97
- );
98
- expect(transformedWrite?.stage).toBe("transform");
99
- });
100
-
101
- it("extracts LLM calls with provider and method", () => {
102
- const result = analyzeTask(TASK_CODE);
103
- expect(result.models).toHaveLength(2);
104
-
105
- const openaiCall = result.models.find((m) => m.provider === "openai");
106
- expect(openaiCall).toBeDefined();
107
- expect(openaiCall?.method).toBe("complete");
108
- expect(openaiCall?.stage).toBe("ingestion");
109
-
110
- const anthropicCall = result.models.find((m) => m.provider === "anthropic");
111
- expect(anthropicCall).toBeDefined();
112
- expect(anthropicCall?.method).toBe("chat");
113
- expect(anthropicCall?.stage).toBe("transform");
114
- });
115
-
116
- it("has no unresolved reads or writes for static filenames", () => {
117
- const result = analyzeTask(TASK_CODE);
118
- expect(result.artifacts.unresolvedReads).toHaveLength(0);
119
- expect(result.artifacts.unresolvedWrites).toHaveLength(0);
120
- });
121
-
122
- it("collects unresolved reads for dynamic filenames", () => {
123
- const code = `
124
- export function stage({ io }) {
125
- const name = getFileName();
126
- io.readArtifact(name);
127
- }
128
- `.trim();
129
- const result = analyzeTask(code);
130
- expect(result.artifacts.unresolvedReads).toHaveLength(1);
131
- expect(result.artifacts.unresolvedReads[0]?.stage).toBe("stage");
132
- });
133
-
134
- it("propagates parse errors", () => {
135
- expect(() => analyzeTask("export function {")).toThrow(Error);
136
- try {
137
- analyzeTask("export function {");
138
- } catch (err) {
139
- expect(err).toBeInstanceOf(Error);
140
- expect((err as Error).message).toMatch(/line \d+, column \d+/);
141
- }
142
- });
143
- });
@@ -1,41 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { parseTaskSource } from "../parser.js";
3
-
4
- describe("parseTaskSource", () => {
5
- it("parses valid ESM code and returns a File node with a body", () => {
6
- const result = parseTaskSource("export function foo() {}");
7
- expect(result.type).toBe("File");
8
- expect(result.program.body.length).toBeGreaterThan(0);
9
- });
10
-
11
- it("parses valid JSX code without error", () => {
12
- const result = parseTaskSource(
13
- "export function Comp() { return <div /> }",
14
- );
15
- expect(result.type).toBe("File");
16
- });
17
-
18
- it("throws an Error with line/column info and cause on invalid code", () => {
19
- expect(() => parseTaskSource("export function {")).toThrow(Error);
20
- try {
21
- parseTaskSource("export function {");
22
- } catch (err) {
23
- expect(err).toBeInstanceOf(Error);
24
- const message = (err as Error).message;
25
- expect(message).toMatch(/line \d+, column \d+/);
26
- expect((err as Error).cause).toBeDefined();
27
- }
28
- });
29
- });
30
-
31
- describe("Babel import interop smoke test", () => {
32
- it("@babel/traverse default import is a function", async () => {
33
- const { default: traverse } = await import("@babel/traverse");
34
- expect(typeof traverse).toBe("function");
35
- });
36
-
37
- it("@babel/generator default import is a function", async () => {
38
- const { default: generate } = await import("@babel/generator");
39
- expect(typeof generate).toBe("function");
40
- });
41
- });
@@ -1,82 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { parse } from "@babel/parser";
3
- import traverse from "@babel/traverse";
4
- import type { NodePath } from "@babel/traverse";
5
- import type { CallExpression } from "@babel/types";
6
- import { isInsideTryCatch, getStageName } from "../utils/ast.js";
7
-
8
- function findReadArtifactPath(code: string): NodePath<CallExpression> | null {
9
- const ast = parse(code, { sourceType: "module", plugins: ["jsx"] });
10
- let captured: NodePath<CallExpression> | null = null;
11
-
12
- traverse(ast, {
13
- CallExpression(path) {
14
- const callee = path.node.callee;
15
- if (
16
- callee.type === "MemberExpression" &&
17
- callee.property.type === "Identifier" &&
18
- callee.property.name === "readArtifact"
19
- ) {
20
- captured = path;
21
- }
22
- },
23
- });
24
-
25
- return captured;
26
- }
27
-
28
- describe("isInsideTryCatch", () => {
29
- it("returns true when call is inside a try block", () => {
30
- const code = `
31
- export async function myStage() {
32
- try {
33
- io.readArtifact("x")
34
- } catch(e) {}
35
- }
36
- `;
37
- const path = findReadArtifactPath(code);
38
- expect(path).not.toBeNull();
39
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
40
- expect(isInsideTryCatch(path!)).toBe(true);
41
- });
42
-
43
- it("returns false when call is outside a try block", () => {
44
- const code = `
45
- export async function myStage() {
46
- io.readArtifact("x")
47
- }
48
- `;
49
- const path = findReadArtifactPath(code);
50
- expect(path).not.toBeNull();
51
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
52
- expect(isInsideTryCatch(path!)).toBe(false);
53
- });
54
- });
55
-
56
- describe("getStageName", () => {
57
- it("returns the stage name for a call inside an exported function", () => {
58
- const code = `
59
- export async function myStage() {
60
- try {
61
- io.readArtifact("x")
62
- } catch(e) {}
63
- }
64
- `;
65
- const path = findReadArtifactPath(code);
66
- expect(path).not.toBeNull();
67
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
68
- expect(getStageName(path!)).toBe("myStage");
69
- });
70
-
71
- it("returns null when call is not inside an exported function", () => {
72
- const code = `
73
- async function notExported() {
74
- io.readArtifact("x")
75
- }
76
- `;
77
- const path = findReadArtifactPath(code);
78
- expect(path).not.toBeNull();
79
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
80
- expect(getStageName(path!)).toBeNull();
81
- });
82
- });
@@ -1,75 +0,0 @@
1
- // ── src/task-analysis/enrichers/analysis-writer.ts ──
2
- // Persists TaskAnalysis to disk as JSON.
3
-
4
- import { mkdir } from "node:fs/promises";
5
- import path from "node:path";
6
- import type { TaskAnalysis } from "../types.ts";
7
-
8
- function validate(analysisData: TaskAnalysis): void {
9
- const { taskFilePath, stages, models, artifacts } = analysisData;
10
-
11
- if (taskFilePath === null || taskFilePath === undefined || taskFilePath === "") {
12
- throw new Error(
13
- `Invalid taskFilePath: expected a non-null, non-empty string, got ${taskFilePath === "" ? "empty string" : String(taskFilePath)}`,
14
- );
15
- }
16
-
17
- if (!Array.isArray(stages)) {
18
- throw new Error(
19
- `Invalid stages: expected an array, got ${typeof stages}`,
20
- );
21
- }
22
-
23
- if (!Array.isArray(models)) {
24
- throw new Error(
25
- `Invalid models: expected an array, got ${typeof models}`,
26
- );
27
- }
28
-
29
- if (
30
- typeof artifacts !== "object" ||
31
- artifacts === null ||
32
- !Array.isArray(artifacts.reads) ||
33
- !Array.isArray(artifacts.writes)
34
- ) {
35
- throw new Error(
36
- `Invalid artifacts: expected an object with reads and writes arrays`,
37
- );
38
- }
39
-
40
- if (
41
- "unresolvedReads" in artifacts &&
42
- !Array.isArray(artifacts.unresolvedReads)
43
- ) {
44
- throw new Error(
45
- `Invalid artifacts.unresolvedReads: expected an array, got ${typeof artifacts.unresolvedReads}`,
46
- );
47
- }
48
-
49
- if (
50
- "unresolvedWrites" in artifacts &&
51
- !Array.isArray(artifacts.unresolvedWrites)
52
- ) {
53
- throw new Error(
54
- `Invalid artifacts.unresolvedWrites: expected an array, got ${typeof artifacts.unresolvedWrites}`,
55
- );
56
- }
57
- }
58
-
59
- export async function writeAnalysisFile(
60
- pipelinePath: string,
61
- taskName: string,
62
- analysisData: TaskAnalysis,
63
- ): Promise<void> {
64
- validate(analysisData);
65
-
66
- const analysisDir = path.join(pipelinePath, "analysis");
67
- await mkdir(analysisDir, { recursive: true });
68
-
69
- const data = { ...analysisData, analyzedAt: new Date().toISOString() };
70
-
71
- await Bun.write(
72
- path.join(analysisDir, `${taskName}.analysis.json`),
73
- JSON.stringify(data, null, 2),
74
- );
75
- }
@@ -1,89 +0,0 @@
1
- // ── src/task-analysis/enrichers/artifact-resolver.ts ──
2
- // LLM-powered resolution of dynamic artifact references.
3
-
4
- import { chat } from "../../llm/index.ts";
5
- import type { UnresolvedArtifactDescriptor, ArtifactResolution } from "../types.ts";
6
-
7
- const FALLBACK: ArtifactResolution = {
8
- resolvedFileName: null,
9
- confidence: 0,
10
- reasoning: "Failed to analyze artifact reference",
11
- };
12
-
13
- function clampConfidence(value: unknown): number {
14
- if (typeof value !== "number" || !isFinite(value)) return 0;
15
- if (value < 0) return 0;
16
- if (value > 1) return 1;
17
- return value;
18
- }
19
-
20
- export async function resolveArtifactReference(
21
- taskCode: string,
22
- unresolvedArtifact: UnresolvedArtifactDescriptor,
23
- availableArtifacts: string[],
24
- ): Promise<ArtifactResolution> {
25
- try {
26
- const messages = [
27
- {
28
- role: "system" as const,
29
- content:
30
- "You are a code analysis expert. Given pipeline task source code, a dynamic artifact expression, and a list of available artifact filenames, determine which artifact filename the expression resolves to. Respond with a JSON object containing: resolvedFileName (string or null), confidence (number between 0 and 1), and reasoning (string).",
31
- },
32
- {
33
- role: "user" as const,
34
- content: `Analyze the following pipeline task source code and determine which artifact filename the dynamic expression resolves to.
35
-
36
- Task source code:
37
- \`\`\`
38
- ${taskCode}
39
- \`\`\`
40
-
41
- Dynamic expression: ${unresolvedArtifact.expression}
42
- Code context: ${unresolvedArtifact.codeContext}
43
- Stage: ${unresolvedArtifact.stage}
44
-
45
- Available artifact filenames:
46
- ${availableArtifacts.map((f) => `- ${f}`).join("\n")}
47
-
48
- Respond with a JSON object with these fields:
49
- - resolvedFileName: the matching filename from the available list, or null if you cannot determine it
50
- - confidence: a number between 0 and 1 indicating your confidence
51
- - reasoning: a string explaining your reasoning`,
52
- },
53
- ];
54
-
55
- const response = await chat({
56
- provider: "deepseek",
57
- model: "deepseek-chat",
58
- messages,
59
- temperature: 0,
60
- responseFormat: { type: "json_object" },
61
- });
62
-
63
- const { content } = response;
64
-
65
- if (typeof content !== "object" || content === null) {
66
- throw new Error(
67
- `Unexpected gateway response: content must be a non-null object, got ${typeof content}`,
68
- );
69
- }
70
-
71
- const { resolvedFileName, confidence, reasoning } = content as Record<string, unknown>;
72
-
73
- let sanitizedFileName = typeof resolvedFileName === "string" ? resolvedFileName : null;
74
- let sanitizedConfidence = clampConfidence(confidence);
75
-
76
- if (sanitizedFileName !== null && !availableArtifacts.includes(sanitizedFileName)) {
77
- sanitizedFileName = null;
78
- sanitizedConfidence = 0;
79
- }
80
-
81
- return {
82
- resolvedFileName: sanitizedFileName,
83
- confidence: sanitizedConfidence,
84
- reasoning: typeof reasoning === "string" ? reasoning : "",
85
- };
86
- } catch {
87
- return FALLBACK;
88
- }
89
- }
@@ -1,143 +0,0 @@
1
- import traverse from "@babel/traverse";
2
- import generate from "@babel/generator";
3
- import type { File as BabelFile } from "@babel/types";
4
- import type { NodePath } from "@babel/traverse";
5
- import type {
6
- ArtifactRead,
7
- ArtifactWrite,
8
- UnresolvedRead,
9
- UnresolvedWrite,
10
- } from "../types.ts";
11
- import { getStageName, isInsideTryCatch } from "../utils/ast.ts";
12
-
13
- export function extractCodeContext(
14
- path: NodePath,
15
- sourceCode: string
16
- ): string {
17
- if (!sourceCode || !path.node.loc) return "";
18
- const lines = sourceCode.split("\n");
19
- const line = path.node.loc.start.line - 1; // convert to 0-based
20
- const start = Math.max(0, line - 2);
21
- const end = Math.min(lines.length, line + 3);
22
- return lines.slice(start, end).join("\n");
23
- }
24
-
25
- function resolveFileName(
26
- argNode: import("@babel/types").Expression | import("@babel/types").SpreadElement
27
- ): string | null {
28
- if (argNode.type === "StringLiteral") {
29
- return argNode.value;
30
- }
31
- if (argNode.type === "TemplateLiteral") {
32
- if (argNode.expressions.length === 0) {
33
- return argNode.quasis.map((q) => q.value.cooked ?? "").join("");
34
- }
35
- // Template with expressions: generate source text, strip backticks
36
- const src = generate(argNode).code;
37
- return src.replace(/^`|`$/g, "");
38
- }
39
- return null;
40
- }
41
-
42
- export function extractArtifactReads(
43
- ast: BabelFile,
44
- sourceCode?: string
45
- ): { reads: ArtifactRead[]; unresolvedReads: UnresolvedRead[] } {
46
- const reads: ArtifactRead[] = [];
47
- const unresolvedReads: UnresolvedRead[] = [];
48
-
49
- traverse(ast, {
50
- CallExpression(path) {
51
- const { callee } = path.node;
52
- if (
53
- callee.type !== "MemberExpression" ||
54
- callee.object.type !== "Identifier" ||
55
- callee.object.name !== "io" ||
56
- callee.property.type !== "Identifier" ||
57
- callee.property.name !== "readArtifact"
58
- )
59
- return;
60
-
61
- const stage = getStageName(path);
62
- if (stage === null) {
63
- const loc = path.node.loc;
64
- throw new Error(
65
- `io.readArtifact call at line ${loc?.start.line ?? "unknown"}, column ${loc?.start.column ?? "unknown"} is not inside an exported function`
66
- );
67
- }
68
-
69
- const arg = path.node.arguments[0];
70
- if (!arg || arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") return;
71
-
72
- const required = !isInsideTryCatch(path);
73
- const fileName = resolveFileName(arg);
74
-
75
- if (fileName !== null) {
76
- reads.push({ fileName, stage, required });
77
- } else {
78
- const expression = generate(arg).code;
79
- const codeContext = sourceCode
80
- ? extractCodeContext(path, sourceCode)
81
- : "";
82
- const location = {
83
- line: arg.loc?.start.line ?? 0,
84
- column: arg.loc?.start.column ?? 0,
85
- };
86
- unresolvedReads.push({ expression, codeContext, stage, required, location });
87
- }
88
- },
89
- });
90
-
91
- return { reads, unresolvedReads };
92
- }
93
-
94
- export function extractArtifactWrites(
95
- ast: BabelFile,
96
- sourceCode?: string
97
- ): { writes: ArtifactWrite[]; unresolvedWrites: UnresolvedWrite[] } {
98
- const writes: ArtifactWrite[] = [];
99
- const unresolvedWrites: UnresolvedWrite[] = [];
100
-
101
- traverse(ast, {
102
- CallExpression(path) {
103
- const { callee } = path.node;
104
- if (
105
- callee.type !== "MemberExpression" ||
106
- callee.object.type !== "Identifier" ||
107
- callee.object.name !== "io" ||
108
- callee.property.type !== "Identifier" ||
109
- callee.property.name !== "writeArtifact"
110
- )
111
- return;
112
-
113
- const stage = getStageName(path);
114
- if (stage === null) {
115
- const loc = path.node.loc;
116
- throw new Error(
117
- `io.writeArtifact call at line ${loc?.start.line ?? "unknown"}, column ${loc?.start.column ?? "unknown"} is not inside an exported function`
118
- );
119
- }
120
-
121
- const arg = path.node.arguments[0];
122
- if (!arg || arg.type === "SpreadElement" || arg.type === "ArgumentPlaceholder") return;
123
-
124
- const fileName = resolveFileName(arg);
125
-
126
- if (fileName !== null) {
127
- writes.push({ fileName, stage });
128
- } else {
129
- const expression = generate(arg).code;
130
- const codeContext = sourceCode
131
- ? extractCodeContext(path, sourceCode)
132
- : "";
133
- const location = {
134
- line: arg.loc?.start.line ?? 0,
135
- column: arg.loc?.start.column ?? 0,
136
- };
137
- unresolvedWrites.push({ expression, codeContext, stage, location });
138
- }
139
- },
140
- });
141
-
142
- return { writes, unresolvedWrites };
143
- }
@@ -1,117 +0,0 @@
1
- import traverse from "@babel/traverse";
2
- import type { NodePath } from "@babel/traverse";
3
- import type { File as BabelFile } from "@babel/types";
4
- import type { ModelCall } from "../types.ts";
5
- import { getStageName } from "../utils/ast.ts";
6
-
7
- function isLLMDirectAccess(
8
- path: NodePath
9
- ): { provider: string; method: string } | null {
10
- const { node } = path;
11
- if (node.type !== "CallExpression") return null;
12
- const { callee } = node;
13
- if (callee.type !== "MemberExpression") return null;
14
- if (callee.property.type !== "Identifier") return null;
15
-
16
- const method = callee.property.name;
17
- const obj = callee.object;
18
- if (obj.type !== "MemberExpression") return null;
19
- if (obj.object.type !== "Identifier") return null;
20
- if (obj.object.name !== "llm") return null;
21
- if (obj.property.type !== "Identifier") return null;
22
-
23
- return { provider: obj.property.name, method };
24
- }
25
-
26
- function isLLMDestructuredAccess(
27
- path: NodePath
28
- ): { provider: string; method: string } | null {
29
- const { node } = path;
30
- if (node.type !== "CallExpression") return null;
31
- const { callee } = node;
32
- if (callee.type !== "MemberExpression") return null;
33
- if (callee.object.type !== "Identifier") return null;
34
- if (callee.property.type !== "Identifier") return null;
35
-
36
- const identName = callee.object.name;
37
- const method = callee.property.name;
38
-
39
- const binding = path.scope.getBinding(identName);
40
- if (!binding) return null;
41
-
42
- const bindingPath = binding.path;
43
-
44
- // Pattern 2: const { provider } = llm;
45
- if (bindingPath.node.type === "VariableDeclarator") {
46
- const { id, init } = bindingPath.node;
47
- if (
48
- init?.type === "Identifier" &&
49
- init.name === "llm" &&
50
- id.type === "ObjectPattern"
51
- ) {
52
- return { provider: identName, method };
53
- }
54
- return null;
55
- }
56
-
57
- // Pattern 3: ({ llm: { provider } }) => ...
58
- // Babel registers the binding for the destructured identifier at the outer
59
- // ObjectPattern level (the whole param pattern). We check that the outer
60
- // ObjectPattern has a property keyed "llm" whose value is an ObjectPattern
61
- // containing the identifier, and that the parent is a function node.
62
- if (bindingPath.node.type === "ObjectPattern") {
63
- const parentNode = bindingPath.parentPath?.node;
64
- const isParam =
65
- parentNode?.type === "FunctionDeclaration" ||
66
- parentNode?.type === "FunctionExpression" ||
67
- parentNode?.type === "ArrowFunctionExpression";
68
- if (!isParam) return null;
69
-
70
- const outerPattern = bindingPath.node;
71
- for (const prop of outerPattern.properties) {
72
- if (
73
- prop.type === "ObjectProperty" &&
74
- prop.key.type === "Identifier" &&
75
- prop.key.name === "llm" &&
76
- prop.value.type === "ObjectPattern"
77
- ) {
78
- const innerPattern = prop.value;
79
- const hasIdent = innerPattern.properties.some(
80
- (p) =>
81
- p.type === "ObjectProperty" &&
82
- p.key.type === "Identifier" &&
83
- p.key.name === identName
84
- );
85
- if (hasIdent) return { provider: identName, method };
86
- }
87
- }
88
- return null;
89
- }
90
-
91
- return null;
92
- }
93
-
94
- export function extractLLMCalls(ast: BabelFile): ModelCall[] {
95
- const calls: ModelCall[] = [];
96
-
97
- traverse(ast, {
98
- CallExpression(path) {
99
- const direct = isLLMDirectAccess(path);
100
- const destructured = direct === null ? isLLMDestructuredAccess(path) : null;
101
- const match = direct ?? destructured;
102
- if (!match) return;
103
-
104
- const stage = getStageName(path);
105
- if (stage === null) {
106
- const loc = path.node.loc;
107
- throw new Error(
108
- `LLM call at line ${loc?.start.line ?? "unknown"}, column ${loc?.start.column ?? "unknown"} is not inside an exported function`
109
- );
110
- }
111
-
112
- calls.push({ provider: match.provider, method: match.method, stage });
113
- },
114
- });
115
-
116
- return calls;
117
- }