@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,10 +1,16 @@
|
|
|
1
|
-
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
1
|
+
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
2
2
|
import type { ChatResponse } from "../../providers/types.ts";
|
|
3
3
|
|
|
4
4
|
mock.module("../../llm/index.ts", () => ({
|
|
5
5
|
chat: mock(),
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
|
+
const internalLlm = { internalProvider: "deepseek", internalModel: "deepseek-v4-flash" };
|
|
9
|
+
|
|
10
|
+
mock.module("../../core/config.ts", () => ({
|
|
11
|
+
getConfig: () => ({ llm: internalLlm }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
8
14
|
import { chat } from "../../llm/index.ts";
|
|
9
15
|
import { deduceArtifactSchema } from "../enrichers/schema-deducer.ts";
|
|
10
16
|
|
|
@@ -35,14 +41,21 @@ const VALID_EXAMPLE = { id: "abc", value: 42 };
|
|
|
35
41
|
function makeResponse(content: Record<string, unknown>): Promise<ChatResponse> {
|
|
36
42
|
return Promise.resolve({
|
|
37
43
|
content,
|
|
38
|
-
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
|
|
44
|
+
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30, source: "reported" },
|
|
39
45
|
});
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
const DEFAULT_INTERNAL_LLM = { ...internalLlm };
|
|
49
|
+
|
|
42
50
|
beforeEach(() => {
|
|
43
51
|
mockedChat.mockClear();
|
|
44
52
|
});
|
|
45
53
|
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
internalLlm.internalProvider = DEFAULT_INTERNAL_LLM.internalProvider;
|
|
56
|
+
internalLlm.internalModel = DEFAULT_INTERNAL_LLM.internalModel;
|
|
57
|
+
});
|
|
58
|
+
|
|
46
59
|
describe("deduceArtifactSchema", () => {
|
|
47
60
|
it("returns DeducedSchema on valid LLM response", async () => {
|
|
48
61
|
mockedChat.mockReturnValue(
|
|
@@ -60,6 +73,25 @@ describe("deduceArtifactSchema", () => {
|
|
|
60
73
|
expect(result.reasoning).toBe("The artifact stores processed pipeline data.");
|
|
61
74
|
});
|
|
62
75
|
|
|
76
|
+
it("resolves provider and model from the internal config", async () => {
|
|
77
|
+
internalLlm.internalProvider = "zai";
|
|
78
|
+
internalLlm.internalModel = "glm-4";
|
|
79
|
+
mockedChat.mockReturnValue(
|
|
80
|
+
makeResponse({
|
|
81
|
+
schema: VALID_SCHEMA,
|
|
82
|
+
example: VALID_EXAMPLE,
|
|
83
|
+
reasoning: "uses configured model",
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
await deduceArtifactSchema(TASK_CODE, ARTIFACT);
|
|
88
|
+
|
|
89
|
+
expect(mockedChat.mock.calls[0]?.[0]).toMatchObject({
|
|
90
|
+
provider: "zhipu",
|
|
91
|
+
model: "glm-4",
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
63
95
|
it("throws when schema field is missing", async () => {
|
|
64
96
|
mockedChat.mockReturnValue(
|
|
65
97
|
makeResponse({
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const SRC_DIR = path.resolve(fileURLToPath(new URL("../../", import.meta.url)));
|
|
7
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
8
|
+
|
|
9
|
+
// The deterministic AST pipeline (step 10 / AC-11) is deleted. No source file may
|
|
10
|
+
// import the removed modules or the Babel toolchain that backed them.
|
|
11
|
+
const FORBIDDEN_PATTERNS: RegExp[] = [
|
|
12
|
+
/task-analysis\/parser/,
|
|
13
|
+
/task-analysis\/extractors/,
|
|
14
|
+
/task-analysis\/utils\/ast/,
|
|
15
|
+
/task-analysis\/enrichers\/artifact-resolver/,
|
|
16
|
+
/task-analysis\/enrichers\/analysis-writer/,
|
|
17
|
+
/@babel\//,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
async function collectSourceFiles(dir: string): Promise<string[]> {
|
|
21
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
22
|
+
const files: string[] = [];
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const full = path.join(dir, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
if (entry.name === "node_modules") continue;
|
|
27
|
+
files.push(...(await collectSourceFiles(full)));
|
|
28
|
+
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
29
|
+
files.push(full);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return files;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("deleted AST pipeline", () => {
|
|
36
|
+
it("is not imported anywhere in src/", async () => {
|
|
37
|
+
const files = await collectSourceFiles(SRC_DIR);
|
|
38
|
+
const offenders: { file: string; pattern: string }[] = [];
|
|
39
|
+
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
if (file === SELF) continue;
|
|
42
|
+
const text = await readFile(file, "utf8");
|
|
43
|
+
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
44
|
+
if (pattern.test(text)) {
|
|
45
|
+
offenders.push({ file: path.relative(SRC_DIR, file), pattern: String(pattern) });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
expect(offenders).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { TaskAnalysisRepository } from "../repository.ts";
|
|
6
|
+
import type { PersistedTaskAnalysis } from "../types.ts";
|
|
7
|
+
|
|
8
|
+
const ANALYSIS: PersistedTaskAnalysis = {
|
|
9
|
+
summary: "Fetches and summarizes source data.",
|
|
10
|
+
stages: [{ name: "fetch", purpose: "Retrieve the input document." }],
|
|
11
|
+
artifacts: {
|
|
12
|
+
reads: [{ fileName: "input.json", role: "source data" }],
|
|
13
|
+
writes: [{ fileName: "output.json", role: "summary" }],
|
|
14
|
+
},
|
|
15
|
+
models: [{ provider: "openai", method: "chat", stage: "fetch" }],
|
|
16
|
+
analyzedAt: "2026-06-22T00:00:00.000Z",
|
|
17
|
+
analysisStatus: "ok",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let tmpDir: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tmpDir = await mkdtemp(path.join(os.tmpdir(), "task-analysis-repo-"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("TaskAnalysisRepository.analysisPath", () => {
|
|
31
|
+
it("resolves the canonical pipeline-config tasks path", () => {
|
|
32
|
+
const result = TaskAnalysisRepository.analysisPath("/data", "my-pipe", "research");
|
|
33
|
+
|
|
34
|
+
expect(result).toBe(
|
|
35
|
+
path.join("/data", "pipeline-config", "my-pipe", "tasks", "research.analysis.json"),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("TaskAnalysisRepository.read", () => {
|
|
41
|
+
it("returns null when no analysis file exists", async () => {
|
|
42
|
+
const result = await TaskAnalysisRepository.read(tmpDir, "my-pipe", "research");
|
|
43
|
+
|
|
44
|
+
expect(result).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("TaskAnalysisRepository.write then read", () => {
|
|
49
|
+
it("round-trips the persisted analysis", async () => {
|
|
50
|
+
await TaskAnalysisRepository.write(tmpDir, "my-pipe", "research", ANALYSIS);
|
|
51
|
+
|
|
52
|
+
const result = await TaskAnalysisRepository.read(tmpDir, "my-pipe", "research");
|
|
53
|
+
|
|
54
|
+
expect(result).toEqual(ANALYSIS);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("writes atomically, leaving no temp file behind", async () => {
|
|
58
|
+
await TaskAnalysisRepository.write(tmpDir, "my-pipe", "research", ANALYSIS);
|
|
59
|
+
|
|
60
|
+
const tasksDir = path.join(tmpDir, "pipeline-config", "my-pipe", "tasks");
|
|
61
|
+
const entries = await readdir(tasksDir);
|
|
62
|
+
|
|
63
|
+
expect(entries).toEqual(["research.analysis.json"]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -1,165 +1,98 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { TaskAnalysisSchema } from "../types.ts";
|
|
2
3
|
import type {
|
|
3
|
-
|
|
4
|
-
Stage,
|
|
5
|
-
ArtifactRead,
|
|
6
|
-
ArtifactWrite,
|
|
7
|
-
UnresolvedRead,
|
|
8
|
-
UnresolvedWrite,
|
|
9
|
-
ModelCall,
|
|
10
|
-
ArtifactData,
|
|
4
|
+
AnalysisStatus,
|
|
11
5
|
TaskAnalysis,
|
|
12
6
|
PersistedTaskAnalysis,
|
|
13
7
|
DeducedSchema,
|
|
14
|
-
ArtifactResolution,
|
|
15
8
|
ArtifactDescriptor,
|
|
16
|
-
|
|
17
|
-
} from "../types.js";
|
|
18
|
-
|
|
19
|
-
describe("task-analysis types", () => {
|
|
20
|
-
it("SourceLocation accepts line and column", () => {
|
|
21
|
-
const loc: SourceLocation = { line: 1, column: 5 };
|
|
22
|
-
expect(loc.line).toBe(1);
|
|
23
|
-
expect(loc.column).toBe(5);
|
|
24
|
-
});
|
|
9
|
+
} from "../types.ts";
|
|
25
10
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
11
|
+
function wellFormedAnalysis(): TaskAnalysis {
|
|
12
|
+
return {
|
|
13
|
+
summary: "Ingests data and refines it into a report.",
|
|
14
|
+
stages: [
|
|
15
|
+
{ name: "ingestion", purpose: "Read raw input" },
|
|
16
|
+
{ name: "refine", purpose: "Produce the report" },
|
|
17
|
+
],
|
|
18
|
+
artifacts: {
|
|
19
|
+
reads: [{ fileName: "input.json", role: "raw source data" }],
|
|
20
|
+
writes: [{ fileName: "report.json", role: "final report" }],
|
|
21
|
+
},
|
|
22
|
+
models: [{ provider: "deepseek", method: "chat", stage: "refine" }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
expect(read.required).toBe(true);
|
|
36
|
-
});
|
|
26
|
+
describe("TaskAnalysisSchema validator", () => {
|
|
27
|
+
it("accepts a well-formed analysis", () => {
|
|
28
|
+
const result = TaskAnalysisSchema.safeParse(wellFormedAnalysis());
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
const write: ArtifactWrite = { fileName: "output.json", stage: "refine" };
|
|
40
|
-
expect(write.fileName).toBe("output.json");
|
|
41
|
-
expect(write.stage).toBe("refine");
|
|
30
|
+
expect(result.success).toBe(true);
|
|
42
31
|
});
|
|
43
32
|
|
|
44
|
-
it("
|
|
45
|
-
const
|
|
46
|
-
const unresolved: UnresolvedRead = {
|
|
47
|
-
expression: "dynamicName",
|
|
48
|
-
codeContext: "io.readArtifact(dynamicName)",
|
|
49
|
-
stage: "parse",
|
|
50
|
-
required: false,
|
|
51
|
-
location: loc,
|
|
52
|
-
};
|
|
53
|
-
expect(unresolved.expression).toBe("dynamicName");
|
|
54
|
-
expect(unresolved.required).toBe(false);
|
|
55
|
-
expect(unresolved.location.line).toBe(3);
|
|
56
|
-
});
|
|
33
|
+
it("rejects an analysis missing summary", () => {
|
|
34
|
+
const { summary, ...rest } = wellFormedAnalysis();
|
|
57
35
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
expression: "outputVar",
|
|
62
|
-
codeContext: "io.writeArtifact(outputVar)",
|
|
63
|
-
stage: "write",
|
|
64
|
-
location: loc,
|
|
65
|
-
};
|
|
66
|
-
expect(unresolved.stage).toBe("write");
|
|
67
|
-
expect(unresolved.location.column).toBe(0);
|
|
36
|
+
const result = TaskAnalysisSchema.safeParse(rest);
|
|
37
|
+
|
|
38
|
+
expect(result.success).toBe(false);
|
|
68
39
|
});
|
|
69
40
|
|
|
70
|
-
it("
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
41
|
+
it("rejects a stage missing purpose", () => {
|
|
42
|
+
const analysis = wellFormedAnalysis();
|
|
43
|
+
analysis.stages[0] = { name: "ingestion" } as never;
|
|
44
|
+
|
|
45
|
+
const result = TaskAnalysisSchema.safeParse(analysis);
|
|
46
|
+
|
|
47
|
+
expect(result.success).toBe(false);
|
|
74
48
|
});
|
|
75
49
|
|
|
76
|
-
it("
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
expect(Array.isArray(data.reads)).toBe(true);
|
|
84
|
-
expect(Array.isArray(data.writes)).toBe(true);
|
|
85
|
-
expect(Array.isArray(data.unresolvedReads)).toBe(true);
|
|
86
|
-
expect(Array.isArray(data.unresolvedWrites)).toBe(true);
|
|
50
|
+
it("rejects a read artifact missing role", () => {
|
|
51
|
+
const analysis = wellFormedAnalysis();
|
|
52
|
+
analysis.artifacts.reads[0] = { fileName: "input.json" } as never;
|
|
53
|
+
|
|
54
|
+
const result = TaskAnalysisSchema.safeParse(analysis);
|
|
55
|
+
|
|
56
|
+
expect(result.success).toBe(false);
|
|
87
57
|
});
|
|
88
58
|
|
|
89
|
-
it("
|
|
90
|
-
const analysis
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
};
|
|
96
|
-
expect(analysis.taskFilePath).toBeNull();
|
|
59
|
+
it("rejects models that are not an array", () => {
|
|
60
|
+
const analysis = { ...wellFormedAnalysis(), models: "deepseek" };
|
|
61
|
+
|
|
62
|
+
const result = TaskAnalysisSchema.safeParse(analysis);
|
|
63
|
+
|
|
64
|
+
expect(result.success).toBe(false);
|
|
97
65
|
});
|
|
66
|
+
});
|
|
98
67
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
models: [],
|
|
105
|
-
};
|
|
106
|
-
expect(analysis.taskFilePath).toBe("/path/to/task.js");
|
|
68
|
+
describe("doc-oriented analysis types", () => {
|
|
69
|
+
it("AnalysisStatus enumerates ok, unconfigured, and error", () => {
|
|
70
|
+
const statuses: AnalysisStatus[] = ["ok", "unconfigured", "error"];
|
|
71
|
+
|
|
72
|
+
expect(statuses).toEqual(["ok", "unconfigured", "error"]);
|
|
107
73
|
});
|
|
108
74
|
|
|
109
|
-
it("PersistedTaskAnalysis
|
|
75
|
+
it("PersistedTaskAnalysis adds analyzedAt, analysisStatus, and optional analysisError", () => {
|
|
110
76
|
const persisted: PersistedTaskAnalysis = {
|
|
111
|
-
|
|
112
|
-
stages: [],
|
|
113
|
-
artifacts: { reads: [], writes: [], unresolvedReads: [], unresolvedWrites: [] },
|
|
114
|
-
models: [],
|
|
77
|
+
...wellFormedAnalysis(),
|
|
115
78
|
analyzedAt: "2026-03-02T00:00:00.000Z",
|
|
79
|
+
analysisStatus: "error",
|
|
80
|
+
analysisError: "LLM call failed",
|
|
116
81
|
};
|
|
117
|
-
|
|
118
|
-
expect(persisted.
|
|
82
|
+
|
|
83
|
+
expect(persisted.analysisStatus).toBe("error");
|
|
84
|
+
expect(persisted.analysisError).toBe("LLM call failed");
|
|
119
85
|
});
|
|
120
86
|
|
|
121
|
-
it("DeducedSchema
|
|
87
|
+
it("keeps DeducedSchema and ArtifactDescriptor", () => {
|
|
122
88
|
const deduced: DeducedSchema = {
|
|
123
|
-
schema: { type: "object"
|
|
89
|
+
schema: { type: "object" },
|
|
124
90
|
example: { name: "test" },
|
|
125
91
|
reasoning: "Inferred from usage",
|
|
126
92
|
};
|
|
127
|
-
expect(deduced.reasoning).toBe("Inferred from usage");
|
|
128
|
-
expect(deduced.example).toEqual({ name: "test" });
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("ArtifactResolution accepts resolvedFileName as null", () => {
|
|
132
|
-
const resolution: ArtifactResolution = {
|
|
133
|
-
resolvedFileName: null,
|
|
134
|
-
confidence: 0,
|
|
135
|
-
reasoning: "Could not resolve",
|
|
136
|
-
};
|
|
137
|
-
expect(resolution.resolvedFileName).toBeNull();
|
|
138
|
-
expect(resolution.confidence).toBe(0);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("ArtifactResolution accepts resolvedFileName as a string", () => {
|
|
142
|
-
const resolution: ArtifactResolution = {
|
|
143
|
-
resolvedFileName: "data.json",
|
|
144
|
-
confidence: 0.9,
|
|
145
|
-
reasoning: "High confidence match",
|
|
146
|
-
};
|
|
147
|
-
expect(resolution.resolvedFileName).toBe("data.json");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("ArtifactDescriptor accepts fileName and stage", () => {
|
|
151
93
|
const descriptor: ArtifactDescriptor = { fileName: "report.json", stage: "generate" };
|
|
152
|
-
expect(descriptor.fileName).toBe("report.json");
|
|
153
|
-
expect(descriptor.stage).toBe("generate");
|
|
154
|
-
});
|
|
155
94
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
expression: "fileName",
|
|
159
|
-
codeContext: "io.readArtifact(fileName)",
|
|
160
|
-
stage: "process",
|
|
161
|
-
};
|
|
162
|
-
expect(descriptor.expression).toBe("fileName");
|
|
163
|
-
expect(descriptor.codeContext).toBe("io.readArtifact(fileName)");
|
|
95
|
+
expect(deduced.reasoning).toBe("Inferred from usage");
|
|
96
|
+
expect(descriptor.stage).toBe("generate");
|
|
164
97
|
});
|
|
165
98
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ── src/task-analysis/analyzer.ts ──
|
|
2
|
+
// LLM-only task analyzer: never throws, returns a status-tagged doc-oriented analysis.
|
|
3
|
+
|
|
4
|
+
import { chat, getAvailableProviders } from "../llm/index.ts";
|
|
5
|
+
import { getConfig } from "../core/config.ts";
|
|
6
|
+
import type { ChatMessage, ProviderName } from "../providers/types.ts";
|
|
7
|
+
import { TaskAnalysisSchema, type TaskAnalysis, type AnalysisStatus } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export type AnalysisResult = TaskAnalysis & {
|
|
10
|
+
analysisStatus: AnalysisStatus;
|
|
11
|
+
analysisError?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Maps a config provider key to its gateway provider name. */
|
|
15
|
+
function toGatewayProvider(configName: string): ProviderName {
|
|
16
|
+
if (configName === "zai") return "zhipu";
|
|
17
|
+
if (configName === "claude-code") return "claudecode";
|
|
18
|
+
return configName as ProviderName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Empty doc-shaped placeholder used for non-`ok` results. */
|
|
22
|
+
function emptyAnalysis(): TaskAnalysis {
|
|
23
|
+
return {
|
|
24
|
+
summary: "",
|
|
25
|
+
stages: [],
|
|
26
|
+
artifacts: { reads: [], writes: [] },
|
|
27
|
+
models: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildMessages(code: string): ChatMessage[] {
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
role: "system",
|
|
35
|
+
content:
|
|
36
|
+
"You document prompt-orchestration pipeline tasks. Read the task source and produce a concise, " +
|
|
37
|
+
"approximate reference. Respond with a single JSON object only, no prose or code fences.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
role: "user",
|
|
41
|
+
content:
|
|
42
|
+
"Analyze this pipeline task source and return a JSON object with these fields:\n" +
|
|
43
|
+
"- summary: one sentence describing what the task does\n" +
|
|
44
|
+
"- stages: array of { name: stage/function name, purpose: one sentence on its role }\n" +
|
|
45
|
+
"- artifacts.reads: array of { fileName, role } for artifacts the task reads\n" +
|
|
46
|
+
"- artifacts.writes: array of { fileName, role } for artifacts the task writes\n" +
|
|
47
|
+
"- models: array of { provider, method, stage } for LLM calls the task makes\n\n" +
|
|
48
|
+
"Use empty arrays when a section does not apply.\n\n" +
|
|
49
|
+
"Task source:\n```\n" +
|
|
50
|
+
code +
|
|
51
|
+
"\n```",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function analyzeTask(
|
|
57
|
+
code: string,
|
|
58
|
+
opts?: { provider?: string; model?: string },
|
|
59
|
+
): Promise<AnalysisResult> {
|
|
60
|
+
try {
|
|
61
|
+
const llm = getConfig().llm;
|
|
62
|
+
const configProvider = opts?.provider ?? llm.internalProvider;
|
|
63
|
+
const model = opts?.model ?? llm.internalModel;
|
|
64
|
+
const provider = toGatewayProvider(configProvider);
|
|
65
|
+
|
|
66
|
+
if (!getAvailableProviders()[provider]) {
|
|
67
|
+
return {
|
|
68
|
+
...emptyAnalysis(),
|
|
69
|
+
analysisStatus: "unconfigured",
|
|
70
|
+
analysisError: `No API key configured for the internal analysis provider "${configProvider}". Set the provider's API key or configure PO_INTERNAL_LLM_PROVIDER.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const response = await chat({
|
|
75
|
+
provider,
|
|
76
|
+
model,
|
|
77
|
+
messages: buildMessages(code),
|
|
78
|
+
temperature: 0,
|
|
79
|
+
responseFormat: { type: "json_object" },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const analysis = TaskAnalysisSchema.parse(response.content);
|
|
83
|
+
return { ...analysis, analysisStatus: "ok" };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return {
|
|
86
|
+
...emptyAnalysis(),
|
|
87
|
+
analysisStatus: "error",
|
|
88
|
+
analysisError: err instanceof Error ? err.message : String(err),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
import Ajv from "ajv";
|
|
5
5
|
import addFormats from "ajv-formats";
|
|
6
6
|
import { chat } from "../../llm/index.ts";
|
|
7
|
+
import { getConfig } from "../../core/config.ts";
|
|
8
|
+
import type { ProviderName } from "../../providers/types.ts";
|
|
7
9
|
import type { ArtifactDescriptor, DeducedSchema } from "../types.ts";
|
|
8
10
|
|
|
9
11
|
const ajv = addFormats(new Ajv());
|
|
10
12
|
|
|
13
|
+
/** Maps a config provider key to its gateway provider name. */
|
|
14
|
+
function toGatewayProvider(configName: string): ProviderName {
|
|
15
|
+
if (configName === "zai") return "zhipu";
|
|
16
|
+
if (configName === "claude-code") return "claudecode";
|
|
17
|
+
return configName as ProviderName;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
export async function deduceArtifactSchema(
|
|
12
21
|
taskCode: string,
|
|
13
22
|
artifact: ArtifactDescriptor,
|
|
@@ -34,9 +43,11 @@ Respond with a JSON object with these fields:
|
|
|
34
43
|
},
|
|
35
44
|
];
|
|
36
45
|
|
|
46
|
+
const llm = getConfig().llm;
|
|
47
|
+
|
|
37
48
|
const response = await chat({
|
|
38
|
-
provider:
|
|
39
|
-
model:
|
|
49
|
+
provider: toGatewayProvider(llm.internalProvider),
|
|
50
|
+
model: llm.internalModel,
|
|
40
51
|
messages,
|
|
41
52
|
temperature: 0,
|
|
42
53
|
responseFormat: { type: "json_object" },
|
|
@@ -1,40 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { extractArtifactReads, extractArtifactWrites } from "./extractors/artifacts.ts";
|
|
4
|
-
import { extractLLMCalls } from "./extractors/llm-calls.ts";
|
|
5
|
-
import type { TaskAnalysis } from "./types.ts";
|
|
6
|
-
|
|
7
|
-
export function analyzeTask(
|
|
8
|
-
code: string,
|
|
9
|
-
taskFilePath?: string | null
|
|
10
|
-
): TaskAnalysis {
|
|
11
|
-
const ast = parseTaskSource(code);
|
|
12
|
-
const stages = extractStages(ast);
|
|
13
|
-
const { reads, unresolvedReads } = extractArtifactReads(ast, code);
|
|
14
|
-
const { writes, unresolvedWrites } = extractArtifactWrites(ast, code);
|
|
15
|
-
const models = extractLLMCalls(ast);
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
taskFilePath: taskFilePath ?? null,
|
|
19
|
-
stages,
|
|
20
|
-
artifacts: { reads, writes, unresolvedReads, unresolvedWrites },
|
|
21
|
-
models,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export { parseTaskSource } from "./parser.ts";
|
|
26
|
-
export { extractStages } from "./extractors/stages.ts";
|
|
27
|
-
export {
|
|
28
|
-
extractArtifactReads,
|
|
29
|
-
extractArtifactWrites,
|
|
30
|
-
extractCodeContext,
|
|
31
|
-
} from "./extractors/artifacts.ts";
|
|
32
|
-
export { extractLLMCalls } from "./extractors/llm-calls.ts";
|
|
33
|
-
export { isInsideTryCatch, getStageName } from "./utils/ast.ts";
|
|
34
|
-
|
|
1
|
+
export { analyzeTask } from "./analyzer.ts";
|
|
2
|
+
export { TaskAnalysisRepository } from "./repository.ts";
|
|
35
3
|
export { deduceArtifactSchema } from "./enrichers/schema-deducer.ts";
|
|
36
|
-
export { resolveArtifactReference } from "./enrichers/artifact-resolver.ts";
|
|
37
4
|
export { writeSchemaFiles } from "./enrichers/schema-writer.ts";
|
|
38
|
-
export { writeAnalysisFile } from "./enrichers/analysis-writer.ts";
|
|
39
5
|
|
|
40
6
|
export type * from "./types.ts";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir, rename } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { PersistedTaskAnalysis } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export const TaskAnalysisRepository = {
|
|
7
|
+
analysisPath(root: string, slug: string, taskName: string): string {
|
|
8
|
+
return path.join(root, "pipeline-config", slug, "tasks", `${taskName}.analysis.json`);
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
async write(
|
|
12
|
+
root: string,
|
|
13
|
+
slug: string,
|
|
14
|
+
taskName: string,
|
|
15
|
+
data: PersistedTaskAnalysis,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const filePath = this.analysisPath(root, slug, taskName);
|
|
18
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
19
|
+
|
|
20
|
+
const tmpPath = `${filePath}.tmp`;
|
|
21
|
+
await Bun.write(tmpPath, JSON.stringify(data, null, 2));
|
|
22
|
+
await rename(tmpPath, filePath);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async read(
|
|
26
|
+
root: string,
|
|
27
|
+
slug: string,
|
|
28
|
+
taskName: string,
|
|
29
|
+
): Promise<PersistedTaskAnalysis | null> {
|
|
30
|
+
const filePath = this.analysisPath(root, slug, taskName);
|
|
31
|
+
const file = Bun.file(filePath);
|
|
32
|
+
|
|
33
|
+
if (!(await file.exists())) return null;
|
|
34
|
+
|
|
35
|
+
return (await file.json()) as PersistedTaskAnalysis;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async readFromPath(validatedPath: string): Promise<PersistedTaskAnalysis | null> {
|
|
39
|
+
const file = Bun.file(validatedPath);
|
|
40
|
+
|
|
41
|
+
if (!(await file.exists())) return null;
|
|
42
|
+
|
|
43
|
+
return (await file.json()) as PersistedTaskAnalysis;
|
|
44
|
+
},
|
|
45
|
+
} as const;
|