@vsirotin/ts-stop 1.13.1 → 2.4.2

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 (125) hide show
  1. package/README.md +20 -5
  2. package/ai/skills/sfsm-compare-json-uml-diagram/SKILL.md +62 -0
  3. package/ai/skills/sfsm-json-to-uml-diagram/SKILL.md +50 -0
  4. package/ai/skills/sfsm-uml-diagram-to-json/SKILL.md +49 -0
  5. package/lib/esm/index.js +2 -9
  6. package/lib/esm/sfsm/CommandReceiver.js +17 -0
  7. package/lib/esm/sfsm/ControllerHub.js +82 -0
  8. package/lib/esm/sfsm/FaResolver.js +24 -3
  9. package/lib/esm/sfsm/Sfsm.js +48 -7
  10. package/lib/esm/sfsm/SignalSender.js +27 -0
  11. package/lib/esm/sfsm/index.js +3 -1
  12. package/lib/index.d.ts +1 -9
  13. package/lib/index.d.ts.map +1 -1
  14. package/lib/index.js +2 -9
  15. package/lib/index.js.map +1 -1
  16. package/lib/sfsm/CommandReceiver.d.ts +23 -0
  17. package/lib/sfsm/CommandReceiver.d.ts.map +1 -0
  18. package/lib/sfsm/CommandReceiver.js +22 -0
  19. package/lib/sfsm/CommandReceiver.js.map +1 -0
  20. package/lib/sfsm/ControllerHub.d.ts +56 -0
  21. package/lib/sfsm/ControllerHub.d.ts.map +1 -0
  22. package/lib/sfsm/ControllerHub.js +87 -0
  23. package/lib/sfsm/ControllerHub.js.map +1 -0
  24. package/lib/sfsm/FaResolver.d.ts +14 -0
  25. package/lib/sfsm/FaResolver.d.ts.map +1 -1
  26. package/lib/sfsm/FaResolver.js +24 -3
  27. package/lib/sfsm/FaResolver.js.map +1 -1
  28. package/lib/sfsm/Sfsm.d.ts +26 -0
  29. package/lib/sfsm/Sfsm.d.ts.map +1 -1
  30. package/lib/sfsm/Sfsm.js +48 -7
  31. package/lib/sfsm/Sfsm.js.map +1 -1
  32. package/lib/sfsm/SignalSender.d.ts +24 -0
  33. package/lib/sfsm/SignalSender.d.ts.map +1 -0
  34. package/lib/sfsm/SignalSender.js +32 -0
  35. package/lib/sfsm/SignalSender.js.map +1 -0
  36. package/lib/sfsm/index.d.ts +3 -1
  37. package/lib/sfsm/index.d.ts.map +1 -1
  38. package/lib/sfsm/index.js +7 -3
  39. package/lib/sfsm/index.js.map +1 -1
  40. package/lib/sfsm/interfaces.d.ts +1 -1
  41. package/lib/sfsm/types.d.ts +21 -0
  42. package/lib/sfsm/types.d.ts.map +1 -1
  43. package/package.json +12 -9
  44. package/scripts/compare-compact-jsons.js +146 -0
  45. package/scripts/drawio-to-json.js +149 -0
  46. package/scripts/json-to-drawio-old.js +311 -0
  47. package/scripts/json-to-drawio.js +309 -0
  48. package/scripts/merge-fas.js +0 -0
  49. package/scripts/reduce-fa.js +0 -0
  50. package/scripts/update-fa.js +0 -0
  51. package/tutorial/01-finite-state-machine.md +223 -0
  52. package/tutorial/02-stacked-finite-state-machine.md +32 -0
  53. package/tutorial/03-advanced-themes.md +110 -0
  54. package/tutorial/04-tools.md +164 -0
  55. package/tutorial/images/Turnstile-img.png +0 -0
  56. package/tutorial/images/TurnstileBaseFA.png +0 -0
  57. package/LICENSE +0 -201
  58. package/LICENSE-COMMERCIAL.md +0 -41
  59. package/lib/DefaultState.d.ts +0 -9
  60. package/lib/DefaultState.d.ts.map +0 -1
  61. package/lib/DefaultState.js +0 -15
  62. package/lib/DefaultState.js.map +0 -1
  63. package/lib/FiniteStateMachine.d.ts +0 -227
  64. package/lib/FiniteStateMachine.d.ts.map +0 -1
  65. package/lib/FiniteStateMachine.js +0 -410
  66. package/lib/FiniteStateMachine.js.map +0 -1
  67. package/lib/IStateWithActions.d.ts +0 -18
  68. package/lib/IStateWithActions.d.ts.map +0 -1
  69. package/lib/IStateWithActions.js +0 -3
  70. package/lib/IStateWithActions.js.map +0 -1
  71. package/lib/IStateWithOutputSignal.d.ts +0 -4
  72. package/lib/IStateWithOutputSignal.d.ts.map +0 -1
  73. package/lib/IStateWithOutputSignal.js +0 -3
  74. package/lib/IStateWithOutputSignal.js.map +0 -1
  75. package/lib/MatrixBasedStateMachine.d.ts +0 -108
  76. package/lib/MatrixBasedStateMachine.d.ts.map +0 -1
  77. package/lib/MatrixBasedStateMachine.js +0 -132
  78. package/lib/MatrixBasedStateMachine.js.map +0 -1
  79. package/lib/TransitionMatrix.d.ts +0 -61
  80. package/lib/TransitionMatrix.d.ts.map +0 -1
  81. package/lib/TransitionMatrix.js +0 -104
  82. package/lib/TransitionMatrix.js.map +0 -1
  83. package/lib/esm/DefaultState.js +0 -10
  84. package/lib/esm/FiniteStateMachine.js +0 -405
  85. package/lib/esm/IStateWithActions.js +0 -1
  86. package/lib/esm/IStateWithOutputSignal.js +0 -1
  87. package/lib/esm/MatrixBasedStateMachine.js +0 -127
  88. package/lib/esm/TransitionMatrix.js +0 -98
  89. package/lib/esm/fa/DefaultState.js +0 -10
  90. package/lib/esm/fa/FiniteStateMachine.js +0 -405
  91. package/lib/esm/fa/IStateWithActions.js +0 -1
  92. package/lib/esm/fa/IStateWithOutputSignal.js +0 -1
  93. package/lib/esm/fa/MatrixBasedStateMachine.js +0 -127
  94. package/lib/esm/fa/TransitionMatrix.js +0 -98
  95. package/lib/esm/sfsm/ExternalWorldHub.js +0 -80
  96. package/lib/esm/sfsm/index.browser.js +0 -5
  97. package/lib/fa/DefaultState.d.ts +0 -9
  98. package/lib/fa/DefaultState.d.ts.map +0 -1
  99. package/lib/fa/DefaultState.js +0 -15
  100. package/lib/fa/DefaultState.js.map +0 -1
  101. package/lib/fa/FiniteStateMachine.d.ts +0 -227
  102. package/lib/fa/FiniteStateMachine.d.ts.map +0 -1
  103. package/lib/fa/FiniteStateMachine.js +0 -410
  104. package/lib/fa/FiniteStateMachine.js.map +0 -1
  105. package/lib/fa/IStateWithActions.d.ts +0 -18
  106. package/lib/fa/IStateWithActions.d.ts.map +0 -1
  107. package/lib/fa/IStateWithActions.js +0 -3
  108. package/lib/fa/IStateWithActions.js.map +0 -1
  109. package/lib/fa/IStateWithOutputSignal.d.ts +0 -4
  110. package/lib/fa/IStateWithOutputSignal.d.ts.map +0 -1
  111. package/lib/fa/IStateWithOutputSignal.js +0 -3
  112. package/lib/fa/IStateWithOutputSignal.js.map +0 -1
  113. package/lib/fa/MatrixBasedStateMachine.d.ts +0 -108
  114. package/lib/fa/MatrixBasedStateMachine.d.ts.map +0 -1
  115. package/lib/fa/MatrixBasedStateMachine.js +0 -132
  116. package/lib/fa/MatrixBasedStateMachine.js.map +0 -1
  117. package/lib/fa/TransitionMatrix.d.ts +0 -61
  118. package/lib/fa/TransitionMatrix.d.ts.map +0 -1
  119. package/lib/fa/TransitionMatrix.js +0 -104
  120. package/lib/fa/TransitionMatrix.js.map +0 -1
  121. package/lib/sfsm/ExternalWorldHub.d.ts +0 -52
  122. package/lib/sfsm/ExternalWorldHub.d.ts.map +0 -1
  123. package/lib/sfsm/ExternalWorldHub.js +0 -85
  124. package/lib/sfsm/ExternalWorldHub.js.map +0 -1
  125. package/lib/sfsm/index.browser.d.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vsirotin/ts-stop",
