circuit_breaker-wf 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +52 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +116 -0
  6. data/LICENSE +21 -0
  7. data/README.md +324 -0
  8. data/examples/document/README.md +150 -0
  9. data/examples/document/document_assistant.rb +535 -0
  10. data/examples/document/document_rules.rb +60 -0
  11. data/examples/document/document_token.rb +83 -0
  12. data/examples/document/document_workflow.rb +114 -0
  13. data/examples/document/mock_executor.rb +80 -0
  14. data/lib/circuit_breaker/executors/README.md +664 -0
  15. data/lib/circuit_breaker/executors/agent_executor.rb +187 -0
  16. data/lib/circuit_breaker/executors/assistant_executor.rb +245 -0
  17. data/lib/circuit_breaker/executors/base_executor.rb +56 -0
  18. data/lib/circuit_breaker/executors/docker_executor.rb +56 -0
  19. data/lib/circuit_breaker/executors/dsl.rb +97 -0
  20. data/lib/circuit_breaker/executors/llm/memory.rb +82 -0
  21. data/lib/circuit_breaker/executors/llm/tools.rb +94 -0
  22. data/lib/circuit_breaker/executors/nats_executor.rb +230 -0
  23. data/lib/circuit_breaker/executors/serverless_executor.rb +25 -0
  24. data/lib/circuit_breaker/executors/step_executor.rb +47 -0
  25. data/lib/circuit_breaker/history.rb +81 -0
  26. data/lib/circuit_breaker/rules.rb +251 -0
  27. data/lib/circuit_breaker/templates/mermaid.html.erb +51 -0
  28. data/lib/circuit_breaker/templates/plantuml.html.erb +55 -0
  29. data/lib/circuit_breaker/token.rb +486 -0
  30. data/lib/circuit_breaker/visualizer.rb +173 -0
  31. data/lib/circuit_breaker/workflow_dsl.rb +359 -0
  32. data/lib/circuit_breaker.rb +236 -0
  33. data/workflow-editor/.gitignore +24 -0
  34. data/workflow-editor/README.md +106 -0
  35. data/workflow-editor/eslint.config.js +28 -0
  36. data/workflow-editor/index.html +13 -0
  37. data/workflow-editor/package-lock.json +6864 -0
  38. data/workflow-editor/package.json +50 -0
  39. data/workflow-editor/postcss.config.js +6 -0
  40. data/workflow-editor/public/vite.svg +1 -0
  41. data/workflow-editor/src/App.css +42 -0
  42. data/workflow-editor/src/App.tsx +365 -0
  43. data/workflow-editor/src/assets/react.svg +1 -0
  44. data/workflow-editor/src/components/AddNodeButton.tsx +68 -0
  45. data/workflow-editor/src/components/EdgeDetails.tsx +175 -0
  46. data/workflow-editor/src/components/NodeDetails.tsx +177 -0
  47. data/workflow-editor/src/components/ResizablePanel.tsx +74 -0
  48. data/workflow-editor/src/components/SaveButton.tsx +45 -0
  49. data/workflow-editor/src/config/change_workflow.yaml +59 -0
  50. data/workflow-editor/src/config/constants.ts +11 -0
  51. data/workflow-editor/src/config/flowConfig.ts +189 -0
  52. data/workflow-editor/src/config/uiConfig.ts +77 -0
  53. data/workflow-editor/src/config/workflow.yaml +58 -0
  54. data/workflow-editor/src/hooks/useKeyPress.ts +29 -0
  55. data/workflow-editor/src/index.css +34 -0
  56. data/workflow-editor/src/main.tsx +10 -0
  57. data/workflow-editor/src/server/saveWorkflow.ts +81 -0
  58. data/workflow-editor/src/utils/saveWorkflow.ts +92 -0
  59. data/workflow-editor/src/utils/workflowLoader.ts +26 -0
  60. data/workflow-editor/src/utils/workflowTransformer.ts +91 -0
  61. data/workflow-editor/src/vite-env.d.ts +1 -0
  62. data/workflow-editor/src/yaml.d.ts +4 -0
  63. data/workflow-editor/tailwind.config.js +15 -0
  64. data/workflow-editor/tsconfig.app.json +26 -0
  65. data/workflow-editor/tsconfig.json +7 -0
  66. data/workflow-editor/tsconfig.node.json +24 -0
  67. data/workflow-editor/vite.config.ts +8 -0
  68. metadata +267 -0
