taskmeld 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +172 -0
- package/README.zh-CN.md +172 -0
- package/dist/src/app/app-context-env.js +51 -0
- package/dist/src/app/create-app-context.js +127 -0
- package/dist/src/app/data-dir.js +29 -0
- package/dist/src/app/pipeline-config.js +105 -0
- package/dist/src/app/pipeline-plugin-config.js +2 -0
- package/dist/src/app/pipeline-registry.js +502 -0
- package/dist/src/app/pipeline-runtime.js +202 -0
- package/dist/src/app/runtime-store.js +151 -0
- package/dist/src/app/user-config.js +37 -0
- package/dist/src/artifacts/artifact-cleanup.js +192 -0
- package/dist/src/artifacts/artifact-index.js +262 -0
- package/dist/src/artifacts/artifact-rebuilder.js +120 -0
- package/dist/src/artifacts/storage-service.js +371 -0
- package/dist/src/cli/bootstrap.js +226 -0
- package/dist/src/cli/commands/agent.js +126 -0
- package/dist/src/cli/commands/artifact.js +175 -0
- package/dist/src/cli/commands/init.js +150 -0
- package/dist/src/cli/commands/pipeline/errors.js +37 -0
- package/dist/src/cli/commands/pipeline/result.js +179 -0
- package/dist/src/cli/commands/pipeline/selector.js +51 -0
- package/dist/src/cli/commands/pipeline/types.js +2 -0
- package/dist/src/cli/commands/pipeline/watch.js +67 -0
- package/dist/src/cli/commands/pipeline.js +339 -0
- package/dist/src/cli/commands/scheduler.js +81 -0
- package/dist/src/cli/commands/server.js +70 -0
- package/dist/src/cli/commands/system.js +21 -0
- package/dist/src/cli/errors.js +71 -0
- package/dist/src/cli/help.js +184 -0
- package/dist/src/cli/index.js +65 -0
- package/dist/src/cli/output.js +19 -0
- package/dist/src/cli/renderers/engine/json.js +67 -0
- package/dist/src/cli/renderers/engine/markdown.js +95 -0
- package/dist/src/cli/renderers/engine/types.js +2 -0
- package/dist/src/cli/renderers/engine/utils.js +32 -0
- package/dist/src/cli/renderers/index.js +27 -0
- package/dist/src/cli/renderers/specs/agent.js +78 -0
- package/dist/src/cli/renderers/specs/artifact.js +32 -0
- package/dist/src/cli/renderers/specs/index.js +36 -0
- package/dist/src/cli/renderers/specs/init.js +25 -0
- package/dist/src/cli/renderers/specs/pipeline.js +561 -0
- package/dist/src/cli/renderers/specs/scheduler.js +46 -0
- package/dist/src/cli/renderers/specs/server.js +38 -0
- package/dist/src/cli/renderers/specs/system.js +36 -0
- package/dist/src/cli/router.js +199 -0
- package/dist/src/cli/server-runtime-client.js +780 -0
- package/dist/src/cli/types.js +2 -0
- package/dist/src/gateway/frame-sanitizer.js +78 -0
- package/dist/src/gateway/gateway-client.js +462 -0
- package/dist/src/gateway/index.js +18 -0
- package/dist/src/gateway/types.js +2 -0
- package/dist/src/index.js +123 -0
- package/dist/src/logs/run-log-reader.js +141 -0
- package/dist/src/logs/run-log-service.js +42 -0
- package/dist/src/logs/run-log-types.js +2 -0
- package/dist/src/pipeline/agent-activity.js +191 -0
- package/dist/src/pipeline/artifact-storage.js +208 -0
- package/dist/src/pipeline/diagnostics/dependency-diagnostic.js +105 -0
- package/dist/src/pipeline/diagnostics/index.js +6 -0
- package/dist/src/pipeline/dispatch/pipeline-inbound-queue.js +215 -0
- package/dist/src/pipeline/dispatch/pipeline-link-dispatcher.js +66 -0
- package/dist/src/pipeline/dispatch/pipeline-link-store.js +94 -0
- package/dist/src/pipeline/dispatch/pipeline-queue-drainer.js +71 -0
- package/dist/src/pipeline/execution/dependency-check.js +52 -0
- package/dist/src/pipeline/execution/execution-result.js +2 -0
- package/dist/src/pipeline/execution/group-item-executor.js +128 -0
- package/dist/src/pipeline/execution/index.js +5 -0
- package/dist/src/pipeline/execution/node-item-executor.js +58 -0
- package/dist/src/pipeline/execution/node-runner.js +159 -0
- package/dist/src/pipeline/execution/readiness-state.js +10 -0
- package/dist/src/pipeline/execution/reject-handler.js +94 -0
- package/dist/src/pipeline/execution/rejected-artifact-archiver.js +45 -0
- package/dist/src/pipeline/execution/route-item-manager.js +253 -0
- package/dist/src/pipeline/execution/run-abort-controller.js +66 -0
- package/dist/src/pipeline/execution/run-state-helpers.js +257 -0
- package/dist/src/pipeline/execution/service.js +165 -0
- package/dist/src/pipeline/execution/session-registry.js +96 -0
- package/dist/src/pipeline/execution/structured-node-runner.js +411 -0
- package/dist/src/pipeline/execution-status.js +96 -0
- package/dist/src/pipeline/execution-timeout.js +21 -0
- package/dist/src/pipeline/identity/index.js +32 -0
- package/dist/src/pipeline/identity/types.js +2 -0
- package/dist/src/pipeline/item-batch-controller.js +227 -0
- package/dist/src/pipeline/output/pipeline-output-resolver.js +91 -0
- package/dist/src/pipeline/output/pipeline-output-store.js +60 -0
- package/dist/src/pipeline/runtime-model.js +173 -0
- package/dist/src/pipeline/scheduler/dependency-state.js +144 -0
- package/dist/src/pipeline/scheduler-service.js +314 -0
- package/dist/src/pipeline/state/group-item-state.js +50 -0
- package/dist/src/pipeline/state/group-run-state.js +41 -0
- package/dist/src/pipeline/state/index.js +20 -0
- package/dist/src/pipeline/state/node-item-state.js +67 -0
- package/dist/src/pipeline/state/node-run-state.js +51 -0
- package/dist/src/pipeline/state/types.js +2 -0
- package/dist/src/pipeline/state-machine.js +101 -0
- package/dist/src/pipeline/structured-output/contract.js +133 -0
- package/dist/src/pipeline/structured-output/index.js +22 -0
- package/dist/src/pipeline/structured-output/parser.js +214 -0
- package/dist/src/pipeline/structured-output/prompt.js +290 -0
- package/dist/src/pipeline/structured-output/waiter.js +139 -0
- package/dist/src/pipeline/template.js +135 -0
- package/dist/src/pipeline/timeline-log-store.js +57 -0
- package/dist/src/pipeline/tool-activity.js +94 -0
- package/dist/src/pipeline/types/pipeline-link.js +7 -0
- package/dist/src/pipeline/types/pipeline-output.js +11 -0
- package/dist/src/pipeline/types/workflow.js +2 -0
- package/dist/src/pipeline/workflow/branch-rules.js +74 -0
- package/dist/src/pipeline/workflow/defaults.js +48 -0
- package/dist/src/pipeline/workflow/io.js +89 -0
- package/dist/src/pipeline/workflow/normalize.js +347 -0
- package/dist/src/pipeline/workflow/routes.js +16 -0
- package/dist/src/pipeline/workflow/template-mapper.js +113 -0
- package/dist/src/pipeline/workflow/validate.js +312 -0
- package/dist/src/pipeline/workflow-graph.js +165 -0
- package/dist/src/server/api-handler.js +163 -0
- package/dist/src/server/http-utils.js +34 -0
- package/dist/src/server/middleware.js +61 -0
- package/dist/src/server/router.js +105 -0
- package/dist/src/server/routes/agents.js +189 -0
- package/dist/src/server/routes/artifacts.js +163 -0
- package/dist/src/server/routes/gateway.js +18 -0
- package/dist/src/server/routes/health.js +16 -0
- package/dist/src/server/routes/logs.js +73 -0
- package/dist/src/server/routes/pipeline-batch.js +163 -0
- package/dist/src/server/routes/pipeline-diagnostics.js +33 -0
- package/dist/src/server/routes/pipeline-identity.js +24 -0
- package/dist/src/server/routes/pipeline-links.js +117 -0
- package/dist/src/server/routes/pipeline-outputs.js +27 -0
- package/dist/src/server/routes/pipeline-queue.js +62 -0
- package/dist/src/server/routes/pipeline-runtime.js +162 -0
- package/dist/src/server/routes/pipeline-scheduler.js +69 -0
- package/dist/src/server/routes/pipeline-workflow.js +180 -0
- package/dist/src/server/routes/pipelines.js +96 -0
- package/dist/src/server/routes/sessions.js +244 -0
- package/dist/src/server/routes/timeline.js +14 -0
- package/dist/src/server/serve-static.js +42 -0
- package/dist/src/server/types.js +2 -0
- package/dist/src/services/agent-service.js +79 -0
- package/dist/src/services/artifact-service.js +74 -0
- package/dist/src/services/gateway-read-helpers.js +10 -0
- package/dist/src/services/index.js +23 -0
- package/dist/src/services/pipeline-service.js +529 -0
- package/dist/src/services/pipeline-status.js +93 -0
- package/dist/src/services/read-services.js +60 -0
- package/dist/src/services/scheduler-service.js +37 -0
- package/dist/src/services/session-service.js +227 -0
- package/dist/src/services/system-service.js +26 -0
- package/dist/src/transport/ws-broker.js +48 -0
- package/dist/src/utils/array.js +17 -0
- package/dist/src/utils/guards.js +5 -0
- package/dist/src/utils/session.js +60 -0
- package/dist/src/version.js +5 -0
- package/package.json +61 -0
- package/web/dist/assets/index-CWnfhkn-.js +65 -0
- package/web/dist/assets/index-gZ0xOfSO.css +1 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/web/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/web/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/web/dist/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/web/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/web/dist/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/web/dist/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
- package/web/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
- package/web/dist/favicon.svg +10 -0
- package/web/dist/index.html +14 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeRouteListWithDefaults = exports.DEFAULT_BRANCH_ROUTE_VALUE = exports.MAINLINE_ROUTE_VALUE = void 0;
|
|
4
|
+
exports.MAINLINE_ROUTE_VALUE = "yes";
|
|
5
|
+
exports.DEFAULT_BRANCH_ROUTE_VALUE = "no";
|
|
6
|
+
const normalizeRouteListWithDefaults = (routes) => {
|
|
7
|
+
const normalized = new Set([exports.MAINLINE_ROUTE_VALUE, exports.DEFAULT_BRANCH_ROUTE_VALUE]);
|
|
8
|
+
for (const route of routes) {
|
|
9
|
+
const trimmed = route.trim();
|
|
10
|
+
if (!trimmed)
|
|
11
|
+
continue;
|
|
12
|
+
normalized.add(trimmed);
|
|
13
|
+
}
|
|
14
|
+
return [...normalized];
|
|
15
|
+
};
|
|
16
|
+
exports.normalizeRouteListWithDefaults = normalizeRouteListWithDefaults;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeTemplateNodesIntoWorkflow = exports.workflowToTemplateNodes = void 0;
|
|
4
|
+
const validate_1 = require("./validate");
|
|
5
|
+
const toUniqueList = (items) => [...new Set(items)];
|
|
6
|
+
// ====== Workflow → Template nodes (unified, with dedup) ======
|
|
7
|
+
/**
|
|
8
|
+
* 从 WorkflowDefinitionRuntime 提取模板节点(仅 dependency 类型的出边)。
|
|
9
|
+
* 这是 workflow → template 映射的唯一权威实现。
|
|
10
|
+
*/
|
|
11
|
+
const workflowToTemplateNodes = (workflow) => {
|
|
12
|
+
const incomingByNodeId = new Map();
|
|
13
|
+
for (const edge of workflow.edges) {
|
|
14
|
+
// template.dependsOn 只表达依赖边;路由边属于分流语义,不能回写成普通依赖。
|
|
15
|
+
if (edge.when !== null)
|
|
16
|
+
continue;
|
|
17
|
+
const prev = incomingByNodeId.get(edge.to) ?? [];
|
|
18
|
+
prev.push(edge.from);
|
|
19
|
+
incomingByNodeId.set(edge.to, toUniqueList(prev));
|
|
20
|
+
}
|
|
21
|
+
return workflow.nodes.map((node) => ({
|
|
22
|
+
id: node.id,
|
|
23
|
+
title: node.name,
|
|
24
|
+
executor: node.executor,
|
|
25
|
+
instruction: node.instruction,
|
|
26
|
+
outputSpec: node.outputSpec,
|
|
27
|
+
dependsOn: incomingByNodeId.get(node.id) ?? [],
|
|
28
|
+
allowReject: node.allowReject,
|
|
29
|
+
maxRejectCount: node.maxRejectCount,
|
|
30
|
+
}));
|
|
31
|
+
};
|
|
32
|
+
exports.workflowToTemplateNodes = workflowToTemplateNodes;
|
|
33
|
+
// ====== Merge template nodes into workflow ======
|
|
34
|
+
const mergeTemplateNodesIntoWorkflow = (current, nextTemplateNodes) => {
|
|
35
|
+
const currentNodeById = new Map(current.nodes.map((node) => [node.id, node]));
|
|
36
|
+
const nodeIds = new Set(nextTemplateNodes.map((node) => node.id));
|
|
37
|
+
const groupIds = new Set(current.groups.map((group) => group.id));
|
|
38
|
+
const mergedNodes = nextTemplateNodes.map((tpl) => {
|
|
39
|
+
const existing = currentNodeById.get(tpl.id);
|
|
40
|
+
if (!existing) {
|
|
41
|
+
return {
|
|
42
|
+
id: tpl.id,
|
|
43
|
+
name: tpl.title,
|
|
44
|
+
type: "task",
|
|
45
|
+
enabled: true,
|
|
46
|
+
isMainline: true,
|
|
47
|
+
lane: "main",
|
|
48
|
+
parallelGroupId: null,
|
|
49
|
+
executor: tpl.executor,
|
|
50
|
+
inputMode: "single",
|
|
51
|
+
outputMode: "single",
|
|
52
|
+
dependencyPolicy: "all",
|
|
53
|
+
routePolicy: null,
|
|
54
|
+
retryPolicy: {
|
|
55
|
+
maxAttempts: 2,
|
|
56
|
+
backoffMs: 0,
|
|
57
|
+
},
|
|
58
|
+
outputSpec: tpl.outputSpec,
|
|
59
|
+
instruction: tpl.instruction,
|
|
60
|
+
allowReject: tpl.allowReject,
|
|
61
|
+
maxRejectCount: tpl.maxRejectCount,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
...existing,
|
|
66
|
+
name: tpl.title,
|
|
67
|
+
executor: tpl.executor,
|
|
68
|
+
instruction: tpl.instruction,
|
|
69
|
+
outputSpec: tpl.outputSpec,
|
|
70
|
+
allowReject: tpl.allowReject,
|
|
71
|
+
maxRejectCount: tpl.maxRejectCount,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
const unconditionalEdges = [];
|
|
75
|
+
for (const node of nextTemplateNodes) {
|
|
76
|
+
for (const dep of node.dependsOn) {
|
|
77
|
+
if (!nodeIds.has(dep))
|
|
78
|
+
continue;
|
|
79
|
+
unconditionalEdges.push({
|
|
80
|
+
from: dep,
|
|
81
|
+
to: node.id,
|
|
82
|
+
when: null,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const conditionalEdges = current.edges.filter((edge) => edge.when !== null &&
|
|
87
|
+
(nodeIds.has(edge.from) || groupIds.has(edge.from)) &&
|
|
88
|
+
(nodeIds.has(edge.to) || groupIds.has(edge.to)));
|
|
89
|
+
const allEdges = [...unconditionalEdges, ...conditionalEdges];
|
|
90
|
+
const edgeSeen = new Set();
|
|
91
|
+
const mergedEdges = [];
|
|
92
|
+
for (const edge of allEdges) {
|
|
93
|
+
const key = `${edge.from}|${edge.when ?? ""}|${edge.to}`;
|
|
94
|
+
if (edgeSeen.has(key))
|
|
95
|
+
continue;
|
|
96
|
+
edgeSeen.add(key);
|
|
97
|
+
mergedEdges.push(edge);
|
|
98
|
+
}
|
|
99
|
+
const mergedGroups = current.groups.filter((group) => group.members.every((member) => nodeIds.has(member)));
|
|
100
|
+
const merged = {
|
|
101
|
+
version: "3.0",
|
|
102
|
+
scheduler: current.scheduler,
|
|
103
|
+
plugins: current.plugins,
|
|
104
|
+
output: current.output,
|
|
105
|
+
nodes: mergedNodes,
|
|
106
|
+
edges: mergedEdges,
|
|
107
|
+
groups: mergedGroups,
|
|
108
|
+
};
|
|
109
|
+
if (!(0, validate_1.validateWorkflowGraph)(merged).ok)
|
|
110
|
+
return current;
|
|
111
|
+
return merged;
|
|
112
|
+
};
|
|
113
|
+
exports.mergeTemplateNodesIntoWorkflow = mergeTemplateNodesIntoWorkflow;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateWorkflowDefinition = exports.validateWorkflowOutputConfig = exports.validateWorkflowGraph = void 0;
|
|
4
|
+
const branch_rules_1 = require("./branch-rules");
|
|
5
|
+
const routes_1 = require("./routes");
|
|
6
|
+
// ====== Validation ======
|
|
7
|
+
const validateWorkflowGraph = (workflow) => {
|
|
8
|
+
if (workflow.nodes.length === 0) {
|
|
9
|
+
return workflow.edges.length === 0 && workflow.groups.length === 0
|
|
10
|
+
? { ok: true }
|
|
11
|
+
: { ok: false, error: "invalid_workflow_definition", detail: "空 workflow 不能包含 edges 或 groups" };
|
|
12
|
+
}
|
|
13
|
+
const nodeIds = new Set(workflow.nodes.map((node) => node.id));
|
|
14
|
+
if (nodeIds.size !== workflow.nodes.length) {
|
|
15
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "workflow.nodes 存在重复 id" };
|
|
16
|
+
}
|
|
17
|
+
const groupIds = new Set(workflow.groups.map((group) => group.id));
|
|
18
|
+
const entityIds = new Set([...nodeIds, ...groupIds]);
|
|
19
|
+
const outgoing = new Map();
|
|
20
|
+
const indegree = new Map();
|
|
21
|
+
for (const node of workflow.nodes) {
|
|
22
|
+
outgoing.set(node.id, []);
|
|
23
|
+
indegree.set(node.id, 0);
|
|
24
|
+
}
|
|
25
|
+
for (const group of workflow.groups) {
|
|
26
|
+
outgoing.set(group.id, []);
|
|
27
|
+
indegree.set(group.id, 0);
|
|
28
|
+
}
|
|
29
|
+
const edgeDedupe = new Set();
|
|
30
|
+
for (const edge of workflow.edges) {
|
|
31
|
+
if (!entityIds.has(edge.from) || !entityIds.has(edge.to)) {
|
|
32
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `边引用了不存在实体: ${edge.from} -> ${edge.to}` };
|
|
33
|
+
}
|
|
34
|
+
if (edge.from === edge.to) {
|
|
35
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `检测到自环边: ${edge.from} -> ${edge.to}` };
|
|
36
|
+
}
|
|
37
|
+
const key = `${edge.from}|${edge.when ?? ""}|${edge.to}`;
|
|
38
|
+
if (edgeDedupe.has(key)) {
|
|
39
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `检测到重复边: ${edge.from} -> ${edge.to}` };
|
|
40
|
+
}
|
|
41
|
+
edgeDedupe.add(key);
|
|
42
|
+
outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to]);
|
|
43
|
+
indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1);
|
|
44
|
+
}
|
|
45
|
+
const outgoingKindsBySource = new Map();
|
|
46
|
+
const edgesBySource = new Map();
|
|
47
|
+
for (const edge of workflow.edges) {
|
|
48
|
+
const kind = edge.when === null ? "dependency" : "route";
|
|
49
|
+
const kinds = outgoingKindsBySource.get(edge.from) ?? new Set();
|
|
50
|
+
kinds.add(kind);
|
|
51
|
+
outgoingKindsBySource.set(edge.from, kinds);
|
|
52
|
+
edgesBySource.set(edge.from, [...(edgesBySource.get(edge.from) ?? []), edge]);
|
|
53
|
+
}
|
|
54
|
+
for (const [sourceId, kinds] of outgoingKindsBySource.entries()) {
|
|
55
|
+
if (kinds.size <= 1)
|
|
56
|
+
continue;
|
|
57
|
+
const sourceNode = workflow.nodes.find((node) => node.id === sourceId);
|
|
58
|
+
if (sourceNode?.routePolicy)
|
|
59
|
+
continue;
|
|
60
|
+
// 非分流节点仍禁止同一节点混合依赖边和路由边,避免无条件放行导致重复执行。
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: "mixed_outgoing_edge_kinds_forbidden",
|
|
64
|
+
detail: `节点 ${sourceId} 同时存在 dependency 与 route 出边,已禁止保存`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Phase 2: 基于显式 scope 的跨支线边检测。
|
|
68
|
+
// computeNodeScopes + isCrossBranchEdgeByScope 使用显式 branchScopeId(缺失时从 route 边推导)。
|
|
69
|
+
{
|
|
70
|
+
const explicitScopes = new Map();
|
|
71
|
+
const mergeNodeIds = new Set();
|
|
72
|
+
for (const node of workflow.nodes) {
|
|
73
|
+
if (node.branchScopeId != null) {
|
|
74
|
+
explicitScopes.set(node.id, node.branchScopeId);
|
|
75
|
+
}
|
|
76
|
+
// merge 节点(dependencyPolicy !== "all")是显式分支汇聚点,接受来自不同 scope 的依赖边
|
|
77
|
+
if (node.dependencyPolicy && node.dependencyPolicy !== "all") {
|
|
78
|
+
mergeNodeIds.add(node.id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const nodeScopes = (0, branch_rules_1.computeNodeScopes)(workflow.nodes, workflow.edges, explicitScopes);
|
|
82
|
+
// 对 merge 节点清除 scope,避免其被误判为跨支线(与 workflow-graph.ts 的 buildIndices 保持一致)
|
|
83
|
+
for (const nodeId of mergeNodeIds) {
|
|
84
|
+
nodeScopes.set(nodeId, null);
|
|
85
|
+
}
|
|
86
|
+
const scopeCrossEdges = workflow.edges.filter((edge) => (0, branch_rules_1.isCrossBranchEdgeByScope)(edge, nodeScopes));
|
|
87
|
+
if (scopeCrossEdges.length > 0) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: "cross_branch_edge_forbidden",
|
|
91
|
+
detail: `禁止跨支线无条件边: ${scopeCrossEdges[0].from} -> ${scopeCrossEdges[0].to}(from 分支 ${nodeScopes.get(scopeCrossEdges[0].from) ?? "main"} -> to 分支 ${nodeScopes.get(scopeCrossEdges[0].to) ?? "main"},跨支线依赖边需要显式 merge 节点)`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const uniqueGroupIds = new Set();
|
|
96
|
+
const explicitGroupById = new Map(workflow.groups.map((group) => [group.id, group]));
|
|
97
|
+
for (const group of workflow.groups) {
|
|
98
|
+
if (uniqueGroupIds.has(group.id)) {
|
|
99
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `并行组 id 重复: ${group.id}` };
|
|
100
|
+
}
|
|
101
|
+
uniqueGroupIds.add(group.id);
|
|
102
|
+
for (const member of group.members) {
|
|
103
|
+
if (!nodeIds.has(member)) {
|
|
104
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `并行组 ${group.id} 引用了不存在成员 ${member}` };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const node of workflow.nodes) {
|
|
109
|
+
const groupId = node.parallelGroupId?.trim();
|
|
110
|
+
if (!groupId)
|
|
111
|
+
continue;
|
|
112
|
+
const group = explicitGroupById.get(groupId);
|
|
113
|
+
if (!group)
|
|
114
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 引用了不存在并行组 ${groupId}` };
|
|
115
|
+
if (!group.members.includes(node.id)) {
|
|
116
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 未加入其声明的并行组 ${groupId}` };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const group of workflow.groups) {
|
|
120
|
+
const memberSet = new Set(group.members);
|
|
121
|
+
const groupIncoming = new Set(workflow.edges
|
|
122
|
+
.filter((edge) => edge.to === group.id)
|
|
123
|
+
.map((edge) => edge.from));
|
|
124
|
+
for (const edge of workflow.edges) {
|
|
125
|
+
if (edge.when !== null)
|
|
126
|
+
continue;
|
|
127
|
+
if (!memberSet.has(edge.to))
|
|
128
|
+
continue;
|
|
129
|
+
if (edge.to === group.id)
|
|
130
|
+
continue;
|
|
131
|
+
if (edge.from === group.id)
|
|
132
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `并行组 ${group.id} 不能直接连入成员节点` };
|
|
133
|
+
if (memberSet.has(edge.from))
|
|
134
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `并行组 ${group.id} 成员之间禁止直接依赖` };
|
|
135
|
+
if (groupIncoming.has(edge.from))
|
|
136
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `并行组 ${group.id} 的入口节点不能直连成员` };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const group of workflow.groups) {
|
|
140
|
+
// joinPolicy 仅支持 "all";any/quorum 运行时未实现,保存时显式拒绝
|
|
141
|
+
if (group.joinPolicy !== "all") {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error: "join_policy_not_supported",
|
|
145
|
+
detail: `并行组 ${group.id} 的 joinPolicy "${group.joinPolicy}" 未支持,当前仅支持 "all"`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const node of workflow.nodes) {
|
|
150
|
+
if (node.routePolicy) {
|
|
151
|
+
const { allowed } = node.routePolicy;
|
|
152
|
+
if (allowed.length < 2 || allowed.length > 5) {
|
|
153
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 的路由集合长度非法` };
|
|
154
|
+
}
|
|
155
|
+
if (!allowed.includes(routes_1.MAINLINE_ROUTE_VALUE) || !allowed.includes(routes_1.DEFAULT_BRANCH_ROUTE_VALUE)) {
|
|
156
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 开启分流后必须包含 yes 和 no` };
|
|
157
|
+
}
|
|
158
|
+
const outgoingEdges = edgesBySource.get(node.id) ?? [];
|
|
159
|
+
const dependencyEdges = outgoingEdges.filter((edge) => edge.when === null);
|
|
160
|
+
if (dependencyEdges.length > 1) {
|
|
161
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 的 yes 主线依赖边最多只能有 1 条` };
|
|
162
|
+
}
|
|
163
|
+
const routeEdgeCounts = new Map();
|
|
164
|
+
for (const edge of outgoingEdges.filter((item) => item.when !== null)) {
|
|
165
|
+
routeEdgeCounts.set(edge.when ?? "", (routeEdgeCounts.get(edge.when ?? "") ?? 0) + 1);
|
|
166
|
+
if (edge.when === routes_1.MAINLINE_ROUTE_VALUE) {
|
|
167
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 的 yes 不能保存为路由边` };
|
|
168
|
+
}
|
|
169
|
+
if (!allowed.includes(edge.when ?? "")) {
|
|
170
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 存在未声明的路由边: ${edge.when}` };
|
|
171
|
+
}
|
|
172
|
+
const targetNode = workflow.nodes.find((candidate) => candidate.id === edge.to);
|
|
173
|
+
const targetGroup = workflow.groups.find((group) => group.id === edge.to);
|
|
174
|
+
const targetGroupMembers = targetGroup
|
|
175
|
+
? targetGroup.members.map((memberId) => workflow.nodes.find((candidate) => candidate.id === memberId)).filter(Boolean)
|
|
176
|
+
: [];
|
|
177
|
+
const isBranchTarget = targetNode?.lane === "branch" || (targetGroupMembers.length > 0 && targetGroupMembers.every((member) => member?.lane === "branch"));
|
|
178
|
+
if (!isBranchTarget) {
|
|
179
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 的路由 ${edge.when} 只能指向支线节点或支线并行组` };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const route of allowed.filter((item) => item !== routes_1.MAINLINE_ROUTE_VALUE)) {
|
|
183
|
+
if ((routeEdgeCounts.get(route) ?? 0) !== 1) {
|
|
184
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 的路由 ${route} 必须配置且只能配置 1 个支线目标` };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (node.dependencyPolicy !== undefined && node.dependencyPolicy !== "all" && node.dependencyPolicy !== "any") {
|
|
189
|
+
return { ok: false, error: "invalid_workflow_definition", detail: `节点 ${node.id} 的 dependencyPolicy 非法` };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const queue = [...[...entityIds].filter((id) => (indegree.get(id) ?? 0) === 0)];
|
|
193
|
+
let visited = 0;
|
|
194
|
+
while (queue.length > 0) {
|
|
195
|
+
const nodeId = queue.shift();
|
|
196
|
+
visited += 1;
|
|
197
|
+
for (const nextId of outgoing.get(nodeId) ?? []) {
|
|
198
|
+
const nextDegree = (indegree.get(nextId) ?? 0) - 1;
|
|
199
|
+
indegree.set(nextId, nextDegree);
|
|
200
|
+
if (nextDegree === 0) {
|
|
201
|
+
queue.push(nextId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (visited !== entityIds.size) {
|
|
206
|
+
return { ok: false, error: "invalid_workflow_definition", detail: "工作流存在环路,无法拓扑排序" };
|
|
207
|
+
}
|
|
208
|
+
return { ok: true };
|
|
209
|
+
};
|
|
210
|
+
exports.validateWorkflowGraph = validateWorkflowGraph;
|
|
211
|
+
const validateWorkflowOutputConfig = (workflow) => {
|
|
212
|
+
const output = workflow.output ?? { mode: "mainline_last", nodeId: null };
|
|
213
|
+
if (workflow.nodes.length === 0) {
|
|
214
|
+
return output.mode === "explicit" && output.nodeId
|
|
215
|
+
? { ok: false, error: "invalid_workflow_output_config", detail: "空 workflow 不能指定输出节点" }
|
|
216
|
+
: { ok: true };
|
|
217
|
+
}
|
|
218
|
+
if (output.mode === "explicit") {
|
|
219
|
+
if (!output.nodeId) {
|
|
220
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: "mode=explicit 时 nodeId 必填" };
|
|
221
|
+
}
|
|
222
|
+
const node = workflow.nodes.find((n) => n.id === output.nodeId);
|
|
223
|
+
if (!node) {
|
|
224
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: `输出节点 ${output.nodeId} 不存在` };
|
|
225
|
+
}
|
|
226
|
+
if (!node.enabled) {
|
|
227
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: `输出节点 ${output.nodeId} 必须 enabled` };
|
|
228
|
+
}
|
|
229
|
+
if (node.lane !== "main") {
|
|
230
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: `输出节点 ${output.nodeId} 必须是主线节点` };
|
|
231
|
+
}
|
|
232
|
+
if (node.branchScopeId) {
|
|
233
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: `输出节点 ${output.nodeId} 不能属于支线 scope` };
|
|
234
|
+
}
|
|
235
|
+
return { ok: true };
|
|
236
|
+
}
|
|
237
|
+
// mode === "mainline_last" — auto-derive unique mainline sink via reachability
|
|
238
|
+
const mainlineNodeIds = new Set(workflow.nodes
|
|
239
|
+
.filter((n) => n.enabled && n.lane === "main" && !n.branchScopeId && !n.routeSourceNodeId && !n.routeValue)
|
|
240
|
+
.map((n) => n.id));
|
|
241
|
+
if (mainlineNodeIds.size === 0) {
|
|
242
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: "没有可用的主线节点" };
|
|
243
|
+
}
|
|
244
|
+
// Build full adjacency (all nodes, all edges) for reachability DFS
|
|
245
|
+
const allNodeIds = new Set(workflow.nodes.map((n) => n.id));
|
|
246
|
+
const successors = new Map();
|
|
247
|
+
for (const id of allNodeIds)
|
|
248
|
+
successors.set(id, []);
|
|
249
|
+
for (const edge of workflow.edges) {
|
|
250
|
+
const list = successors.get(edge.from);
|
|
251
|
+
if (list)
|
|
252
|
+
list.push(edge.to);
|
|
253
|
+
}
|
|
254
|
+
// Build indegree/outdegree in full graph (used for orphan detection)
|
|
255
|
+
const indegree = new Map();
|
|
256
|
+
const outdegree = new Map();
|
|
257
|
+
for (const id of allNodeIds) {
|
|
258
|
+
indegree.set(id, 0);
|
|
259
|
+
outdegree.set(id, 0);
|
|
260
|
+
}
|
|
261
|
+
for (const edge of workflow.edges) {
|
|
262
|
+
outdegree.set(edge.from, (outdegree.get(edge.from) ?? 0) + 1);
|
|
263
|
+
indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1);
|
|
264
|
+
}
|
|
265
|
+
// Route nodes are routers, never endpoints
|
|
266
|
+
const routeNodeIds = new Set(workflow.nodes.filter((n) => n.routePolicy != null).map((n) => n.id));
|
|
267
|
+
// DFS from each mainline candidate: can it reach another mainline node?
|
|
268
|
+
const canReachMainline = new Set();
|
|
269
|
+
for (const nodeId of mainlineNodeIds) {
|
|
270
|
+
const visited = new Set();
|
|
271
|
+
const stack = [...(successors.get(nodeId) ?? [])];
|
|
272
|
+
while (stack.length > 0) {
|
|
273
|
+
const current = stack.pop();
|
|
274
|
+
if (visited.has(current))
|
|
275
|
+
continue;
|
|
276
|
+
visited.add(current);
|
|
277
|
+
if (mainlineNodeIds.has(current)) {
|
|
278
|
+
canReachMainline.add(nodeId);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
for (const next of successors.get(current) ?? []) {
|
|
282
|
+
if (!visited.has(next))
|
|
283
|
+
stack.push(next);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// A sink is a mainline node that:
|
|
288
|
+
// - cannot reach another mainline node (no downstream path)
|
|
289
|
+
// - is NOT a route node (routers forward to branches, not endpoints)
|
|
290
|
+
const sinkNodes = [...mainlineNodeIds].filter((id) => !canReachMainline.has(id) && !routeNodeIds.has(id));
|
|
291
|
+
// Orphans: disconnected nodes (no in/out edges at all) are not real sinks.
|
|
292
|
+
// But if ALL candidates are orphans, treat them as valid (single-node case).
|
|
293
|
+
const orphanIds = new Set([...mainlineNodeIds].filter((id) => (indegree.get(id) ?? 0) === 0 && (outdegree.get(id) ?? 0) === 0));
|
|
294
|
+
const allOrphans = orphanIds.size === mainlineNodeIds.size;
|
|
295
|
+
const effectiveSinks = allOrphans
|
|
296
|
+
? sinkNodes // all candidates are orphans, keep as-is
|
|
297
|
+
: sinkNodes.filter((id) => !orphanIds.has(id)); // exclude orphans
|
|
298
|
+
if (effectiveSinks.length === 0) {
|
|
299
|
+
return { ok: false, error: "invalid_workflow_output_config", detail: "无法推导唯一主线 sink 节点" };
|
|
300
|
+
}
|
|
301
|
+
if (effectiveSinks.length > 1) {
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
error: "invalid_workflow_output_config",
|
|
305
|
+
detail: `存在多个主线 sink 节点: ${effectiveSinks.join(", ")},请切换到 mode=explicit 并指定 nodeId`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return { ok: true };
|
|
309
|
+
};
|
|
310
|
+
exports.validateWorkflowOutputConfig = validateWorkflowOutputConfig;
|
|
311
|
+
const validateWorkflowDefinition = (workflow) => (0, exports.validateWorkflowGraph)(workflow);
|
|
312
|
+
exports.validateWorkflowDefinition = validateWorkflowDefinition;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWorkflowGraph = exports.workflowToTemplateNodes = void 0;
|
|
4
|
+
const template_mapper_1 = require("./workflow/template-mapper");
|
|
5
|
+
const branch_rules_1 = require("./workflow/branch-rules");
|
|
6
|
+
// Re-export for backward compatibility
|
|
7
|
+
var template_mapper_2 = require("./workflow/template-mapper");
|
|
8
|
+
Object.defineProperty(exports, "workflowToTemplateNodes", { enumerable: true, get: function () { return template_mapper_2.workflowToTemplateNodes; } });
|
|
9
|
+
const buildIndices = (workflow) => {
|
|
10
|
+
const nodeById = new Map();
|
|
11
|
+
const incomingEdgesByTarget = new Map();
|
|
12
|
+
const outgoingEdgesBySource = new Map();
|
|
13
|
+
for (const node of workflow.nodes) {
|
|
14
|
+
nodeById.set(node.id, node);
|
|
15
|
+
}
|
|
16
|
+
for (const edge of workflow.edges) {
|
|
17
|
+
const incoming = incomingEdgesByTarget.get(edge.to) ?? [];
|
|
18
|
+
incoming.push(edge);
|
|
19
|
+
incomingEdgesByTarget.set(edge.to, incoming);
|
|
20
|
+
const outgoing = outgoingEdgesBySource.get(edge.from) ?? [];
|
|
21
|
+
outgoing.push(edge);
|
|
22
|
+
outgoingEdgesBySource.set(edge.from, outgoing);
|
|
23
|
+
}
|
|
24
|
+
// 计算节点 branch scope:优先使用显式 branchScopeId,缺失时从 route 边推导。
|
|
25
|
+
// 将 nodes 和 groups 都纳入 scope 计算,确保 group 的 scope 被正确传播。
|
|
26
|
+
const explicitScopes = new Map();
|
|
27
|
+
for (const node of workflow.nodes) {
|
|
28
|
+
if (node.branchScopeId != null) {
|
|
29
|
+
explicitScopes.set(node.id, node.branchScopeId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const allEntities = [
|
|
33
|
+
...workflow.nodes.map((n) => ({ id: n.id })),
|
|
34
|
+
...workflow.groups.map((g) => ({ id: g.id })),
|
|
35
|
+
];
|
|
36
|
+
const nodeScopes = (0, branch_rules_1.computeNodeScopes)(allEntities, workflow.edges, explicitScopes);
|
|
37
|
+
// explicitScopeIds: 显式声明了非默认 merge 策略的节点(dependencyPolicy != "all"),
|
|
38
|
+
// 是多个分支的显式汇聚点,应接受来自不同 scope 的依赖边。
|
|
39
|
+
// 将其 scope 重置为 null,避免跨支线误判阻断合法的分支合并。
|
|
40
|
+
for (const node of workflow.nodes) {
|
|
41
|
+
if (node.dependencyPolicy && node.dependencyPolicy !== "all") {
|
|
42
|
+
nodeScopes.set(node.id, null);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const explicit = workflow.groups.filter((group) => group.type === "parallel" && group.members.length >= 2);
|
|
46
|
+
const seen = new Set(explicit.map((group) => group.id));
|
|
47
|
+
const inferredMembers = new Map();
|
|
48
|
+
for (const workflowNode of workflow.nodes) {
|
|
49
|
+
const groupId = workflowNode.parallelGroupId?.trim();
|
|
50
|
+
if (!groupId || seen.has(groupId))
|
|
51
|
+
continue;
|
|
52
|
+
const current = inferredMembers.get(groupId) ?? [];
|
|
53
|
+
current.push(workflowNode.id);
|
|
54
|
+
inferredMembers.set(groupId, current);
|
|
55
|
+
}
|
|
56
|
+
const inferred = [...inferredMembers.entries()]
|
|
57
|
+
.filter(([, members]) => members.length >= 2)
|
|
58
|
+
.map(([id, members]) => ({
|
|
59
|
+
id,
|
|
60
|
+
type: "parallel",
|
|
61
|
+
members,
|
|
62
|
+
joinPolicy: "all",
|
|
63
|
+
}));
|
|
64
|
+
const groups = [...explicit, ...inferred];
|
|
65
|
+
const groupById = new Map(groups.map((group) => [group.id, group]));
|
|
66
|
+
const parallelGroupByMemberNodeId = new Map();
|
|
67
|
+
for (const group of groups) {
|
|
68
|
+
for (const memberId of group.members) {
|
|
69
|
+
parallelGroupByMemberNodeId.set(memberId, group);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
nodeById,
|
|
74
|
+
incomingEdgesByTarget,
|
|
75
|
+
outgoingEdgesBySource,
|
|
76
|
+
groupById,
|
|
77
|
+
parallelGroupByMemberNodeId,
|
|
78
|
+
groups,
|
|
79
|
+
nodeScopes,
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const createWorkflowGraph = (initialWorkflow, initialTemplateNodes) => {
|
|
83
|
+
let workflow = initialWorkflow;
|
|
84
|
+
let templateNodes = initialTemplateNodes ?? (0, template_mapper_1.workflowToTemplateNodes)(initialWorkflow);
|
|
85
|
+
let indices = buildIndices(workflow);
|
|
86
|
+
const rebuild = () => {
|
|
87
|
+
indices = buildIndices(workflow);
|
|
88
|
+
};
|
|
89
|
+
const setWorkflow = (nextWorkflow) => {
|
|
90
|
+
workflow = nextWorkflow;
|
|
91
|
+
templateNodes = (0, template_mapper_1.workflowToTemplateNodes)(nextWorkflow);
|
|
92
|
+
rebuild();
|
|
93
|
+
};
|
|
94
|
+
const setTemplateNodes = (nextTemplateNodes) => {
|
|
95
|
+
templateNodes = nextTemplateNodes;
|
|
96
|
+
workflow = (0, template_mapper_1.mergeTemplateNodesIntoWorkflow)(workflow, nextTemplateNodes);
|
|
97
|
+
rebuild();
|
|
98
|
+
};
|
|
99
|
+
const getIncomingEdges = (targetId) => indices.incomingEdgesByTarget.get(targetId) ?? [];
|
|
100
|
+
const getOutgoingEdges = (sourceId) => indices.outgoingEdgesBySource.get(sourceId) ?? [];
|
|
101
|
+
const getWorkflowNodeById = (nodeId) => indices.nodeById.get(nodeId) ?? null;
|
|
102
|
+
const getWorkflowGroupById = (groupId) => indices.groupById.get(groupId) ?? null;
|
|
103
|
+
const getParallelGroupByMemberNodeId = (nodeId) => indices.parallelGroupByMemberNodeId.get(nodeId) ?? null;
|
|
104
|
+
const isWorkflowNodeEnabled = (nodeId) => getWorkflowNodeById(nodeId)?.enabled !== false;
|
|
105
|
+
const isGroupId = (id) => indices.groupById.has(id);
|
|
106
|
+
// Phase 2: 基于 scope 判断支线身份,替代入边形状推断。
|
|
107
|
+
// scope 为 null → 主线节点;scope 非 null → 支线节点。
|
|
108
|
+
const isBranchNode = (nodeId) => {
|
|
109
|
+
const scope = indices.nodeScopes.get(nodeId);
|
|
110
|
+
return scope != null;
|
|
111
|
+
};
|
|
112
|
+
// Phase 2: 基于 scope 判断跨支线边,替代入边形状推断。
|
|
113
|
+
// 旧版在存在 B1→B2 普通边时会因 B2 不再"纯支线"而漏判,新版不受此影响。
|
|
114
|
+
const isCrossBranchEdge = (edge) => (0, branch_rules_1.isCrossBranchEdgeByScope)(edge, indices.nodeScopes);
|
|
115
|
+
const getNodeScope = (nodeId) => indices.nodeScopes.get(nodeId) ?? null;
|
|
116
|
+
const getNodesWithWorkflowMeta = (nodes) => nodes.map((node) => {
|
|
117
|
+
const matched = getWorkflowNodeById(node.id);
|
|
118
|
+
return {
|
|
119
|
+
...node,
|
|
120
|
+
isMainline: matched?.isMainline ?? true,
|
|
121
|
+
lane: matched?.lane ?? "main",
|
|
122
|
+
parallelGroupId: matched?.parallelGroupId ?? null,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
const syncRunGroupsFromWorkflow = (run) => {
|
|
126
|
+
const current = new Map((run.groups ?? []).map((group) => [group.id, group]));
|
|
127
|
+
run.groups = indices.groups.map((group) => ({
|
|
128
|
+
id: group.id,
|
|
129
|
+
title: `并行组 ${group.id}`,
|
|
130
|
+
status: current.get(group.id)?.status ?? "blocked",
|
|
131
|
+
members: group.members,
|
|
132
|
+
joinPolicy: group.joinPolicy,
|
|
133
|
+
artifacts: current.get(group.id)?.artifacts ?? [],
|
|
134
|
+
startedAt: current.get(group.id)?.startedAt ?? null,
|
|
135
|
+
finishedAt: current.get(group.id)?.finishedAt ?? null,
|
|
136
|
+
lastError: current.get(group.id)?.lastError ?? null,
|
|
137
|
+
}));
|
|
138
|
+
if (!run.groupItemRuns)
|
|
139
|
+
run.groupItemRuns = [];
|
|
140
|
+
const groupIds = new Set(indices.groups.map((group) => group.id));
|
|
141
|
+
run.groupItemRuns = run.groupItemRuns.filter((item) => groupIds.has(item.groupId));
|
|
142
|
+
};
|
|
143
|
+
const getRunGroupMeta = (groupId, groups) => groups.find((group) => group.id === groupId) ?? null;
|
|
144
|
+
return {
|
|
145
|
+
getWorkflow: () => workflow,
|
|
146
|
+
setWorkflow,
|
|
147
|
+
getTemplateNodes: () => templateNodes,
|
|
148
|
+
setTemplateNodes,
|
|
149
|
+
getIndices: () => indices,
|
|
150
|
+
getIncomingEdges,
|
|
151
|
+
getOutgoingEdges,
|
|
152
|
+
getWorkflowNodeById,
|
|
153
|
+
getWorkflowGroupById,
|
|
154
|
+
getParallelGroupByMemberNodeId,
|
|
155
|
+
isWorkflowNodeEnabled,
|
|
156
|
+
isGroupId,
|
|
157
|
+
isBranchNode,
|
|
158
|
+
isCrossBranchEdge,
|
|
159
|
+
getNodeScope,
|
|
160
|
+
getNodesWithWorkflowMeta,
|
|
161
|
+
syncRunGroupsFromWorkflow,
|
|
162
|
+
getRunGroupMeta,
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
exports.createWorkflowGraph = createWorkflowGraph;
|