@ryanfw/prompt-orchestration-pipeline 1.3.2 → 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.
- package/docs/http-api.md +80 -4
- package/package.json +1 -4
- package/src/api/__tests__/index.test.ts +443 -32
- package/src/api/index.ts +108 -127
- package/src/cli/__tests__/analyze-task.test.ts +40 -7
- package/src/cli/__tests__/index.test.ts +337 -17
- package/src/cli/__tests__/types.test.ts +0 -18
- package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
- package/src/cli/analyze-task.ts +3 -14
- package/src/cli/index.ts +73 -61
- package/src/cli/types.ts +0 -13
- package/src/cli/update-pipeline-json.ts +3 -14
- package/src/config/__tests__/paths.test.ts +46 -1
- package/src/config/__tests__/pipeline-registry.test.ts +767 -0
- package/src/config/__tests__/sse-events.test.ts +470 -0
- package/src/config/__tests__/statuses.test.ts +41 -0
- package/src/config/paths.ts +11 -2
- package/src/config/pipeline-registry.ts +258 -0
- package/src/config/sse-events.ts +122 -0
- package/src/config/statuses.ts +20 -1
- package/src/core/__tests__/agent-step.test.ts +115 -0
- package/src/core/__tests__/config.test.ts +545 -105
- package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
- package/src/core/__tests__/job-concurrency.test.ts +260 -0
- package/src/core/__tests__/job-submission.test.ts +396 -0
- package/src/core/__tests__/job-view.test.ts +1341 -0
- package/src/core/__tests__/json-file.test.ts +111 -0
- package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
- package/src/core/__tests__/logger.test.ts +22 -0
- package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
- package/src/core/__tests__/orchestrator.test.ts +615 -2
- package/src/core/__tests__/pipeline-runner.test.ts +946 -9
- package/src/core/__tests__/redact.test.ts +153 -0
- package/src/core/__tests__/runner-liveness.test.ts +910 -0
- package/src/core/__tests__/seed-naming.test.ts +57 -0
- package/src/core/__tests__/single-derivation.test.ts +159 -0
- package/src/core/__tests__/task-runner.test.ts +594 -3
- package/src/core/__tests__/task-telemetry.test.ts +241 -0
- package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
- package/src/core/agent-step.ts +4 -1
- package/src/core/agent-types.ts +5 -4
- package/src/core/config.ts +134 -222
- package/src/core/control.ts +3 -0
- package/src/core/job-concurrency.ts +56 -20
- package/src/core/job-submission.ts +133 -0
- package/src/core/job-view.ts +473 -0
- package/src/core/json-file.ts +45 -0
- package/src/core/lifecycle-policy.ts +23 -8
- package/src/core/logger.ts +0 -29
- package/src/core/orchestrator.ts +200 -74
- package/src/core/pipeline-runner.ts +85 -53
- package/src/core/redact.ts +40 -0
- package/src/core/runner-liveness.ts +280 -0
- package/src/core/seed-naming.ts +9 -0
- package/src/core/status-writer.ts +27 -33
- package/src/core/task-runner.ts +356 -319
- package/src/core/task-telemetry.ts +107 -0
- package/src/harness/__tests__/subprocess.test.ts +112 -1
- package/src/harness/subprocess.ts +55 -16
- package/src/llm/__tests__/index.test.ts +684 -33
- package/src/llm/index.ts +310 -67
- package/src/providers/__tests__/alibaba.test.ts +67 -6
- package/src/providers/__tests__/anthropic.test.ts +35 -14
- package/src/providers/__tests__/base.test.ts +62 -0
- package/src/providers/__tests__/claude-code.test.ts +99 -14
- package/src/providers/__tests__/deepseek.test.ts +16 -6
- package/src/providers/__tests__/gemini.test.ts +47 -25
- package/src/providers/__tests__/moonshot.test.ts +27 -0
- package/src/providers/__tests__/openai.test.ts +262 -74
- package/src/providers/__tests__/opencode.test.ts +77 -0
- package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
- package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
- package/src/providers/__tests__/types.test.ts +85 -0
- package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
- package/src/providers/__tests__/zhipu.test.ts +27 -14
- package/src/providers/alibaba.ts +20 -39
- package/src/providers/anthropic.ts +23 -11
- package/src/providers/base.ts +19 -0
- package/src/providers/claude-code.ts +27 -18
- package/src/providers/deepseek.ts +9 -28
- package/src/providers/gemini.ts +20 -58
- package/src/providers/moonshot.ts +15 -11
- package/src/providers/openai.ts +79 -61
- package/src/providers/opencode.ts +16 -33
- package/src/providers/stream-accumulator.ts +27 -21
- package/src/providers/types.ts +29 -4
- package/src/providers/zhipu.ts +15 -44
- package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
- package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
- package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
- package/src/task-analysis/__tests__/repository.test.ts +65 -0
- package/src/task-analysis/__tests__/types.test.ts +63 -130
- package/src/task-analysis/analyzer.ts +91 -0
- package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
- package/src/task-analysis/index.ts +2 -36
- package/src/task-analysis/repository.ts +45 -0
- package/src/task-analysis/types.ts +42 -58
- package/src/ui/client/__tests__/api.test.ts +143 -1
- package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
- package/src/ui/client/__tests__/job-adapter.test.ts +203 -2
- package/src/ui/client/__tests__/load-state.test.ts +70 -0
- package/src/ui/client/__tests__/types.test.ts +66 -3
- package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
- package/src/ui/client/__tests__/useJobList.test.ts +198 -23
- package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
- package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
- package/src/ui/client/adapters/job-adapter.ts +41 -16
- package/src/ui/client/api.ts +38 -15
- package/src/ui/client/bootstrap.ts +19 -14
- package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
- package/src/ui/client/hooks/useJobList.ts +26 -31
- package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
- package/src/ui/client/load-state.ts +20 -0
- package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
- package/src/ui/client/reducers/job-events.ts +137 -0
- package/src/ui/client/types.ts +16 -20
- package/src/ui/components/DAGGrid.tsx +6 -1
- package/src/ui/components/JobDetail.tsx +12 -4
- package/src/ui/components/JobTable.tsx +41 -13
- package/src/ui/components/StageTimeline.tsx +8 -20
- package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
- package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
- package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
- package/src/ui/components/__tests__/JobTable.test.tsx +137 -1
- package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
- package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
- package/src/ui/components/types.ts +35 -15
- package/src/ui/dist/assets/{index--RH3sAt3.js → index-DN3-zvtP.js} +4324 -263
- package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
- package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/pages/PipelineDetail.tsx +60 -4
- package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
- package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
- package/src/ui/pages/__tests__/pages.test.tsx +236 -42
- package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
- package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
- package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
- package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
- package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
- package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
- package/src/ui/server/__tests__/index.test.ts +63 -0
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +512 -2
- package/src/ui/server/__tests__/job-endpoints.test.ts +158 -2
- package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
- package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
- package/src/ui/server/__tests__/router-threading.test.ts +148 -0
- package/src/ui/server/__tests__/router.test.ts +104 -0
- package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
- package/src/ui/server/__tests__/sse-enhancer.test.ts +70 -0
- package/src/ui/server/config-bridge-node.ts +8 -26
- package/src/ui/server/config-bridge.ts +3 -4
- package/src/ui/server/embedded-assets-imports.d.ts +44 -0
- package/src/ui/server/embedded-assets.ts +13 -2
- package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
- package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
- package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
- package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
- package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
- package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
- package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
- package/src/ui/server/endpoints/file-endpoints.ts +15 -10
- package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
- package/src/ui/server/endpoints/job-control-endpoints.ts +129 -141
- package/src/ui/server/endpoints/job-endpoints.ts +7 -0
- package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
- package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
- package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
- package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
- package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
- package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
- package/src/ui/server/index.ts +19 -14
- package/src/ui/server/job-reader.ts +14 -2
- package/src/ui/server/router.ts +33 -10
- package/src/ui/server/sse-broadcast.ts +14 -3
- package/src/ui/server/sse-enhancer.ts +12 -2
- package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
- package/src/ui/state/__tests__/snapshot.test.ts +120 -14
- package/src/ui/state/__tests__/types.test.ts +104 -5
- package/src/ui/state/snapshot.ts +2 -2
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +85 -2
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +250 -22
- package/src/ui/state/transformers/list-transformer.ts +13 -3
- package/src/ui/state/transformers/status-transformer.ts +36 -170
- package/src/ui/state/types.ts +15 -48
- package/src/utils/__tests__/path-containment.test.ts +160 -0
- package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
- package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
- package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
- package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
- package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
- package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
- package/src/task-analysis/__tests__/index.test.ts +0 -143
- package/src/task-analysis/__tests__/parser.test.ts +0 -41
- package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
- package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
- package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
- package/src/task-analysis/extractors/artifacts.ts +0 -143
- package/src/task-analysis/extractors/llm-calls.ts +0 -117
- package/src/task-analysis/extractors/stages.ts +0 -45
- package/src/task-analysis/parser.ts +0 -20
- package/src/task-analysis/utils/ast.ts +0 -45
- package/src/ui/dist/assets/index--RH3sAt3.js.map +0 -1
- package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
- package/src/ui/embedded-assets.js +0 -12
- package/src/ui/server/__tests__/path-containment.test.ts +0 -54
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
2
|
-
import type { ChatResponse } from "../../providers/types.ts";
|
|
3
|
-
|
|
4
|
-
mock.module("../../llm/index.ts", () => ({
|
|
5
|
-
chat: mock(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
import { chat } from "../../llm/index.ts";
|
|
9
|
-
import { resolveArtifactReference } from "../enrichers/artifact-resolver.ts";
|
|
10
|
-
|
|
11
|
-
const mockedChat = chat as ReturnType<typeof mock>;
|
|
12
|
-
|
|
13
|
-
const TASK_CODE = `
|
|
14
|
-
export async function transform({ io }) {
|
|
15
|
-
const input = await io.readArtifact(getInputFile());
|
|
16
|
-
await io.writeArtifact("output.json", input);
|
|
17
|
-
}
|
|
18
|
-
`.trim();
|
|
19
|
-
|
|
20
|
-
const UNRESOLVED = {
|
|
21
|
-
expression: "getInputFile()",
|
|
22
|
-
codeContext: "io.readArtifact(getInputFile())",
|
|
23
|
-
stage: "transform",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const AVAILABLE = ["data.json", "config.json", "output.json"];
|
|
27
|
-
|
|
28
|
-
function makeResponse(content: Record<string, unknown>): Promise<ChatResponse> {
|
|
29
|
-
return Promise.resolve({
|
|
30
|
-
content,
|
|
31
|
-
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
mockedChat.mockClear();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("resolveArtifactReference", () => {
|
|
40
|
-
it("returns result as-is when resolvedFileName is in availableArtifacts", async () => {
|
|
41
|
-
mockedChat.mockReturnValue(
|
|
42
|
-
makeResponse({ resolvedFileName: "data.json", confidence: 0.9, reasoning: "Matches input pattern" }),
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const result = await resolveArtifactReference(TASK_CODE, UNRESOLVED, AVAILABLE);
|
|
46
|
-
|
|
47
|
-
expect(result.resolvedFileName).toBe("data.json");
|
|
48
|
-
expect(result.confidence).toBe(0.9);
|
|
49
|
-
expect(result.reasoning).toBe("Matches input pattern");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("returns null resolvedFileName and 0 confidence when filename not in availableArtifacts", async () => {
|
|
53
|
-
mockedChat.mockReturnValue(
|
|
54
|
-
makeResponse({ resolvedFileName: "hallucinated.json", confidence: 0.8, reasoning: "Guessed" }),
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const result = await resolveArtifactReference(TASK_CODE, UNRESOLVED, AVAILABLE);
|
|
58
|
-
|
|
59
|
-
expect(result.resolvedFileName).toBeNull();
|
|
60
|
-
expect(result.confidence).toBe(0);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("clamps confidence to 1 when value exceeds 1", async () => {
|
|
64
|
-
mockedChat.mockReturnValue(
|
|
65
|
-
makeResponse({ resolvedFileName: "data.json", confidence: 7, reasoning: "Very confident" }),
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const result = await resolveArtifactReference(TASK_CODE, UNRESOLVED, AVAILABLE);
|
|
69
|
-
|
|
70
|
-
expect(result.resolvedFileName).toBe("data.json");
|
|
71
|
-
expect(result.confidence).toBe(1);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("forces confidence to 0 when value is NaN", async () => {
|
|
75
|
-
mockedChat.mockReturnValue(
|
|
76
|
-
makeResponse({ resolvedFileName: "data.json", confidence: NaN, reasoning: "Uncertain" }),
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const result = await resolveArtifactReference(TASK_CODE, UNRESOLVED, AVAILABLE);
|
|
80
|
-
|
|
81
|
-
expect(result.resolvedFileName).toBe("data.json");
|
|
82
|
-
expect(result.confidence).toBe(0);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("clamps confidence to 0 when value is negative", async () => {
|
|
86
|
-
mockedChat.mockReturnValue(
|
|
87
|
-
makeResponse({ resolvedFileName: "data.json", confidence: -1, reasoning: "Negative" }),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const result = await resolveArtifactReference(TASK_CODE, UNRESOLVED, AVAILABLE);
|
|
91
|
-
|
|
92
|
-
expect(result.resolvedFileName).toBe("data.json");
|
|
93
|
-
expect(result.confidence).toBe(0);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("returns fallback when content is a raw string (non-object)", async () => {
|
|
97
|
-
mockedChat.mockReturnValue(
|
|
98
|
-
Promise.resolve({
|
|
99
|
-
content: '{"resolvedFileName":"x.json"}' as unknown as Record<string, unknown>,
|
|
100
|
-
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
101
|
-
}),
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const result = await resolveArtifactReference(TASK_CODE, UNRESOLVED, AVAILABLE);
|
|
105
|
-
|
|
106
|
-
expect(result).toEqual({
|
|
107
|
-
resolvedFileName: null,
|
|
108
|
-
confidence: 0,
|
|
109
|
-
reasoning: "Failed to analyze artifact reference",
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("returns fallback when chat throws", async () => {
|
|
114
|
-
mockedChat.mockRejectedValue(new Error("Network error"));
|
|
115
|
-
|
|
116
|
-
const result = await resolveArtifactReference(TASK_CODE, UNRESOLVED, AVAILABLE);
|
|
117
|
-
|
|
118
|
-
expect(result).toEqual({
|
|
119
|
-
resolvedFileName: null,
|
|
120
|
-
confidence: 0,
|
|
121
|
-
reasoning: "Failed to analyze artifact reference",
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseTaskSource } from "../parser.js";
|
|
3
|
-
import {
|
|
4
|
-
extractArtifactReads,
|
|
5
|
-
extractArtifactWrites,
|
|
6
|
-
extractCodeContext,
|
|
7
|
-
} from "../extractors/artifacts.js";
|
|
8
|
-
import traverse from "@babel/traverse";
|
|
9
|
-
import type { NodePath } from "@babel/traverse";
|
|
10
|
-
|
|
11
|
-
describe("extractArtifactReads", () => {
|
|
12
|
-
it("returns one read with correct fields for a simple string literal", () => {
|
|
13
|
-
const code = `export function s() { io.readArtifact("data.json") }`;
|
|
14
|
-
const ast = parseTaskSource(code);
|
|
15
|
-
const { reads, unresolvedReads } = extractArtifactReads(ast, code);
|
|
16
|
-
|
|
17
|
-
expect(reads).toHaveLength(1);
|
|
18
|
-
expect(unresolvedReads).toHaveLength(0);
|
|
19
|
-
expect(reads[0]).toMatchObject({
|
|
20
|
-
fileName: "data.json",
|
|
21
|
-
stage: "s",
|
|
22
|
-
required: true,
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("sets required=false when inside a try/catch", () => {
|
|
27
|
-
const code = `
|
|
28
|
-
export function s() {
|
|
29
|
-
try {
|
|
30
|
-
io.readArtifact("data.json")
|
|
31
|
-
} catch(e) {}
|
|
32
|
-
}
|
|
33
|
-
`.trim();
|
|
34
|
-
const ast = parseTaskSource(code);
|
|
35
|
-
const { reads } = extractArtifactReads(ast, code);
|
|
36
|
-
|
|
37
|
-
expect(reads).toHaveLength(1);
|
|
38
|
-
expect(reads[0]?.required).toBe(false);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("resolves template literal with expression into reads (not unresolvedReads)", () => {
|
|
42
|
-
const code = `export function s() { io.readArtifact(\`file-\${name}.json\`) }`;
|
|
43
|
-
const ast = parseTaskSource(code);
|
|
44
|
-
const { reads, unresolvedReads } = extractArtifactReads(ast, code);
|
|
45
|
-
|
|
46
|
-
expect(unresolvedReads).toHaveLength(0);
|
|
47
|
-
expect(reads).toHaveLength(1);
|
|
48
|
-
expect(reads[0]?.fileName).toContain("${name}");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("puts dynamic identifiers in unresolvedReads with expression and codeContext", () => {
|
|
52
|
-
const code = `export function s() { io.readArtifact(dynamicVar) }`;
|
|
53
|
-
const ast = parseTaskSource(code);
|
|
54
|
-
const { reads, unresolvedReads } = extractArtifactReads(ast, code);
|
|
55
|
-
|
|
56
|
-
expect(reads).toHaveLength(0);
|
|
57
|
-
expect(unresolvedReads).toHaveLength(1);
|
|
58
|
-
expect(unresolvedReads[0]?.expression).toBe("dynamicVar");
|
|
59
|
-
expect(typeof unresolvedReads[0]?.codeContext).toBe("string");
|
|
60
|
-
expect(unresolvedReads[0]?.codeContext.length).toBeGreaterThan(0);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("throws when io.readArtifact is outside an exported function", () => {
|
|
64
|
-
const code = `function internal() { io.readArtifact("x") }`;
|
|
65
|
-
const ast = parseTaskSource(code);
|
|
66
|
-
expect(() => extractArtifactReads(ast, code)).toThrow();
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe("extractArtifactWrites", () => {
|
|
71
|
-
it("resolves template literal with no expressions into writes", () => {
|
|
72
|
-
const code = "export function s() { io.writeArtifact(`output.json`) }";
|
|
73
|
-
const ast = parseTaskSource(code);
|
|
74
|
-
const { writes, unresolvedWrites } = extractArtifactWrites(ast, code);
|
|
75
|
-
|
|
76
|
-
expect(unresolvedWrites).toHaveLength(0);
|
|
77
|
-
expect(writes).toHaveLength(1);
|
|
78
|
-
expect(writes[0]).toMatchObject({ fileName: "output.json", stage: "s" });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("puts dynamic identifiers in unresolvedWrites with expression and codeContext", () => {
|
|
82
|
-
const code = `export function s() { io.writeArtifact(dynamicVar) }`;
|
|
83
|
-
const ast = parseTaskSource(code);
|
|
84
|
-
const { writes, unresolvedWrites } = extractArtifactWrites(ast, code);
|
|
85
|
-
|
|
86
|
-
expect(writes).toHaveLength(0);
|
|
87
|
-
expect(unresolvedWrites).toHaveLength(1);
|
|
88
|
-
expect(unresolvedWrites[0]?.expression).toBe("dynamicVar");
|
|
89
|
-
expect(typeof unresolvedWrites[0]?.codeContext).toBe("string");
|
|
90
|
-
expect(unresolvedWrites[0]?.codeContext.length).toBeGreaterThan(0);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("throws when io.writeArtifact is outside an exported function", () => {
|
|
94
|
-
const code = `function internal() { io.writeArtifact("x") }`;
|
|
95
|
-
const ast = parseTaskSource(code);
|
|
96
|
-
expect(() => extractArtifactWrites(ast, code)).toThrow();
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe("extractCodeContext", () => {
|
|
101
|
-
it("returns surrounding lines from the source", () => {
|
|
102
|
-
const code = `line1\nline2\nline3\nline4\nline5`;
|
|
103
|
-
const ast = parseTaskSource(
|
|
104
|
-
`export function s() { io.readArtifact("x") }`
|
|
105
|
-
);
|
|
106
|
-
// Find a NodePath to use as anchor
|
|
107
|
-
let capturedPath: NodePath | null = null;
|
|
108
|
-
traverse(ast, {
|
|
109
|
-
CallExpression(p) {
|
|
110
|
-
capturedPath = p;
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
expect(capturedPath).not.toBeNull();
|
|
114
|
-
// The path's loc is line 1 in its own source, but we pass a different sourceCode
|
|
115
|
-
// so we just check it returns a non-empty string without crashing
|
|
116
|
-
const result = extractCodeContext(capturedPath!, code);
|
|
117
|
-
expect(typeof result).toBe("string");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("returns empty string when sourceCode is empty", () => {
|
|
121
|
-
const ast = parseTaskSource(
|
|
122
|
-
`export function s() { io.readArtifact("x") }`
|
|
123
|
-
);
|
|
124
|
-
let capturedPath: NodePath | null = null;
|
|
125
|
-
traverse(ast, {
|
|
126
|
-
CallExpression(p) {
|
|
127
|
-
capturedPath = p;
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
expect(capturedPath).not.toBeNull();
|
|
131
|
-
expect(extractCodeContext(capturedPath!, "")).toBe("");
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseTaskSource } from "../parser.js";
|
|
3
|
-
import { extractLLMCalls } from "../extractors/llm-calls.js";
|
|
4
|
-
|
|
5
|
-
describe("extractLLMCalls", () => {
|
|
6
|
-
it("detects direct llm.provider.method() access", () => {
|
|
7
|
-
const code = `export function s() { llm.deepseek.chat({}) }`;
|
|
8
|
-
const ast = parseTaskSource(code);
|
|
9
|
-
const calls = extractLLMCalls(ast);
|
|
10
|
-
|
|
11
|
-
expect(calls).toHaveLength(1);
|
|
12
|
-
expect(calls[0]).toMatchObject({ provider: "deepseek", method: "chat", stage: "s" });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("detects variable-destructured pattern: const { provider } = llm", () => {
|
|
16
|
-
const code = `export function s() { const { openai } = llm; openai.gpt5({}) }`;
|
|
17
|
-
const ast = parseTaskSource(code);
|
|
18
|
-
const calls = extractLLMCalls(ast);
|
|
19
|
-
|
|
20
|
-
expect(calls).toHaveLength(1);
|
|
21
|
-
expect(calls[0]).toMatchObject({ provider: "openai", method: "gpt5", stage: "s" });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("detects parameter-destructured pattern: ({ llm: { provider } })", () => {
|
|
25
|
-
const code = `export function s({ llm: { anthropic } }) { anthropic.sonnet45({}) }`;
|
|
26
|
-
const ast = parseTaskSource(code);
|
|
27
|
-
const calls = extractLLMCalls(ast);
|
|
28
|
-
|
|
29
|
-
expect(calls).toHaveLength(1);
|
|
30
|
-
expect(calls[0]).toMatchObject({ provider: "anthropic", method: "sonnet45", stage: "s" });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("does not produce a ModelCall for a same-named identifier in a different scope", () => {
|
|
34
|
-
const code = `const deepseek = {}; deepseek.chat({})`;
|
|
35
|
-
const ast = parseTaskSource(code);
|
|
36
|
-
const calls = extractLLMCalls(ast);
|
|
37
|
-
|
|
38
|
-
expect(calls).toHaveLength(0);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("throws when llm.provider.method() is outside an exported function", () => {
|
|
42
|
-
const code = `function internal() { llm.openai.chat({}) }`;
|
|
43
|
-
const ast = parseTaskSource(code);
|
|
44
|
-
expect(() => extractLLMCalls(ast)).toThrow();
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseTaskSource } from "../parser.js";
|
|
3
|
-
import { extractStages } from "../extractors/stages.js";
|
|
4
|
-
|
|
5
|
-
describe("extractStages", () => {
|
|
6
|
-
it("extracts async and non-async function declaration exports with correct order", () => {
|
|
7
|
-
const code = `
|
|
8
|
-
export async function ingestion() {}
|
|
9
|
-
export function parsing() {}
|
|
10
|
-
`.trim();
|
|
11
|
-
const ast = parseTaskSource(code);
|
|
12
|
-
const stages = extractStages(ast);
|
|
13
|
-
|
|
14
|
-
expect(stages).toHaveLength(2);
|
|
15
|
-
|
|
16
|
-
const ingestion = stages.find((s) => s.name === "ingestion");
|
|
17
|
-
const parsing = stages.find((s) => s.name === "parsing");
|
|
18
|
-
|
|
19
|
-
expect(ingestion).toBeDefined();
|
|
20
|
-
expect(ingestion?.isAsync).toBe(true);
|
|
21
|
-
|
|
22
|
-
expect(parsing).toBeDefined();
|
|
23
|
-
expect(parsing?.isAsync).toBe(false);
|
|
24
|
-
|
|
25
|
-
// ascending order: ingestion comes before parsing
|
|
26
|
-
const ingestionIdx = stages.indexOf(ingestion!);
|
|
27
|
-
const parsingIdx = stages.indexOf(parsing!);
|
|
28
|
-
expect(ingestionIdx).toBeLessThan(parsingIdx);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("detects async arrow function export as an async stage", () => {
|
|
32
|
-
const code = `export const refine = async () => {}`;
|
|
33
|
-
const ast = parseTaskSource(code);
|
|
34
|
-
const stages = extractStages(ast);
|
|
35
|
-
|
|
36
|
-
expect(stages).toHaveLength(1);
|
|
37
|
-
expect(stages[0]?.name).toBe("refine");
|
|
38
|
-
expect(stages[0]?.isAsync).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("returns empty array when there are no exports", () => {
|
|
42
|
-
const code = `function internal() {}`;
|
|
43
|
-
const ast = parseTaskSource(code);
|
|
44
|
-
expect(extractStages(ast)).toEqual([]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("ignores non-function exports", () => {
|
|
48
|
-
const code = `export const FOO = 42;`;
|
|
49
|
-
const ast = parseTaskSource(code);
|
|
50
|
-
expect(extractStages(ast)).toEqual([]);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -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
|
-
});
|