3
- "version": "1.13.1",
3
+ "version": "2.4.2",
4
4
  "description": "State Oriented Programming library for TypeScript",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -13,20 +13,18 @@
13
13
  },
14
14
  "./sfsm": {
15
15
  "types": "./lib/sfsm/index.d.ts",
16
- "browser": "./lib/esm/sfsm/index.browser.js",
17
16
  "import": "./lib/esm/sfsm/index.js",
18
17
  "require": "./lib/sfsm/index.js"
19
18
  }
20
19
  },
21
20
  "files": [
22
21
  "lib",
23
- "scripts/reduce-fa.js",
24
- "scripts/merge-fas.js",
25
- "scripts/merge-fas-from-dir.js",
26
- "scripts/update-fa.js",
27
- "README.md",
28
- "LICENSE",
29
- "LICENSE-COMMERCIAL.md"
22
+ "scripts",
23
+ "ai/skills",
24
+ "tutorial",
25
+ "LICENSE-COMMERCIAL.md",
26
+ "LICENSE-PUBLIC.md",
27
+ "release-notes.md"
30
28
  ],
31
29
  "bin": {
32
30
  "reduce-fa": "./scripts/reduce-fa.js",
@@ -60,6 +58,7 @@
60
58
  "@types/jest": "^29.5.14",
61
59
  "@types/js-yaml": "^4.0.9",
62
60
  "@types/mocha": "^10.0.10",
61
+ "@types/node": "^26.1.1",
63
62
  "jest": "^29.5.0",
64
63
  "js-yaml": "^4.1.1",
65
64
  "ts-jest": "^29.1.0",
@@ -67,5 +66,9 @@
67
66
  },
