@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.
- package/README.md +20 -5
- package/ai/skills/sfsm-compare-json-uml-diagram/SKILL.md +62 -0
- package/ai/skills/sfsm-json-to-uml-diagram/SKILL.md +50 -0
- package/ai/skills/sfsm-uml-diagram-to-json/SKILL.md +49 -0
- package/lib/esm/index.js +2 -9
- package/lib/esm/sfsm/CommandReceiver.js +17 -0
- package/lib/esm/sfsm/ControllerHub.js +82 -0
- package/lib/esm/sfsm/FaResolver.js +24 -3
- package/lib/esm/sfsm/Sfsm.js +48 -7
- package/lib/esm/sfsm/SignalSender.js +27 -0
- package/lib/esm/sfsm/index.js +3 -1
- package/lib/index.d.ts +1 -9
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -9
- package/lib/index.js.map +1 -1
- package/lib/sfsm/CommandReceiver.d.ts +23 -0
- package/lib/sfsm/CommandReceiver.d.ts.map +1 -0
- package/lib/sfsm/CommandReceiver.js +22 -0
- package/lib/sfsm/CommandReceiver.js.map +1 -0
- package/lib/sfsm/ControllerHub.d.ts +56 -0
- package/lib/sfsm/ControllerHub.d.ts.map +1 -0
- package/lib/sfsm/ControllerHub.js +87 -0
- package/lib/sfsm/ControllerHub.js.map +1 -0
- package/lib/sfsm/FaResolver.d.ts +14 -0
- package/lib/sfsm/FaResolver.d.ts.map +1 -1
- package/lib/sfsm/FaResolver.js +24 -3
- package/lib/sfsm/FaResolver.js.map +1 -1
- package/lib/sfsm/Sfsm.d.ts +26 -0
- package/lib/sfsm/Sfsm.d.ts.map +1 -1
- package/lib/sfsm/Sfsm.js +48 -7
- package/lib/sfsm/Sfsm.js.map +1 -1
- package/lib/sfsm/SignalSender.d.ts +24 -0
- package/lib/sfsm/SignalSender.d.ts.map +1 -0
- package/lib/sfsm/SignalSender.js +32 -0
- package/lib/sfsm/SignalSender.js.map +1 -0
- package/lib/sfsm/index.d.ts +3 -1
- package/lib/sfsm/index.d.ts.map +1 -1
- package/lib/sfsm/index.js +7 -3
- package/lib/sfsm/index.js.map +1 -1
- package/lib/sfsm/interfaces.d.ts +1 -1
- package/lib/sfsm/types.d.ts +21 -0
- package/lib/sfsm/types.d.ts.map +1 -1
- package/package.json +12 -9
- package/scripts/compare-compact-jsons.js +146 -0
- package/scripts/drawio-to-json.js +149 -0
- package/scripts/json-to-drawio-old.js +311 -0
- package/scripts/json-to-drawio.js +309 -0
- package/scripts/merge-fas.js +0 -0
- package/scripts/reduce-fa.js +0 -0
- package/scripts/update-fa.js +0 -0
- package/tutorial/01-finite-state-machine.md +223 -0
- package/tutorial/02-stacked-finite-state-machine.md +32 -0
- package/tutorial/03-advanced-themes.md +110 -0
- package/tutorial/04-tools.md +164 -0
- package/tutorial/images/Turnstile-img.png +0 -0
- package/tutorial/images/TurnstileBaseFA.png +0 -0
- package/LICENSE +0 -201
- package/LICENSE-COMMERCIAL.md +0 -41
- package/lib/DefaultState.d.ts +0 -9
- package/lib/DefaultState.d.ts.map +0 -1
- package/lib/DefaultState.js +0 -15
- package/lib/DefaultState.js.map +0 -1
- package/lib/FiniteStateMachine.d.ts +0 -227
- package/lib/FiniteStateMachine.d.ts.map +0 -1
- package/lib/FiniteStateMachine.js +0 -410
- package/lib/FiniteStateMachine.js.map +0 -1
- package/lib/IStateWithActions.d.ts +0 -18
- package/lib/IStateWithActions.d.ts.map +0 -1
- package/lib/IStateWithActions.js +0 -3
- package/lib/IStateWithActions.js.map +0 -1
- package/lib/IStateWithOutputSignal.d.ts +0 -4
- package/lib/IStateWithOutputSignal.d.ts.map +0 -1
- package/lib/IStateWithOutputSignal.js +0 -3
- package/lib/IStateWithOutputSignal.js.map +0 -1
- package/lib/MatrixBasedStateMachine.d.ts +0 -108
- package/lib/MatrixBasedStateMachine.d.ts.map +0 -1
- package/lib/MatrixBasedStateMachine.js +0 -132
- package/lib/MatrixBasedStateMachine.js.map +0 -1
- package/lib/TransitionMatrix.d.ts +0 -61
- package/lib/TransitionMatrix.d.ts.map +0 -1
- package/lib/TransitionMatrix.js +0 -104
- package/lib/TransitionMatrix.js.map +0 -1
- package/lib/esm/DefaultState.js +0 -10
- package/lib/esm/FiniteStateMachine.js +0 -405
- package/lib/esm/IStateWithActions.js +0 -1
- package/lib/esm/IStateWithOutputSignal.js +0 -1
- package/lib/esm/MatrixBasedStateMachine.js +0 -127
- package/lib/esm/TransitionMatrix.js +0 -98
- package/lib/esm/fa/DefaultState.js +0 -10
- package/lib/esm/fa/FiniteStateMachine.js +0 -405
- package/lib/esm/fa/IStateWithActions.js +0 -1
- package/lib/esm/fa/IStateWithOutputSignal.js +0 -1
- package/lib/esm/fa/MatrixBasedStateMachine.js +0 -127
- package/lib/esm/fa/TransitionMatrix.js +0 -98
- package/lib/esm/sfsm/ExternalWorldHub.js +0 -80
- package/lib/esm/sfsm/index.browser.js +0 -5
- package/lib/fa/DefaultState.d.ts +0 -9
- package/lib/fa/DefaultState.d.ts.map +0 -1
- package/lib/fa/DefaultState.js +0 -15
- package/lib/fa/DefaultState.js.map +0 -1
- package/lib/fa/FiniteStateMachine.d.ts +0 -227
- package/lib/fa/FiniteStateMachine.d.ts.map +0 -1
- package/lib/fa/FiniteStateMachine.js +0 -410
- package/lib/fa/FiniteStateMachine.js.map +0 -1
- package/lib/fa/IStateWithActions.d.ts +0 -18
- package/lib/fa/IStateWithActions.d.ts.map +0 -1
- package/lib/fa/IStateWithActions.js +0 -3
- package/lib/fa/IStateWithActions.js.map +0 -1
- package/lib/fa/IStateWithOutputSignal.d.ts +0 -4
- package/lib/fa/IStateWithOutputSignal.d.ts.map +0 -1
- package/lib/fa/IStateWithOutputSignal.js +0 -3
- package/lib/fa/IStateWithOutputSignal.js.map +0 -1
- package/lib/fa/MatrixBasedStateMachine.d.ts +0 -108
- package/lib/fa/MatrixBasedStateMachine.d.ts.map +0 -1
- package/lib/fa/MatrixBasedStateMachine.js +0 -132
- package/lib/fa/MatrixBasedStateMachine.js.map +0 -1
- package/lib/fa/TransitionMatrix.d.ts +0 -61
- package/lib/fa/TransitionMatrix.d.ts.map +0 -1
- package/lib/fa/TransitionMatrix.js +0 -104
- package/lib/fa/TransitionMatrix.js.map +0 -1
- package/lib/sfsm/ExternalWorldHub.d.ts +0 -52
- package/lib/sfsm/ExternalWorldHub.d.ts.map +0 -1
- package/lib/sfsm/ExternalWorldHub.js +0 -85
- package/lib/sfsm/ExternalWorldHub.js.map +0 -1
- package/lib/sfsm/index.browser.d.ts +0 -7
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Convert SFSM compact-format FA JSON to draw.io UML state diagram
|
|
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 allFaNames = Object.keys(faData);
|
|
45
|
+
const cellsMap = new Map();
|
|
46
|
+
const edgeList = [];
|
|
47
|
+
const stateNodeMap = new Map(); // `${faKey}:${state}` -> nodeId
|
|
48
|
+
|
|
49
|
+
let containerYOffset = 20; // Track vertical position of containers
|
|
50
|
+
|
|
51
|
+
// Process each FA and create its container with nested states
|
|
52
|
+
for (const faName of allFaNames) {
|
|
53
|
+
containerYOffset = await processFAWithContainer(faName, faData, allFaNames, cellsMap, edgeList, stateNodeMap, containerYOffset);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Convert to drawio
|
|
57
|
+
return buildDrawioXml(cellsMap, edgeList);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function processFAWithContainer(faKey, faData, allFaNames, cellsMap, edgeList, stateNodeMap, containerYPos) {
|
|
61
|
+
const transitions = faData[faKey];
|
|
62
|
+
const states = new Set();
|
|
63
|
+
const exitStates = new Set();
|
|
64
|
+
const subFaStates = new Set();
|
|
65
|
+
|
|
66
|
+
// Collect state information
|
|
67
|
+
for (const tr of transitions) {
|
|
68
|
+
const [from, signal, to] = tr;
|
|
69
|
+
states.add(from);
|
|
70
|
+
states.add(to);
|
|
71
|
+
|
|
72
|
+
if (to.startsWith('E_')) {
|
|
73
|
+
exitStates.add(to);
|
|
74
|
+
}
|
|
75
|
+
if (allFaNames.includes(to)) {
|
|
76
|
+
subFaStates.add(to);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build ELK graph structure for this FA's layout
|
|
81
|
+
const elkChildren = [];
|
|
82
|
+
const nodePositions = new Map(); // nodeId -> {x, y}
|
|
83
|
+
|
|
84
|
+
// Initial state
|
|
85
|
+
const initId = `init-${faKey}`;
|
|
86
|
+
elkChildren.push({
|
|
87
|
+
id: initId,
|
|
88
|
+
width: 20,
|
|
89
|
+
height: 20
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Other states
|
|
93
|
+
for (const state of states) {
|
|
94
|
+
if (state === 'I') continue;
|
|
95
|
+
|
|
96
|
+
const nodeId = `state-${faKey}-${state}`;
|
|
97
|
+
let width = 120;
|
|
98
|
+
let height = 50;
|
|
99
|
+
|
|
100
|
+
if (exitStates.has(state)) {
|
|
101
|
+
width = 70;
|
|
102
|
+
height = 70;
|
|
103
|
+
} else if (subFaStates.has(state)) {
|
|
104
|
+
// Sub-FA: much smaller size
|
|
105
|
+
width = 100;
|
|
106
|
+
height = 60;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
elkChildren.push({
|
|
110
|
+
id: nodeId,
|
|
111
|
+
width: width,
|
|
112
|
+
height: height,
|
|
113
|
+
labels: [{ text: state }]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Build ELK edges for this FA
|
|
118
|
+
const elkEdges = [];
|
|
119
|
+
for (const tr of transitions) {
|
|
120
|
+
const [from, signal, to] = tr;
|
|
121
|
+
const fromId = from === 'I' ? `init-${faKey}` : `state-${faKey}-${from}`;
|
|
122
|
+
const toId = `state-${faKey}-${to}`;
|
|
123
|
+
|
|
124
|
+
elkEdges.push({
|
|
125
|
+
id: `tr-${faKey}-${from}-${to}`,
|
|
126
|
+
sources: [fromId],
|
|
127
|
+
targets: [toId],
|
|
128
|
+
labels: [{ text: signal }]
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Layout this FA using ELK
|
|
133
|
+
const graph = {
|
|
134
|
+
id: `graph-${faKey}`,
|
|
135
|
+
layoutOptions: {
|
|
136
|
+
'elk.algorithm': 'layered',
|
|
137
|
+
'elk.direction': 'DOWN',
|
|
138
|
+
'elk.spacing.nodeNode': '30',
|
|
139
|
+
'elk.spacing.edgeNode': '15',
|
|
140
|
+
'elk.layered.spacing.edgeNodeBetweenLayers': '20'
|
|
141
|
+
},
|
|
142
|
+
children: elkChildren,
|
|
143
|
+
edges: elkEdges
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const layout = await elk.layout(graph);
|
|
148
|
+
|
|
149
|
+
// Extract positions from layout
|
|
150
|
+
if (layout.children) {
|
|
151
|
+
for (const node of layout.children) {
|
|
152
|
+
nodePositions.set(node.id, { x: Math.round(node.x || 0), y: Math.round(node.y || 0) });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn(`Warning: ELK layout failed for ${faKey}, using default positions`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create cells for this FA's states
|
|
160
|
+
// Initial state
|
|
161
|
+
const initPos = nodePositions.get(initId) || { x: 20, y: 20 };
|
|
162
|
+
cellsMap.set(initId, {
|
|
163
|
+
id: initId,
|
|
164
|
+
value: '',
|
|
165
|
+
style: `ellipse;whiteSpace=wrap;html=1;fillColor=#000000;strokeColor=#000000;sfsmRole=initial;sfsmFa=${faKey};`,
|
|
166
|
+
x: initPos.x,
|
|
167
|
+
y: initPos.y,
|
|
168
|
+
width: 20,
|
|
169
|
+
height: 20,
|
|
170
|
+
parent: `container-${faKey}`
|
|
171
|
+
});
|
|
172
|
+
stateNodeMap.set(`${faKey}:I`, initId);
|
|
173
|
+
|
|
174
|
+
// Other states
|
|
175
|
+
for (const state of states) {
|
|
176
|
+
if (state === 'I') continue;
|
|
177
|
+
|
|
178
|
+
const nodeId = `state-${faKey}-${state}`;
|
|
179
|
+
const pos = nodePositions.get(nodeId) || { x: 20, y: 80 };
|
|
180
|
+
let style = '';
|
|
181
|
+
let width = 120;
|
|
182
|
+
let height = 50;
|
|
183
|
+
|
|
184
|
+
if (exitStates.has(state)) {
|
|
185
|
+
style = `ellipse;whiteSpace=wrap;html=1;strokeWidth=3;fillColor=#FFFFFF;sfsmRole=exit;sfsmFa=${faKey};sfsmKey=${state};`;
|
|
186
|
+
width = 70;
|
|
187
|
+
height = 70;
|
|
188
|
+
} else if (subFaStates.has(state)) {
|
|
189
|
+
// Sub-FA: light grey container, smaller size
|
|
190
|
+
style = `rounded=0;whiteSpace=wrap;html=1;fillColor=#E8E8E8;strokeColor=#999999;strokeWidth=2;sfsmRole=subfa;sfsmFa=${faKey};sfsmKey=${state};`;
|
|
191
|
+
width = 100;
|
|
192
|
+
height = 60;
|
|
193
|
+
} else {
|
|
194
|
+
style = `rounded=1;whiteSpace=wrap;html=1;arcSize=20;sfsmRole=state;sfsmFa=${faKey};sfsmKey=${state};`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
cellsMap.set(nodeId, {
|
|
198
|
+
id: nodeId,
|
|
199
|
+
value: state,
|
|
200
|
+
style: style,
|
|
201
|
+
x: pos.x,
|
|
202
|
+
y: pos.y,
|
|
203
|
+
width: width,
|
|
204
|
+
height: height,
|
|
205
|
+
parent: `container-${faKey}`
|
|
206
|
+
});
|
|
207
|
+
stateNodeMap.set(`${faKey}:${state}`, nodeId);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Calculate container size based on layout
|
|
211
|
+
let maxX = 40;
|
|
212
|
+
let maxY = 40;
|
|
213
|
+
for (const [, pos] of nodePositions) {
|
|
214
|
+
maxX = Math.max(maxX, pos.x + 150);
|
|
215
|
+
maxY = Math.max(maxY, pos.y + 100);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const containerWidth = Math.max(280, maxX + 40);
|
|
219
|
+
const containerHeight = Math.max(140, maxY + 40);
|
|
220
|
+
|
|
221
|
+
// Create container for FA at specified Y position
|
|
222
|
+
const containerId = `container-${faKey}`;
|
|
223
|
+
cellsMap.set(containerId, {
|
|
224
|
+
id: containerId,
|
|
225
|
+
value: faKey,
|
|
226
|
+
style: `swimlane;whiteSpace=wrap;html=1;startSize=35;fontStyle=1;rounded=1;sfsmRole=facontainer;sfsmFa=${faKey};`,
|
|
227
|
+
x: 20,
|
|
228
|
+
y: containerYPos,
|
|
229
|
+
width: containerWidth,
|
|
230
|
+
height: containerHeight,
|
|
231
|
+
parent: '1'
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Create transitions
|
|
235
|
+
let transitionCounter = 0;
|
|
236
|
+
for (const tr of transitions) {
|
|
237
|
+
const [from, signal, to, command] = tr;
|
|
238
|
+
const fromState = from === 'I' ? 'I' : from;
|
|
239
|
+
const sourceId = stateNodeMap.get(`${faKey}:${fromState}`);
|
|
240
|
+
const targetId = stateNodeMap.get(`${faKey}:${to}`);
|
|
241
|
+
|
|
242
|
+
if (!sourceId || !targetId) continue;
|
|
243
|
+
|
|
244
|
+
const edgeLabel = command ? `${signal} / ${to}:${command}` : signal;
|
|
245
|
+
const edgeId = `tr-${faKey}-${transitionCounter++}`;
|
|
246
|
+
const isLoop = from === to;
|
|
247
|
+
|
|
248
|
+
let style = `edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;sfsmRole=transition;sfsmFa=${faKey};sfsmSignal=${signal};sfsmCommand=${command || ''};`;
|
|
249
|
+
if (isLoop) {
|
|
250
|
+
style += `exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
edgeList.push({
|
|
254
|
+
id: edgeId,
|
|
255
|
+
value: edgeLabel,
|
|
256
|
+
style: style,
|
|
257
|
+
source: sourceId,
|
|
258
|
+
target: targetId,
|
|
259
|
+
parent: '1'
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Return next Y position for next container (with some spacing)
|
|
264
|
+
return containerYPos + containerHeight + 30;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function buildDrawioXml(cellsMap, edgeList) {
|
|
268
|
+
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
269
|
+
<mxfile host="Electron">
|
|
270
|
+
<diagram id="sfsm-diagram" name="Page-1">
|
|
271
|
+
<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">
|
|
272
|
+
<root>
|
|
273
|
+
<mxCell id="0"/>
|
|
274
|
+
<mxCell id="1" parent="0"/>
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
for (const [cellId, cell] of cellsMap) {
|
|
278
|
+
xml += ` <mxCell id="${cell.id}" value="${escapeXml(cell.value)}" style="${cell.style}" vertex="1" parent="${cell.parent}">
|
|
279
|
+
<mxGeometry x="${cell.x}" y="${cell.y}" width="${cell.width}" height="${cell.height}" as="geometry"/>
|
|
280
|
+
</mxCell>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const edge of edgeList) {
|
|
285
|
+
xml += ` <mxCell id="${edge.id}" value="${escapeXml(edge.value)}" style="${edge.style}" edge="1" parent="${edge.parent}" source="${edge.source}" target="${edge.target}">
|
|
286
|
+
<mxGeometry relative="1" as="geometry"/>
|
|
287
|
+
</mxCell>
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
xml += ` </root>
|
|
292
|
+
</mxGraphModel>
|
|
293
|
+
</diagram>
|
|
294
|
+
</mxfile>`;
|
|
295
|
+
|
|
296
|
+
return xml;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function escapeXml(str) {
|
|
300
|
+
if (!str) return '';
|
|
301
|
+
return String(str)
|
|
302
|
+
.replace(/&/g, '&')
|
|
303
|
+
.replace(/</g, '<')
|
|
304
|
+
.replace(/>/g, '>')
|
|
305
|
+
.replace(/"/g, '"')
|
|
306
|
+
.replace(/'/g, ''');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
main();
|
package/scripts/merge-fas.js
CHANGED
|
File without changes
|
package/scripts/reduce-fa.js
CHANGED
|
File without changes
|
package/scripts/update-fa.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# StOP Tutorial. Part 1; Finite State Machines
|
|
2
|
+
|
|
3
|
+
## 1. What is a Finite Automaton?
|
|
4
|
+
|
|
5
|
+
Mountains of scientific and educational books and articles have been written about finite automata (FA), which manage in an amazing way to not only confuse the reader, but also to frighten practitioners away from using them.
|
|
6
|
+
|
|
7
|
+
However, the basic idea of a finite automaton is as simple as it is fundamental.
|
|
8
|
+
|
|
9
|
+
Namely. We have:
|
|
10
|
+
- **A finite set of states** in which some object can exist, one of which is the initial state
|
|
11
|
+
- **A finite number of signals** that can be sent to this object and possibly lead to a change in its state, i.e., a transition from the current state to another
|
|
12
|
+
|
|
13
|
+
All the magic of finite automata is based on this simple idea.
|
|
14
|
+
|
|
15
|
+
To define a specific finite automaton, you need to specify a list of its states (States S), signals (Signals G), and transitions (Transitions T) in the form of a list of triples:
|
|
16
|
+
|
|
17
|
+
`<s0, g, s1>`, where:
|
|
18
|
+
- `s0` - the state in which the automaton is currently located (initially - the starting state)
|
|
19
|
+
- `g` - the signal
|
|
20
|
+
- `s1` - the state to which our object will transition after receiving the signal
|
|
21
|
+
|
|
22
|
+
Of course, we don't need states in S and signals in G that are not represented in T in any way. For practical use, some other constraints are also important, but we won't delve into the depths of theory for now, and will move on to a concrete programming example.
|
|
23
|
+
|
|
24
|
+
Let's examine the use of a finite automaton (FA) using the example of a very simple automaton - a primitive turnstile that lets someone into the metro or a paid restroom after they drop a coin or special token into its slot.
|
|
25
|
+
|
|
26
|
+
Like this:
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
This automaton has two states: **locked** and **unlocked**, and two signals: **coin received** (coin) and **person passed through** (push).
|
|
31
|
+
|
|
32
|
+
## 2. Defining the turnstile with the StOP library
|
|
33
|
+
|
|
34
|
+
The [TypeScript StOP library](https://github.com/vsirotin/StOP) processes finite automata with the `Sfsm` engine (available from the `@vsirotin/ts-stop` package).
|
|
35
|
+
|
|
36
|
+
SFSM stands for "Stacked Finite State Machine" — a finite automaton that can contain other finite automata as sub-machines, and can be stacked into hierarchies of arbitrary depth. The turnstile is a simple FA with no children, so it is a good starting point for our tutorial. Because of its simple behaviour we talk about finite automata (FA) and not about finite machines.
|
|
37
|
+
|
|
38
|
+
Some FA is described as a list of transitions, each one a triple (or, when a command must be sent, a quadruple — covered in a later chapter):
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
[s0, g, s1]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
which is a direct, literal translation of the `<s0, g, s1>` triples introduced above. This representation is convenient when a FA is small — for large, dense automata you may prefer other representations, but that is out of scope here.
|
|
45
|
+
|
|
46
|
+
One detail of the `Sfsm` engine is important to know from the very first example: every FA always starts in a reserved entry state named `"I"` or, by complex machines with `"*.I"`, e.g. `"BCC.I"`, that means `initial state`. This is not part of the pure theory above — it is a small, deliberate engine convention that becomes very useful once FAs are combined into hierarchies (a topic of a later chapter). For now, it simply means our turnstile needs one extra transition out of `"I"` into its real initial state, triggered by an explicit "start" signal.
|
|
47
|
+
|
|
48
|
+
Here is the complete turnstile FA, written as compact JSON:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"Turnstile": [
|
|
53
|
+
["I", "Start", "Locked"],
|
|
54
|
+
["Locked", "Coin", "Unlocked"],
|
|
55
|
+
["Unlocked","Push", "Locked"]
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This FA can be visually presented as a simple state diagram:
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
And here is how it is loaded and driven with the `Sfsm` class:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { Sfsm, FaDefinition } from '@vsirotin/ts-stop/sfsm';
|
|
67
|
+
import turnstileFa from './turnstile-fa.json';
|
|
68
|
+
|
|
69
|
+
const sfsm = new Sfsm();
|
|
70
|
+
sfsm.loadFA(turnstileFa as FaDefinition);
|
|
71
|
+
|
|
72
|
+
sfsm.getHeadState(); // 'I' — the reserved entry state
|
|
73
|
+
|
|
74
|
+
sfsm.receiveSignal('start');
|
|
75
|
+
sfsm.getHeadState(); // 'locked'
|
|
76
|
+
|
|
77
|
+
sfsm.receiveSignal('coin');
|
|
78
|
+
sfsm.getHeadState(); // 'unlocked'
|
|
79
|
+
|
|
80
|
+
sfsm.receiveSignal('push');
|
|
81
|
+
sfsm.getHeadState(); // 'locked'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A runnable version of this exact example is available as a unit test: [01-what-is-a-finite-automaton.test.ts](../../ts-stop/test/sfsm/tutorial/01-what-is-a-finite-automaton.test.ts).
|
|
85
|
+
|
|
86
|
+
### 2.1 A type-safe alternative
|
|
87
|
+
|
|
88
|
+
The plain-string form above is convenient, but nothing stops a typo like `"lokced"` from silently compiling — the `Transition` type accepts any string in each slot. If you would rather have the TypeScript compiler catch such typos, declare your states and signals as string-literal union types first, and write the transition list against them with a small generic helper:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { Sfsm, FaDefinition, Transition } from '@vsirotin/ts-stop/sfsm';
|
|
92
|
+
|
|
93
|
+
type TurnstileState = 'I' | 'locked' | 'unlocked';
|
|
94
|
+
type TurnstileSignal = 'start' | 'coin' | 'push';
|
|
95
|
+
|
|
96
|
+
// A transition restricted to a specific pair of state/signal literal types.
|
|
97
|
+
type TypedTransition<S extends string, G extends string> = [S, G, S];
|
|
98
|
+
|
|
99
|
+
// Accepts only transitions built from S/G, returns the plain runtime Transition[]
|
|
100
|
+
// that Sfsm actually consumes — no change to the library's runtime format.
|
|
101
|
+
function typedTransitions<S extends string, G extends string>(
|
|
102
|
+
transitions: Array<TypedTransition<S, G>>
|
|
103
|
+
): Transition[] {
|
|
104
|
+
return transitions;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const turnstileTransitions = typedTransitions<TurnstileState, TurnstileSignal>([
|
|
108
|
+
['I', 'start', 'locked'],
|
|
109
|
+
['locked', 'coin', 'unlocked'],
|
|
110
|
+
['unlocked', 'push', 'locked'],
|
|
111
|
+
// ['locked', 'coin', 'lokced'], // ✗ compile error: 'lokced' is not TurnstileState
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const turnstileFa: FaDefinition = { Turnstile: turnstileTransitions };
|
|
115
|
+
|
|
116
|
+
const sfsm = new Sfsm();
|
|
117
|
+
sfsm.loadFA(turnstileFa);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This costs nothing at runtime — `typedTransitions()` just returns its argument — but any misspelled state or signal name is now a compile-time error instead of a silent bug. A runnable version of this example is available as a unit test: [02-type-safe-fa-definition.test.ts](../../ts-stop/test/sfsm/tutorial/02-type-safe-fa-definition.test.ts).
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## 3. Jokers: wildcard signals and states
|
|
124
|
+
|
|
125
|
+
Writing out every single `<s0, g, s1>` transition by hand works well for a tidy, well-behaved automaton like our turnstile. Real devices, however, are messier: they can receive signals nobody planned for, and they can be told to do the same thing no matter what they happen to be doing at the time. Enumerating every combination by hand would make the transition list explode and, worse, would be all too easy to forget a case.
|
|
126
|
+
|
|
127
|
+
For this, the `Sfsm` engine supports **jokers** — a reserved value (`"*"` by default) that can stand in for "any signal" or "any state" in a transition:
|
|
128
|
+
|
|
129
|
+
- A **joker-signal** transition `[s0, "*", s1]` matches *any* signal while the automaton is in state `s0` — but only as a fallback: if a transition for the exact, literal signal already exists for `s0`, that one wins.
|
|
130
|
+
- A **joker-state** transition `["*", g, s1]` matches signal `g` from *any* current state — again only as a fallback, behind any transition that names the exact, literal state.
|
|
131
|
+
|
|
132
|
+
Because jokers are only a fallback, you can freely mix them with ordinary transitions without worrying about ordering: an exact match always takes priority, so the recommendation is simply to place joker transitions last in the list, purely for readability.
|
|
133
|
+
|
|
134
|
+
Jokers are configured through `SfsmOptions` when constructing the engine:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const sfsm = new Sfsm({
|
|
138
|
+
jokerSignal: '*', // default — the value that means "any signal"
|
|
139
|
+
jokerState: '*' // default — the value that means "any state"
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
You will rarely need to change these from the default `"*"`; the option exists mainly so you can pick a different symbol if `"*"` ever needs to be a real state or signal name in your own FA.
|
|
144
|
+
|
|
145
|
+
### 3.1 Joker signal: reacting to the unexpected (e.g. a power failure)
|
|
146
|
+
|
|
147
|
+
Imagine our turnstile's electronics can, at any moment, receive all sorts of diagnostic signals from its sensors — most of which are irrelevant, except that *any* signal that isn't part of its normal vocabulary (`coin`, `push`) should be treated as a sign that something is wrong (power dropping out, a sensor glitching, a cable disconnected...) and the safest reaction is to shut the turnstile down into a safe `off` state.
|
|
148
|
+
|
|
149
|
+
Instead of trying to list every possible malfunction signal, one joker-signal transition per operational state covers all of them at once:
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"Turnstile": [
|
|
154
|
+
["I", "start", "locked"],
|
|
155
|
+
["locked", "coin", "unlocked"],
|
|
156
|
+
["unlocked", "push", "locked"],
|
|
157
|
+
["locked", "*", "off"],
|
|
158
|
+
["unlocked", "*", "off"]
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { Sfsm, FaDefinition } from '@vsirotin/ts-stop/sfsm';
|
|
165
|
+
|
|
166
|
+
const sfsm = new Sfsm();
|
|
167
|
+
sfsm.loadFA(turnstileWithJokerSignalFa as FaDefinition);
|
|
168
|
+
|
|
169
|
+
sfsm.receiveSignal('start');
|
|
170
|
+
sfsm.getHeadState(); // 'locked'
|
|
171
|
+
|
|
172
|
+
sfsm.receiveSignal('coin');
|
|
173
|
+
sfsm.getHeadState(); // 'unlocked' (exact transition still wins)
|
|
174
|
+
|
|
175
|
+
sfsm.receiveSignal('powerFailure');
|
|
176
|
+
sfsm.getHeadState(); // 'off' (joker-signal fallback)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The turnstile keeps behaving exactly as before for `coin` and `push`; only signals it has no explicit rule for fall through to `*` and trigger the safety shutdown.
|
|
180
|
+
|
|
181
|
+
A runnable version of this example is available as a unit test: [03-joker-signal.test.ts](../../ts-stop/test/sfsm/tutorial/03-joker-signal.test.ts).
|
|
182
|
+
|
|
183
|
+
### 3.2 Joker state: a universal signal for technical personnel
|
|
184
|
+
|
|
185
|
+
Now imagine the opposite situation: a maintenance technician needs to send a `service` signal that must always work, no matter what the turnstile happens to be doing — locked, unlocked, mid-transaction, or even already `off`. The technician should not need to know (or care) about the turnstile's current state; they just need "put this thing into maintenance mode, now."
|
|
186
|
+
|
|
187
|
+
A single joker-state transition expresses exactly that, regardless of how many operational states the FA has:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"Turnstile": [
|
|
192
|
+
["I", "start", "locked"],
|
|
193
|
+
["locked", "coin", "unlocked"],
|
|
194
|
+
["unlocked", "push", "locked"],
|
|
195
|
+
["*", "service", "maintenance"]
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { Sfsm, FaDefinition } from '@vsirotin/ts-stop/sfsm';
|
|
202
|
+
|
|
203
|
+
const sfsm = new Sfsm();
|
|
204
|
+
sfsm.loadFA(turnstileWithJokerStateFa as FaDefinition);
|
|
205
|
+
|
|
206
|
+
sfsm.receiveSignal('start');
|
|
207
|
+
sfsm.receiveSignal('service');
|
|
208
|
+
sfsm.getHeadState(); // 'maintenance' — reached straight from 'locked'
|
|
209
|
+
|
|
210
|
+
// ...and it works the same from any other state:
|
|
211
|
+
sfsm.loadFA(turnstileWithJokerStateFa as FaDefinition);
|
|
212
|
+
sfsm.receiveSignal('start');
|
|
213
|
+
sfsm.receiveSignal('coin'); // now 'unlocked'
|
|
214
|
+
sfsm.receiveSignal('service');
|
|
215
|
+
sfsm.getHeadState(); // 'maintenance' — reached just as easily from 'unlocked'
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
One transition now covers "enter maintenance mode" from every current and future state — including states added to the FA later, with no changes needed to the `service` rule itself.
|
|
219
|
+
|
|
220
|
+
A runnable version of this example is available as a unit test: [03-joker-state.test.ts](../../ts-stop/test/sfsm/tutorial/03-joker-state.test.ts).
|
|
221
|
+
|
|
222
|
+
In the [next chapter](./02-stacked-finite-state-machine.md) we will see how to combine multiple FAs into a hierarchy, and how the `Sfsm` engine processes signals through that hierarchy.
|
|
223
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# StOP Tutorial. Part 2: Stacked Finite State Machines
|
|
2
|
+
|
|
3
|
+
## 4. What is a Stacked Finite State Machine (SFSM)?
|
|
4
|
+
|
|
5
|
+
The turnstile so far is a single, flat automaton — a nice fit for the pure theory from chapter 1. Real systems, however, are rarely that simple: a "check the coin" step is itself a small process with its own states, and a "process a payment" step might contain both "check a coin" and "check a banknote" as alternatives inside it. Modelling all of that as one giant flat FA would quickly become unreadable.
|
|
6
|
+
|
|
7
|
+
The **Stacked Finite State Machine (SFSM)** solves this by allowing a state to be, itself, another whole FA. This turns the picture from a single flat automaton into a **tree of FAs**: a root FA at the top, and any number of nested child FAs underneath it (each of which can itself have children, and so on). The SFSM engine (the `Sfsm` class) keeps an internal **stack** of currently-active FAs to process this tree: the bottom of the stack is always the root FA, and the top (the "head") is whichever FA is currently handling incoming signals. The stack grows (push) when a state turns out to be a sub-FA, and shrinks (pop) when a sub-FA finishes and control returns to its parent.
|
|
8
|
+
|
|
9
|
+
Two small naming conventions make this hierarchy work uniformly for every FA, at any depth:
|
|
10
|
+
- every FA has exactly one **entry state**, always named `"I"` or `"*.I"` (as already seen in chapters 1–3);
|
|
11
|
+
- every FA has one or more **exit states**, each named starting with `"E_"` or `"*-E_"` — reaching one means "this FA is done, hand control back to whoever activated it."
|
|
12
|
+
|
|
13
|
+
Here is a small, self-contained FA that only makes sense as a *child* of something bigger: it models checking a banknote offered as payment, without yet worrying about who offers it or what happens afterwards. It has one entry state, one "business" state per step, and three possible outcomes:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"BPP": [
|
|
18
|
+
["I", "BR.bc", "checking", "BC.check"],
|
|
19
|
+
["checking", "BC.pass", "accepting", "BA.accept"],
|
|
20
|
+
["checking", "BC.reject", "E_rejected"],
|
|
21
|
+
["accepting","BA.changeNeeded", "E_changeNeeded"],
|
|
22
|
+
["accepting","BA.noChangeNeeded","E_noChangeNeeded"]
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Its real purpose, though, is to later become a *state* inside a bigger turnstile finite state machine (FSM), at which point reaching an exit state pops it and hands the triggering signal back to its parent instead of resetting.
|
|
28
|
+
|
|
29
|
+
A runnable version of this example is available as a unit test: [04-stacked-finite-state-machine.test.ts](../../ts-stop/test/sfsm/tutorial/04-stacked-finite-state-machine.test.ts).
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|