@@ -0,0 +1,189 @@
1
+ import { Edge, MarkerType } from 'reactflow';
2
+ import { WORKFLOW_FILE } from './constants';
3
+ import workflowConfig from './workflow.yaml';
4
+ import dagre from 'dagre';
5
+
6
+ // Load and parse the YAML file
7
+ const config = workflowConfig;
8
+
9
+ // Default styles
10
+ const defaultNodeStyle: NodeStyle = {
11
+ padding: 10,
12
+ borderRadius: 8,
13
+ border: '1px solid #ddd',
14
+ backgroundColor: '#fff',
15
+ width: 150,
16
+ fontSize: 14,
17
+ color: '#333',
18
+ fontWeight: 500,
19
+ transition: '0.2s',
20
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
21
+ };
22
+
23
+ const defaultEdgeStyle: EdgeStyle = {
24
+ stroke: '#b1b1b7',
25
+ strokeWidth: 2,
26
+ labelBgPadding: [8, 4],
27
+ labelBgStyle: {
28
+ fill: '#fff',
29
+ stroke: '#e2e2e2',
30
+ strokeWidth: 1,
31
+ borderRadius: 4
32
+ },
33
+ labelStyle: {
34
+ fontSize: 12,
35
+ fill: '#777',
36
+ fontWeight: 500
37
+ },
38
+ selected: {
39
+ stroke: '#3b82f6',
40
+ strokeWidth: 3,
41
+ markerEnd: {
42
+ type: MarkerType.ArrowClosed,
43
+ width: 20,
44
+ height: 20,
45
+ color: '#3b82f6'
46
+ }
47
+ },
48
+ markerEnd: {
49
+ type: MarkerType.ArrowClosed,
50
+ width: 20,
51
+ height: 20,
52
+ color: '#b1b1b7'
53
+ }
54
+ };
55
+
56
+ // Define types for our configuration
57
+ interface NodeStyle {
58
+ padding: number;
59
+ borderRadius: number;
60
+ border: string;
61
+ backgroundColor: string;
62
+ width: number;
63
+ fontSize: number;
64
+ color: string;
65
+ fontWeight: number;
66
+ transition: string;
67
+ boxShadow: string;
68
+ }
69
+
70
+ interface EdgeStyle {
71
+ stroke: string;
72
+ strokeWidth: number;
73
+ labelBgPadding: number[];
74
+ labelBgStyle: {
75
+ fill: string;
76
+ stroke: string;
77
+ strokeWidth: number;
78
+ borderRadius: number;
79
+ };
80
+ labelStyle: {
81
+ fontSize: number;
82
+ fill: string;
83
+ fontWeight: number;
84
+ };
85
+ selected: {
86
+ stroke: string;
87
+ strokeWidth: number;
88
+ markerEnd?: {
89
+ type: string;
90
+ width: number;
91
+ height: number;
92
+ color: string;
93
+ };
94
+ };
95
+ markerEnd?: {
96
+ type: string;
97
+ width: number;
98
+ height: number;
99
+ color: string;
100
+ };
101
+ }
102
+
103
+ // Export the styles
104
+ export const nodeStyles = defaultNodeStyle;
105
+ export const edgeStyles = {
106
+ ...defaultEdgeStyle,
107
+ startLabelStyle: {
108
+ ...defaultEdgeStyle.labelStyle,
109
+ distance: 10
110
+ }
111
+ };
112
+
113
+ // Add selected node styles
114
+ export const selectedNodeStyles = {
115
+ ...nodeStyles,
116
+ border: '2px solid #4a9eff',
117
+ boxShadow: '0 4px 12px rgba(74, 158, 255, 0.2)'
118
+ };
119
+
120
+ // Helper function to capitalize and format state names
121
+ const formatLabel = (state: string) => {
122
+ return state.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
123
+ };
124
+
125
+ // Generate nodes from states
126
+ const states = [...(config.places.states || []), ...(config.places.special_states || [])];
127
+ const nodes = states.map((state) => ({
128
+ id: state.replace(' ', '_'), // Update state ID to replace spaces with underscores
129
+ type: state === 'backlog' ? 'input' : state === 'done' ? 'output' : 'default',
130
+ data: {
131
+ label: formatLabel(state),
132
+ description: `${formatLabel(state)} state`
133
+ },
134
+ position: { x: 0, y: 0 }, // Initial position will be set by dagre
135
+ style: nodeStyles
136
+ }));
137
+
138
+ // Generate edges from transitions
139
+ const allTransitions = [
140
+ ...(config.transitions.regular || []),
141
+ ...(config.transitions.special || [])
142
+ ];
143
+
144
+ const edges: Edge[] = allTransitions.map((transition, index) => ({
145
+ id: `e${index}`,
146
+ source: transition.from.replace(' ', '_'), // Update source state ID to replace spaces with underscores
147
+ target: transition.to.replace(' ', '_'), // Update target state ID to replace spaces with underscores
148
+ label: formatLabel(transition.name),
149
+ style: edgeStyles,
150
+ labelStyle: edgeStyles.labelStyle,
151
+ data: {
152
+ requirements: transition.requires || []
153
+ }
154
+ }));
155
+
156
+ // Create a new dagre graph
157
+ const g = new dagre.graphlib.Graph();
158
+ g.setGraph({
159
+ rankdir: 'TB', // Top to Bottom direction
160
+ nodesep: 100, // Horizontal separation between nodes
161
+ ranksep: 150 // Vertical separation between ranks
162
+ });
163
+ g.setDefaultEdgeLabel(() => ({}));
164
+
165
+ // Add nodes to dagre
166
+ nodes.forEach((node) => {
167
+ g.setNode(node.id, { width: 150, height: 50 });
168
+ });
169
+
170
+ // Add edges to dagre
171
+ edges.forEach((edge) => {
172
+ g.setEdge(edge.source, edge.target);
173
+ });
174
+
175
+ // Calculate layout
176
+ dagre.layout(g);
177
+
178
+ // Apply layout to nodes
179
+ nodes.forEach((node) => {
180
+ const nodeWithPosition = g.node(node.id);
181
+ node.position = {
182
+ x: nodeWithPosition.x - 75, // Center node by subtracting half the width
183
+ y: nodeWithPosition.y - 25 // Center node by subtracting half the height
184
+ };
185
+ });
186
+
187
+ export const initialNodes = nodes;
188
+ export const initialEdges = edges;
189
+ export const defaultViewport = { x: 0, y: 0, zoom: 1.5 }; // 150% zoom
@@ -0,0 +1,77 @@
1
+ import { Node, Edge } from 'reactflow';
2
+
3
+ export const defaultNodeStyle = {
4
+ padding: 20,
5
+ borderRadius: 8,
6
+ border: '1px solid #e1e4e8',
7
+ backgroundColor: '#ffffff',
8
+ width: 180,
9
+ fontSize: 14,
10
+ };
11
+
12
+ export const defaultNodePosition = {
13
+ x: 250,
14
+ y: 100,
15
+ };
16
+
17
+ export const getNodePosition = (index: number) => ({
18
+ x: defaultNodePosition.x,
19
+ y: index * defaultNodePosition.y,
20
+ });
21
+
22
+ export interface WorkflowUIConfig {
23
+ nodeSpacing: number;
24
+ defaultLayout: 'vertical' | 'horizontal';
25
+ stateStyles: {
26
+ [key: string]: {
27
+ backgroundColor?: string;
28
+ borderColor?: string;
29
+ textColor?: string;
30
+ };
31
+ };
32
+ }
33
+
34
+ export const defaultUIConfig: WorkflowUIConfig = {
35
+ nodeSpacing: 100,
36
+ defaultLayout: 'vertical',
37
+ stateStyles: {
38
+ backlog: {
39
+ backgroundColor: '#f3f4f6',
40
+ },
41
+ blocked: {
42
+ backgroundColor: '#fee2e2',
43
+ borderColor: '#ef4444',
44
+ },
45
+ done: {
46
+ backgroundColor: '#dcfce7',
47
+ },
48
+ },
49
+ };
50
+
51
+ export const createNodesFromStates = (states: string[], specialStates: string[] = []): Node[] => {
52
+ const allStates = [...states, ...specialStates];
53
+ return allStates.map((state, index) => ({
54
+ id: state,
55
+ type: index === 0 ? 'input' : index === states.length - 1 ? 'output' : 'default',
56
+ data: {
57
+ label: state.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '),
58
+ description: '',
59
+ },
60
+ position: getNodePosition(index),
61
+ style: {
62
+ ...defaultNodeStyle,
63
+ ...(defaultUIConfig.stateStyles[state] || {}),
64
+ },
65
+ }));
66
+ };
67
+
68
+ export const createEdgesFromTransitions = (transitions: any[]): Edge[] => {
69
+ return transitions.map((transition, index) => ({
70
+ id: `edge-${transition.from}-${transition.to}`,
71
+ source: transition.from,
72
+ target: transition.to,
73
+ label: transition.name.split('_').map(word =>
74
+ word.charAt(0).toUpperCase() + word.slice(1)
75
+ ).join(' '),
76
+ }));
77
+ };
@@ -0,0 +1,58 @@
1
+ object_type: Issue
2
+ places:
3
+ states:
4
+ - backlog
5
+ - sprint_planning
6
+ - assign_issue
7
+ - in_progress
8
+ - in_review
9
+ - testing
10
+ - done
11
+ special_states:
12
+ - blocked
13
+ transitions:
14
+ regular:
15
+ - name: move_to_sprint
16
+ from: backlog
17
+ to: sprint_planning
18
+ requires:
19
+ - description
20
+ - priority
21
+ - name: plan_issue
22
+ from: sprint_planning
23
+ to: assign_issue
24
+ requires:
25
+ - story_points
26
+ - sprint
27
+ - name: start_coding
28
+ from: assign_issue
29
+ to: in_progress
30
+ requires:
31
+ - assignee
32
+ - name: submit_for_review
33
+ from: in_progress
34
+ to: in_review
35
+ requires:
36
+ - pull_request_url
37
+ - name: approve_review
38
+ from: in_review
39
+ to: testing
40
+ requires:
41
+ - review_approvals
42
+ - test_coverage
43
+ - name: pass_testing
44
+ from: testing
45
+ to: done
46
+ requires:
47
+ - qa_approved
48
+ - deployment_approved
49
+ - name: reject_review
50
+ from: in_review
51
+ to: in_progress
52
+ requires:
53
+ - review_comments
54
+ - name: fail_testing
55
+ from: testing
56
+ to: in_progress
57
+ requires:
58
+ - test_failure_reason
@@ -0,0 +1,29 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export const useKeyPress = (targetKeys: string[]) => {
4
+ const [isPressed, setIsPressed] = useState(false);
5
+
6
+ useEffect(() => {
7
+ const downHandler = (event: KeyboardEvent) => {
8
+ if (targetKeys.includes(event.key)) {
9
+ setIsPressed(true);
10
+ }
11
+ };
12
+
13
+ const upHandler = (event: KeyboardEvent) => {
14
+ if (targetKeys.includes(event.key)) {
15
+ setIsPressed(false);
16
+ }
17
+ };
18
+
19
+ window.addEventListener('keydown', downHandler);
20
+ window.addEventListener('keyup', upHandler);
21
+
22
+ return () => {
23
+ window.removeEventListener('keydown', downHandler);
24
+ window.removeEventListener('keyup', upHandler);
25
+ };
26
+ }, [targetKeys]);
27
+
28
+ return isPressed;
29
+ };
@@ -0,0 +1,34 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
7
+ line-height: 1.5;
8
+ font-weight: 400;
9
+ color-scheme: light;
10
+ background-color: #f8fafc;
11
+ }
12
+
13
+ body {
14
+ margin: 0;
15
+ min-width: 320px;
16
+ min-height: 100vh;
17
+ }
18
+
19
+ .react-flow__node {
20
+ font-family: Inter, system-ui, -apple-system, sans-serif;
21
+ }
22
+
23
+ .react-flow__controls {
24
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
25
+ border-radius: 8px;
26
+ overflow: hidden;
27
+ }
28
+
29
+ .react-flow__controls button {
30
+ background: #ffffff;
31
+ border: 1px solid #e5e7eb;
32
+ color: #4b5563;
33
+ padding: 4px;
34
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -0,0 +1,81 @@
1
+ import express from 'express';
2
+ import { dump, load } from 'js-yaml';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import cors from 'cors';
6
+ import { fileURLToPath } from 'url';
7
+ import { WORKFLOW_PATH, YAML_HEADER } from '../config/constants';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const app = express();
13
+ app.use(express.json());
14
+ app.use(cors());
15
+
16
+ const PORT = 3001;
17
+
18
+ // Get current workflow
19
+ app.get('/api/get-workflow', async (req, res) => {
20
+ try {
21
+ const filePath = path.resolve(__dirname, '../../', WORKFLOW_PATH);
22
+ const yamlContent = fs.readFileSync(filePath, 'utf8');
23
+ const workflowData = load(yamlContent);
24
+ res.json(workflowData);
25
+ } catch (error) {
26
+ console.error('Error reading workflow:', error);
27
+ res.status(500).json({
28
+ success: false,
29
+ message: 'Error reading workflow',
30
+ error: error.message
31
+ });
32
+ }
33
+ });
34
+
35
+ app.post('/api/save-workflow', async (req, res) => {
36
+ try {
37
+ console.log('Received save workflow request');
38
+
39
+ if (!req.body) {
40
+ console.error('No workflow data provided');
41
+ throw new Error('No workflow data provided');
42
+ }
43
+
44
+ const workflowData = req.body;
45
+
46
+ // Convert the workflow data to YAML
47
+ const yamlContent = dump(workflowData, {
48
+ indent: 2,
49
+ lineWidth: -1, // Don't wrap lines
50
+ noRefs: true, // Don't use aliases
51
+ });
52
+
53
+ // Add YAML header and save
54
+ const fullContent = YAML_HEADER + yamlContent;
55
+ const filePath = path.resolve(__dirname, '../../', WORKFLOW_PATH);
56
+ fs.writeFileSync(filePath, fullContent, 'utf8');
57
+
58
+ console.log('Workflow saved successfully');
59
+ res.json({ success: true, message: 'Workflow saved successfully' });
60
+ } catch (error) {
61
+ console.error('Error saving workflow:', error);
62
+ res.status(500).json({
63
+ success: false,
64
+ message: 'Error saving workflow',
65
+ error: error.message
66
+ });
67
+ }
68
+ });
69
+
70
+ const server = app.listen(PORT, () => {
71
+ console.log(`Server running on http://localhost:${PORT}`);
72
+ });
73
+
74
+ // Handle graceful shutdown
75
+ process.on('SIGINT', () => {
76
+ console.log('\nGracefully shutting down server...');
77
+ server.close(() => {
78
+ console.log('Server shutdown complete.');
79
+ process.exit(0);
80
+ });
81
+ });
@@ -0,0 +1,92 @@
1
+ import { Node, Edge } from 'reactflow';
2
+
3
+ interface WorkflowData {
4
+ object_type: string;
5
+ places: {
6
+ states: string[];
7
+ special_states: string[];
8
+ };
9
+ transitions: {
10
+ regular: {
11
+ name: string;
12
+ from: string;
13
+ to: string;
14
+ requires?: string[];
15
+ }[];
16
+ special?: {
17
+ name: string;
18
+ from: string;
19
+ to: string;
20
+ requires?: string[];
21
+ }[];
22
+ };
23
+ }
24
+
25
+ export const saveWorkflow = async (nodes: Node[], edges: Edge[]) => {
26
+ console.log('saveWorkflow called with nodes:', nodes);
27
+
28
+ // Create a map of node IDs to their current labels
29
+ const nodeIdToLabel = new Map(nodes.map(node => [
30
+ node.id,
31
+ node.data.label.toLowerCase().replace(/\s+/g, '_')
32
+ ]));
33
+
34
+ // Get regular states (just the IDs)
35
+ const states = nodes
36
+ .filter(node => node.type !== 'special' && node.id !== 'blocked')
37
+ .map(node => node.data.label.toLowerCase().replace(/\s+/g, '_'));
38
+
39
+ // Get special states (just the IDs)
40
+ const specialStates = nodes
41
+ .filter(node => node.type === 'special' || node.id === 'blocked')
42
+ .map(node => node.data.label.toLowerCase().replace(/\s+/g, '_'));
43
+
44
+ // Convert edges to transitions, using the current node labels
45
+ const transitions = edges.map(edge => ({
46
+ name: (edge.startLabel || 'transition').toLowerCase().replace(/\s+/g, '_'),
47
+ from: nodeIdToLabel.get(edge.source) || edge.source,
48
+ to: nodeIdToLabel.get(edge.target) || edge.target,
49
+ requires: edge.data?.requirements || []
50
+ }));
51
+
52
+ const workflowData: WorkflowData = {
53
+ object_type: 'Issue',
54
+ places: {
55
+ states,
56
+ special_states: specialStates
57
+ },
58
+ transitions: {
59
+ regular: transitions
60
+ }
61
+ };
62
+
63
+ try {
64
+ const response = await fetch('http://localhost:3001/api/save-workflow', {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ },
69
+ body: JSON.stringify(workflowData),
70
+ });
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`HTTP error! status: ${response.status}`);
74
+ }
75
+
76
+ const result = await response.json();
77
+ console.log('Save successful:', result);
78
+
79
+ // After successful save, update the local workflow file
80
+ const workflowResponse = await fetch('http://localhost:3001/api/get-workflow');
81
+ if (workflowResponse.ok) {
82
+ const updatedWorkflow = await workflowResponse.json();
83
+ // You can dispatch an event or use a callback here to update the graph
84
+ window.dispatchEvent(new CustomEvent('workflowUpdated', { detail: updatedWorkflow }));
85
+ }
86
+
87
+ return result.success;
88
+ } catch (error) {
89
+ console.error('Error saving workflow:', error);
90
+ throw error;
91
+ }
92
+ };
@@ -0,0 +1,26 @@
1
+ import { load as yamlLoad, dump as yamlDump } from 'js-yaml';
2
+ import { WorkflowDSL } from './workflowTransformer';
3
+ import { UIWorkflow } from './workflowTransformer';
4
+ import { transformDSLToUI, transformUIToDSL } from './workflowTransformer';
5
+
6
+ export const loadWorkflow = async (path: string): Promise<UIWorkflow> => {
7
+ try {
8
+ const response = await fetch(path);
9
+ const yamlContent = await response.text();
10
+ const dsl = yamlLoad(yamlContent) as WorkflowDSL;
11
+ return transformDSLToUI(dsl);
12
+ } catch (error) {
13
+ console.error('Error loading workflow:', error);
14
+ throw error;
15
+ }
16
+ };
17
+
18
+ export const saveWorkflow = async (workflow: UIWorkflow): Promise<string> => {
19
+ try {
20
+ const dsl = transformUIToDSL(workflow);
21
+ return yamlDump(dsl);
22
+ } catch (error) {
23
+ console.error('Error saving workflow:', error);
24
+ throw error;
25
+ }
26
+ };
@@ -0,0 +1,91 @@
1
+ import { Node, Edge } from 'reactflow';
2
+ import { createNodesFromStates, createEdgesFromTransitions } from '../config/uiConfig';
3
+
4
+ export interface WorkflowDSL {
5
+ object_type: string;
6
+ places: {
7
+ states: string[];
8
+ special_states?: string[];
9
+ };
10
+ transitions: {
11
+ regular: Array<{
12
+ name: string;
13
+ from: string;
14
+ to: string;
15
+ requires?: string[];
16
+ }>;
17
+ blocking?: Array<{
18
+ name: string;
19
+ from: string | string[];
20
+ to: string | string[];
21
+ requires?: string[];
22
+ }>;
23
+ };
24
+ validations?: Array<{
25
+ state: string;
26
+ conditions: Array<{
27
+ field: string;
28
+ required: boolean;
29
+ }>;
30
+ }>;
31
+ }
32
+
33
+ export interface UIWorkflow {
34
+ nodes: Node[];
35
+ edges: Edge[];
36
+ }
37
+
38
+ export const transformDSLToUI = (dsl: WorkflowDSL): UIWorkflow => {
39
+ const nodes = createNodesFromStates(
40
+ dsl.places.states,
41
+ dsl.places.special_states || []
42
+ );
43
+
44
+ const allTransitions = [
45
+ ...dsl.transitions.regular,
46
+ ...(dsl.transitions.blocking || []).map(blocking => {
47
+ const fromStates = Array.isArray(blocking.from) ? blocking.from : [blocking.from];
48
+ const toStates = Array.isArray(blocking.to) ? blocking.to : [blocking.to];
49
+
50
+ return fromStates.flatMap(from =>
51
+ toStates.map(to => ({
52
+ name: blocking.name,
53
+ from,
54
+ to,
55
+ requires: blocking.requires
56
+ }))
57
+ );
58
+ }).flat()
59
+ ];
60
+
61
+ const edges = createEdgesFromTransitions(allTransitions);
62
+
63
+ return { nodes, edges };
64
+ };
65
+
66
+ export const transformUIToDSL = (ui: UIWorkflow): WorkflowDSL => {
67
+ const states = ui.nodes
68
+ .filter(node => node.type !== 'special')
69
+ .map(node => node.id);
70
+
71
+ const special_states = ui.nodes
72
+ .filter(node => node.type === 'special')
73
+ .map(node => node.id);
74
+
75
+ const transitions = ui.edges.map(edge => ({
76
+ name: edge.label?.toLowerCase().replace(/\s+/g, '_') || '',
77
+ from: edge.source,
78
+ to: edge.target,
79
+ }));
80
+
81
+ return {
82
+ object_type: 'Issue',
83
+ places: {
84
+ states,
85
+ special_states: special_states.length > 0 ? special_states : undefined,
86
+ },
87
+ transitions: {
88
+ regular: transitions,
89
+ },
90
+ };
91
+ };
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,4 @@
1
+ declare module '*.yaml' {
2
+ const content: any;
3
+ export default content;
4
+ }