68
67
  "publishConfig": {
69
68
  "access": "public"
69
+ },
70
+ "dependencies": {
71
+ "elkjs": "^0.12.0",
72
+ "xml2js": "^0.6.2"
70
73
  }
71
74
  }
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Compare two compact SFSM JSON files
4
+ * Usage: node compare-compact-jsons.js <json-a> <json-b>
5
+ */
6
+
7
+ const fs = require('fs');
8
+
9
+ // ============ MAIN ============
10
+ function main() {
11
+ const [jsonAPath, jsonBPath] = process.argv.slice(2);
12
+
13
+ if (!jsonAPath || !jsonBPath) {
14
+ console.error('Usage: node compare-compact-jsons.js <json-a> <json-b>');
15
+ process.exit(1);
16
+ }
17
+
18
+ try {
19
+ const jsonA = JSON.parse(fs.readFileSync(jsonAPath, 'utf-8'));
20
+ const jsonB = JSON.parse(fs.readFileSync(jsonBPath, 'utf-8'));
21
+
22
+ console.log('\n======== SFSM COMPACT JSON COMPARISON ========\n');
23
+ console.log(`File A: ${jsonAPath}`);
24
+ console.log(`File B: ${jsonBPath}\n`);
25
+
26
+ compareJsons(jsonA, jsonB);
27
+
28
+ } catch (err) {
29
+ console.error('Error:', err.message);
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ // ============ COMPARISON LOGIC ============
35
+
36
+ function compareJsons(jsonA, jsonB) {
37
+ const fasA = Object.keys(jsonA);
38
+ const fasB = Object.keys(jsonB);
39
+
40
+ console.log('--- FA NAMES ---');
41
+ console.log(`A: [${fasA.join(', ')}] (${fasA.length} FAs)`);
42
+ console.log(`B: [${fasB.join(', ')}] (${fasB.length} FAs)`);
43
+
44
+ const missingInB = fasA.filter(fa => !fasB.includes(fa));
45
+ const missingInA = fasB.filter(fa => !fasA.includes(fa));
46
+
47
+ if (missingInB.length > 0) {
48
+ console.log(` ❌ Missing in B: ${missingInB.join(', ')}`);
49
+ }
50
+ if (missingInA.length > 0) {
51
+ console.log(` ❌ Missing in A: ${missingInA.join(', ')}`);
52
+ }
53
+
54
+ const commonFas = fasA.filter(fa => fasB.includes(fa));
55
+ if (commonFas.length === fasA.length && commonFas.length === fasB.length) {
56
+ console.log(` ✓ All FAs match\n`);
57
+ } else {
58
+ console.log();
59
+ }
60
+
61
+ // Compare transitions per FA
62
+ console.log('--- TRANSITIONS PER FA ---');
63
+ let totalTransitionsA = 0;
64
+ let totalTransitionsB = 0;
65
+ let totalDifferences = 0;
66
+
67
+ for (const fa of commonFas) {
68
+ const transA = jsonA[fa] || [];
69
+ const transB = jsonB[fa] || [];
70
+
71
+ totalTransitionsA += transA.length;
72
+ totalTransitionsB += transB.length;
73
+
74
+ console.log(`\nFA: ${fa}`);
75
+ console.log(` A: ${transA.length} transitions`);
76
+ console.log(` B: ${transB.length} transitions`);
77
+
78
+ // Normalize transitions for comparison
79
+ const transANorm = transA.map(tr => normalizeTransition(tr));
80
+ const transBNorm = transB.map(tr => normalizeTransition(tr));
81
+
82
+ // Find missing and extra transitions
83
+ const missingInBTrans = [];
84
+ const missingInATrans = [];
85
+
86
+ for (let i = 0; i < transANorm.length; i++) {
87
+ const trStr = transANorm[i];
88
+ const found = transBNorm.some(t => t === trStr);
89
+ if (!found) {
90
+ missingInBTrans.push(transA[i]);
91
+ }
92
+ }
93
+
94
+ for (let i = 0; i < transBNorm.length; i++) {
95
+ const trStr = transBNorm[i];
96
+ const found = transANorm.some(t => t === trStr);
97
+ if (!found) {
98
+ missingInATrans.push(transB[i]);
99
+ }
100
+ }
101
+
102
+ if (missingInBTrans.length === 0 && missingInATrans.length === 0) {
103
+ console.log(` ✓ All transitions match`);
104
+ } else {
105
+ if (missingInBTrans.length > 0) {
106
+ console.log(` ❌ Missing in B (${missingInBTrans.length}):`);
107
+ for (const tr of missingInBTrans) {
108
+ console.log(` ${JSON.stringify(tr)}`);
109
+ }
110
+ totalDifferences += missingInBTrans.length;
111
+ }
112
+ if (missingInATrans.length > 0) {
113
+ console.log(` ❌ Extra in B / Missing in A (${missingInATrans.length}):`);
114
+ for (const tr of missingInATrans) {
115
+ console.log(` ${JSON.stringify(tr)}`);
116
+ }
117
+ totalDifferences += missingInATrans.length;
118
+ }
119
+ }
120
+ }
121
+
122
+ // Summary
123
+ console.log('\n--- SUMMARY ---');
124
+ console.log(`Total FAs: A=${fasA.length}, B=${fasB.length}`);
125
+ console.log(`Total transitions: A=${totalTransitionsA}, B=${totalTransitionsB}`);
126
+ console.log(`Total differences: ${totalDifferences}`);
127
+
128
+ if (totalDifferences === 0 && fasA.length === fasB.length && totalTransitionsA === totalTransitionsB) {
129
+ console.log('\n✓ FILES ARE IDENTICAL');
130
+ } else {
131
+ console.log('\n❌ FILES DIFFER');
132
+ }
133
+ console.log();
134
+ }
135
+
136
+ function normalizeTransition(tr) {
137
+ // Transition format: [from, signal, to] or [from, signal, to, command]
138
+ if (tr.length === 3) {
139
+ return `${tr[0]}|${tr[1]}|${tr[2]}`;
140
+ } else if (tr.length === 4) {
141
+ return `${tr[0]}|${tr[1]}|${tr[2]}|${tr[3]}`;
142
+ }
143
+ return '';
144
+ }
145
+
146
+ main();
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Convert draw.io UML state diagram back to compact SFSM JSON
4
+ * Usage: node drawio-to-json.js <input-drawio> <output-json>
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const xml2js = require('xml2js');
10
+
11
+ // ============ MAIN ============
12
+ async function main() {
13
+ const [inputDrawioPath, outputJsonPath] = process.argv.slice(2);
14
+
15
+ if (!inputDrawioPath || !outputJsonPath) {
16
+ console.error('Usage: node drawio-to-json.js <input-drawio> <output-json>');
17
+ process.exit(1);
18
+ }
19
+
20
+ try {
21
+ const drawioContent = fs.readFileSync(inputDrawioPath, 'utf-8');
22
+ const json = await convertDrawioToJson(drawioContent);
23
+
24
+ // Ensure output directory exists
25
+ const outputDir = path.dirname(outputJsonPath);
26
+ if (!fs.existsSync(outputDir)) {
27
+ fs.mkdirSync(outputDir, { recursive: true });
28
+ }
29
+
30
+ fs.writeFileSync(outputJsonPath, JSON.stringify(json, null, 2));
31
+ console.log(`Generated: ${outputJsonPath}`);
32
+ } catch (err) {
33
+ console.error('Error:', err.message);
34
+ process.exit(1);
35
+ }
36
+ }
37
+
38
+ // ============ CONVERSION LOGIC ============
39
+
40
+ async function convertDrawioToJson(drawioContent) {
41
+ const parser = new xml2js.Parser();
42
+ const result = await parser.parseStringPromise(drawioContent);
43
+
44
+ const root = result.mxfile.diagram[0].mxGraphModel[0].root[0];
45
+ const mxCells = root.mxCell || [];
46
+
47
+ // Parse cells
48
+ const cellsMap = new Map(); // id -> cell info
49
+ const transitionsByFa = new Map(); // faName -> [transitions]
50
+ const faNames = new Set();
51
+
52
+ for (const cell of mxCells) {
53
+ const id = cell.$.id;
54
+ const style = cell.$.style || '';
55
+ const value = cell.$.value || '';
56
+
57
+ // Extract SFSM metadata from style
58
+ const sfsmRole = extractStyleAttr(style, 'sfsmRole');
59
+ const sfsmFa = extractStyleAttr(style, 'sfsmFa');
60
+ const sfsmKey = extractStyleAttr(style, 'sfsmKey');
61
+ const sfsmSignal = extractStyleAttr(style, 'sfsmSignal');
62
+ const sfsmCommand = extractStyleAttr(style, 'sfsmCommand');
63
+
64
+ if (sfsmFa) {
65
+ faNames.add(sfsmFa);
66
+ if (!transitionsByFa.has(sfsmFa)) {
67
+ transitionsByFa.set(sfsmFa, []);
68
+ }
69
+ }
70
+
71
+ cellsMap.set(id, {
72
+ id,
73
+ value,
74
+ sfsmRole,
75
+ sfsmFa,
76
+ sfsmKey,
77
+ sfsmSignal,
78
+ sfsmCommand,
79
+ source: cell.$.source,
80
+ target: cell.$.target
81
+ });
82
+ }
83
+
84
+ // Build transitions
85
+ for (const [faName] of transitionsByFa) {
86
+ const faTransitions = [];
87
+
88
+ // Iterate through cells to find transition edges for this FA
89
+ for (const cell of mxCells) {
90
+ const cellId = cell.$.id;
91
+ const cellInfo = cellsMap.get(cellId);
92
+
93
+ if (!cellInfo) continue;
94
+ if (cellInfo.sfsmRole !== 'transition' || cellInfo.sfsmFa !== faName) continue;
95
+
96
+ const sourceId = cell.$.source;
97
+ const targetId = cell.$.target;
98
+
99
+ if (!sourceId || !targetId) continue;
100
+
101
+ const sourceCell = cellsMap.get(sourceId);
102
+ const targetCell = cellsMap.get(targetId);
103
+
104
+ if (!sourceCell || !targetCell) continue;
105
+
106
+ // Determine from state
107
+ let fromState;
108
+ if (sourceCell.sfsmRole === 'initial') {
109
+ fromState = 'I';
110
+ } else if (sourceCell.sfsmKey) {
111
+ fromState = sourceCell.sfsmKey;
112
+ } else {
113
+ continue;
114
+ }
115
+
116
+ // Determine to state
117
+ const toState = targetCell.sfsmKey;
118
+ if (!toState) continue;
119
+
120
+ const signal = cellInfo.sfsmSignal;
121
+ const command = cellInfo.sfsmCommand;
122
+
123
+ // Build transition tuple
124
+ if (command && command.trim()) {
125
+ faTransitions.push([fromState, signal, toState, command]);
126
+ } else {
127
+ faTransitions.push([fromState, signal, toState]);
128
+ }
129
+ }
130
+
131
+ transitionsByFa.set(faName, faTransitions);
132
+ }
133
+
134
+ // Build compact JSON
135
+ const compactJson = {};
136
+ for (const faName of faNames) {
137
+ compactJson[faName] = transitionsByFa.get(faName) || [];
138
+ }
139
+
140
+ return compactJson;
141
+ }
142
+
143
+ function extractStyleAttr(style, attrName) {
144
+ const regex = new RegExp(`${attrName}=([^;]+)`);
145
+ const match = style.match(regex);
146
+ return match ? match[1] : '';
147
+ }
148
+
149
+ main();
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Convert SFSM compact-format FA JSON to draw.io UML state diagram using ELK.js for hierarchical layout
4
+ * Each FA is a separate container, sub-FAs are nested containers with light grey background
5
+ * Usage: node json-to-drawio.js <input-json> <output-drawio>
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const ELK = require('elkjs').default;
11
+
12
+ const elk = new ELK();
13
+
14
+ // ============ MAIN ============
15
+ async function main() {
16
+ const [inputJsonPath, outputDrawioPath] = process.argv.slice(2);
17
+
18
+ if (!inputJsonPath || !outputDrawioPath) {
19
+ console.error('Usage: node json-to-drawio.js <input-json> <output-drawio>');
20
+ process.exit(1);
21
+ }
22
+
23
+ try {
24
+ const faData = JSON.parse(fs.readFileSync(inputJsonPath, 'utf-8'));
25
+ const drawio = await generateDrawio(faData);
26
+
27
+ // Ensure output directory exists
28
+ const outputDir = path.dirname(outputDrawioPath);
29
+ if (!fs.existsSync(outputDir)) {
30
+ fs.mkdirSync(outputDir, { recursive: true });
31
+ }
32
+
33
+ fs.writeFileSync(outputDrawioPath, drawio);
34
+ console.log(`Generated: ${outputDrawioPath}`);
35
+ } catch (err) {
36
+ console.error('Error:', err.message);
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ // ============ CONVERSION LOGIC ============
42
+
43
+ async function generateDrawio(faData) {
44
+ const rootFaName = getRootFaName(faData);
45
+ const allFaNames = Object.keys(faData);
46
+
47
+ // Build hierarchical graph structure
48
+ const { cellsMap, edgeList } = await buildHierarchicalGraph(rootFaName, faData, allFaNames);
49
+
50
+ // Convert to drawio
51
+ return buildDrawioXml(cellsMap, edgeList);
52
+ }
53
+
54
+ function getRootFaName(faData) {
55
+ const allFaNames = Object.keys(faData);
56
+ const referencedFas = new Set();
57
+
58
+ for (const faKey of allFaNames) {
59
+ const transitions = faData[faKey];
60
+ for (const tr of transitions) {
61
+ const toState = tr[2];
62
+ if (allFaNames.includes(toState)) {
63
+ referencedFas.add(toState);
64
+ }
65
+ }
66
+ }
67
+
68
+ for (const faName of allFaNames) {
69
+ if (!referencedFas.has(faName)) {
70
+ return faName;
71
+ }
72
+ }
73
+
74
+ return allFaNames[0];
75
+ }
76
+
77
+ function buildGraphForELK(rootFaName, faData, allFaNames) {
78
+ const nodes = [];
79
+ const edges = [];
80
+ const nodeMap = new Map(); // nodeId -> { faKey, state, label, type }
81
+ const edgeMap = new Map(); // edgeId -> { from, signal, to, command, faKey }
82
+ const stateNodeMap = new Map(); // `${faKey}:${state}` -> nodeId
83
+
84
+ let nodeCounter = 0;
85
+ let edgeCounter = 0;
86
+
87
+ // Process all FAs (both root and sub-FAs)
88
+ for (const faName of allFaNames) {
89
+ processFA(faName, faData, allFaNames, nodes, edges, nodeMap, edgeMap, stateNodeMap, nodeCounter, edgeCounter);
90
+ }
91
+
92
+ return { nodes, edges: edges, nodeMap, edgeMap };
93
+ }
94
+
95
+ function processFA(faKey, faData, allFaNames, nodes, edges, nodeMap, edgeMap, stateNodeMap, nodeCounter, edgeCounter) {
96
+ const transitions = faData[faKey];
97
+ const states = new Set();
98
+ const exitStates = new Set();
99
+
100
+ // Collect states and exit states
101
+ for (const tr of transitions) {
102
+ const [from, signal, to, command] = tr;
103
+ states.add(from);
104
+ states.add(to);
105
+ if (to.startsWith('E_')) {
106
+ exitStates.add(to);
107
+ }
108
+ }
109
+
110
+ // Create node for initial state
111
+ const initId = `init-${faKey}`;
112
+ nodeMap.set(initId, { faKey, state: 'I', label: '', type: 'initial' });
113
+ stateNodeMap.set(`${faKey}:I`, initId);
114
+ nodes.push({
115
+ id: initId,
116
+ width: 20,
117
+ height: 20,
118
+ properties: { sfsmRole: 'initial', sfsmFa: faKey }
119
+ });
120
+
121
+ // Create nodes for other states
122
+ for (const state of states) {
123
+ if (state === 'I') continue;
124
+
125
+ let type = 'state';
126
+ let label = state;
127
+
128
+ if (exitStates.has(state)) {
129
+ type = 'exit';
130
+ } else if (allFaNames.includes(state)) {
131
+ type = 'subfa';
132
+ }
133
+
134
+ const nodeId = `state-${faKey}-${state}`;
135
+ nodeMap.set(nodeId, { faKey, state, label, type });
136
+ stateNodeMap.set(`${faKey}:${state}`, nodeId);
137
+
138
+ const width = type === 'exit' ? 70 : 140;
139
+ const height = type === 'exit' ? 70 : 50;
140
+
141
+ nodes.push({
142
+ id: nodeId,
143
+ width: width,
144
+ height: height,
145
+ labels: [{ text: label }],
146
+ properties: {
147
+ sfsmRole: type,
148
+ sfsmFa: faKey,
149
+ sfsmKey: state
150
+ }
151
+ });
152
+ }
153
+
154
+ // Create edges
155
+ for (const tr of transitions) {
156
+ const [from, signal, to, command] = tr;
157
+ const sourceId = stateNodeMap.get(`${faKey}:${from}`);
158
+ const targetId = stateNodeMap.get(`${faKey}:${to}`);
159
+
160
+ if (!sourceId || !targetId) continue;
161
+
162
+ const edgeLabel = command ? `${signal} / ${to}:${command}` : signal;
163
+ const edgeId = `tr-${faKey}-${edgeCounter++}`;
164
+
165
+ edgeMap.set(edgeId, { from, signal, to, command, faKey });
166
+
167
+ edges.push({
168
+ id: edgeId,
169
+ sources: [sourceId],
170
+ targets: [targetId],
171
+ labels: [{ text: edgeLabel }],
172
+ properties: {
173
+ sfsmRole: 'transition',
174
+ sfsmFa: faKey,
175
+ sfsmSignal: signal,
176
+ sfsmCommand: command || ''
177
+ }
178
+ });
179
+ }
180
+ }
181
+
182
+ function buildDrawioXmlFromLayout(layout, nodeMap, edgeMap, faData, allFaNames) {
183
+ const cellsMap = new Map();
184
+ const edges = [];
185
+
186
+ // Convert ELK nodes to drawio cells
187
+ function convertNodes(elkNodes, parent = '1') {
188
+ if (!elkNodes) return;
189
+
190
+ for (const elkNode of elkNodes) {
191
+ const nodeInfo = nodeMap.get(elkNode.id);
192
+ if (!nodeInfo) continue;
193
+
194
+ const { faKey, state, label, type } = nodeInfo;
195
+ let style = '';
196
+ let width = 20;
197
+ let height = 20;
198
+
199
+ switch (type) {
200
+ case 'initial':
201
+ style = `ellipse;whiteSpace=wrap;html=1;fillColor=#000000;strokeColor=#000000;sfsmRole=initial;sfsmFa=${faKey};`;
202
+ width = 20;
203
+ height = 20;
204
+ break;
205
+ case 'exit':
206
+ style = `ellipse;whiteSpace=wrap;html=1;strokeWidth=3;fillColor=#FFFFFF;sfsmRole=exit;sfsmFa=${faKey};sfsmKey=${state};`;
207
+ width = 70;
208
+ height = 70;
209
+ break;
210
+ case 'subfa':
211
+ style = `swimlane;whiteSpace=wrap;html=1;startSize=26;sfsmRole=subfa;sfsmFa=${faKey};sfsmKey=${state};`;
212
+ width = 200;
213
+ height = 200;
214
+ break;
215
+ default:
216
+ style = `rounded=1;whiteSpace=wrap;html=1;arcSize=20;sfsmRole=state;sfsmFa=${faKey};sfsmKey=${state};`;
217
+ width = 140;
218
+ height = 50;
219
+ }
220
+
221
+ cellsMap.set(elkNode.id, {
222
+ id: elkNode.id,
223
+ value: label,
224
+ style: style,
225
+ x: Math.round(elkNode.x || 0),
226
+ y: Math.round(elkNode.y || 0),
227
+ width: width,
228
+ height: height,
229
+ parent: parent
230
+ });
231
+
232
+ // Recursively process children
233
+ if (elkNode.children) {
234
+ convertNodes(elkNode.children, elkNode.id);
235
+ }
236
+ }
237
+ }
238
+
239
+ convertNodes(layout.children);
240
+
241
+ // Convert ELK edges to drawio edges
242
+ if (layout.edges) {
243
+ for (const elkEdge of layout.edges) {
244
+ const edgeInfo = edgeMap.get(elkEdge.id);
245
+ if (!edgeInfo) continue;
246
+
247
+ const { from, signal, to, command, faKey } = edgeInfo;
248
+ const edgeLabel = command ? `${signal} / ${to}:${command}` : signal;
249
+
250
+ let style = `edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;sfsmRole=transition;sfsmFa=${faKey};sfsmSignal=${signal};sfsmCommand=${command || ''};`;
251
+ if (from === to) {
252
+ style += `exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;`;
253
+ }
254
+
255
+ edges.push({
256
+ id: elkEdge.id,
257
+ value: edgeLabel,
258
+ style: style,
259
+ source: elkEdge.sources[0],
260
+ target: elkEdge.targets[0],
261
+ parent: '1'
262
+ });
263
+ }
264
+ }
265
+
266
+ return buildDrawioXml(cellsMap, edges);
267
+ }
268
+
269
+ function buildDrawioXml(cellsMap, edges) {
270
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>
271
+ <mxfile host="Electron">
272
+ <diagram id="sfsm-diagram" name="Page-1">
273
+ <mxGraphModel dx="800" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
274
+ <root>
275
+ <mxCell id="0"/>
276
+ <mxCell id="1" parent="0"/>
277
+ `;
278
+
279
+ for (const [cellId, cell] of cellsMap) {
280
+ xml += ` <mxCell id="${cell.id}" value="${escapeXml(cell.value)}" style="${cell.style}" vertex="1" parent="${cell.parent}">
281
+ <mxGeometry x="${cell.x}" y="${cell.y}" width="${cell.width}" height="${cell.height}" as="geometry"/>
282
+ </mxCell>
283
+ `;
284
+ }
285
+
286
+ for (const edge of edges) {
287
+ xml += ` <mxCell id="${edge.id}" value="${escapeXml(edge.value)}" style="${edge.style}" edge="1" parent="${edge.parent}" source="${edge.source}" target="${edge.target}">
288
+ <mxGeometry relative="1" as="geometry"/>
289
+ </mxCell>
290
+ `;
291
+ }
292
+
293
+ xml += ` </root>
294
+ </mxGraphModel>
295
+ </diagram>
296
+ </mxfile>`;
297
+
298
+ return xml;
299
+ }
300
+
301
+ function escapeXml(str) {
302
+ if (!str) return '';
303
+ return String(str)
304
+ .replace(/&/g, '&amp;')
305
+ .replace(/</g, '&lt;')
306
+ .replace(/>/g, '&gt;')
307
+ .replace(/"/g, '&quot;')
308
+ .replace(/'/g, '&apos;');
309
+ }
310
+
311
+ main();