@vsirotin/ts-stop 1.13.1 → 2.4.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.
Files changed (122) hide show
  1. package/ai/skills/sfsm-compare-json-uml-diagram/SKILL.md +62 -0
  2. package/ai/skills/sfsm-json-to-uml-diagram/SKILL.md +50 -0
  3. package/ai/skills/sfsm-uml-diagram-to-json/SKILL.md +49 -0
  4. package/lib/esm/sfsm/CommandReceiver.js +17 -0
  5. package/lib/esm/sfsm/ControllerHub.js +82 -0
  6. package/lib/esm/sfsm/FaResolver.js +24 -3
  7. package/lib/esm/sfsm/Sfsm.js +48 -7
  8. package/lib/esm/sfsm/SignalSender.js +27 -0
  9. package/lib/esm/sfsm/index.js +3 -1
  10. package/lib/sfsm/CommandReceiver.d.ts +23 -0
  11. package/lib/sfsm/CommandReceiver.d.ts.map +1 -0
  12. package/lib/sfsm/CommandReceiver.js +22 -0
  13. package/lib/sfsm/CommandReceiver.js.map +1 -0
  14. package/lib/sfsm/ControllerHub.d.ts +56 -0
  15. package/lib/sfsm/ControllerHub.d.ts.map +1 -0
  16. package/lib/sfsm/ControllerHub.js +87 -0
  17. package/lib/sfsm/ControllerHub.js.map +1 -0
  18. package/lib/sfsm/FaResolver.d.ts +14 -0
  19. package/lib/sfsm/FaResolver.d.ts.map +1 -1
  20. package/lib/sfsm/FaResolver.js +24 -3
  21. package/lib/sfsm/FaResolver.js.map +1 -1
  22. package/lib/sfsm/Sfsm.d.ts +26 -0
  23. package/lib/sfsm/Sfsm.d.ts.map +1 -1
  24. package/lib/sfsm/Sfsm.js +48 -7
  25. package/lib/sfsm/Sfsm.js.map +1 -1
  26. package/lib/sfsm/SignalSender.d.ts +24 -0
  27. package/lib/sfsm/SignalSender.d.ts.map +1 -0
  28. package/lib/sfsm/SignalSender.js +32 -0
  29. package/lib/sfsm/SignalSender.js.map +1 -0
  30. package/lib/sfsm/index.d.ts +3 -1
  31. package/lib/sfsm/index.d.ts.map +1 -1
  32. package/lib/sfsm/index.js +7 -3
  33. package/lib/sfsm/index.js.map +1 -1
  34. package/lib/sfsm/interfaces.d.ts +1 -1
  35. package/lib/sfsm/types.d.ts +21 -0
  36. package/lib/sfsm/types.d.ts.map +1 -1
  37. package/package.json +9 -8
  38. package/scripts/compare-compact-jsons.js +146 -0
  39. package/scripts/drawio-to-json.js +149 -0
  40. package/scripts/json-to-drawio-old.js +311 -0
  41. package/scripts/json-to-drawio.js +309 -0
  42. package/tutorial/01-finite-state-machine.md +223 -0
  43. package/tutorial/02-stacked-finite-state-machine.md +32 -0
  44. package/tutorial/03-advanced-themes.md +110 -0
  45. package/tutorial/04-tools.md +164 -0
  46. package/tutorial/images/Turnstile-img.png +0 -0
  47. package/tutorial/images/TurnstileBaseFA.png +0 -0
  48. package/LICENSE +0 -201
  49. package/LICENSE-COMMERCIAL.md +0 -41
  50. package/README.md +0 -19
  51. package/lib/DefaultState.d.ts +0 -9
  52. package/lib/DefaultState.d.ts.map +0 -1
  53. package/lib/DefaultState.js +0 -15
  54. package/lib/DefaultState.js.map +0 -1
  55. package/lib/FiniteStateMachine.d.ts +0 -227
  56. package/lib/FiniteStateMachine.d.ts.map +0 -1
  57. package/lib/FiniteStateMachine.js +0 -410
  58. package/lib/FiniteStateMachine.js.map +0 -1
  59. package/lib/IStateWithActions.d.ts +0 -18
  60. package/lib/IStateWithActions.d.ts.map +0 -1
  61. package/lib/IStateWithActions.js +0 -3
  62. package/lib/IStateWithActions.js.map +0 -1
  63. package/lib/IStateWithOutputSignal.d.ts +0 -4
  64. package/lib/IStateWithOutputSignal.d.ts.map +0 -1
  65. package/lib/IStateWithOutputSignal.js +0 -3
  66. package/lib/IStateWithOutputSignal.js.map +0 -1
  67. package/lib/MatrixBasedStateMachine.d.ts +0 -108
  68. package/lib/MatrixBasedStateMachine.d.ts.map +0 -1
  69. package/lib/MatrixBasedStateMachine.js +0 -132
  70. package/lib/MatrixBasedStateMachine.js.map +0 -1
  71. package/lib/TransitionMatrix.d.ts +0 -61
  72. package/lib/TransitionMatrix.d.ts.map +0 -1
  73. package/lib/TransitionMatrix.js +0 -104
  74. package/lib/TransitionMatrix.js.map +0 -1
  75. package/lib/esm/DefaultState.js +0 -10
  76. package/lib/esm/FiniteStateMachine.js +0 -405
  77. package/lib/esm/IStateWithActions.js +0 -1
  78. package/lib/esm/IStateWithOutputSignal.js +0 -1
  79. package/lib/esm/MatrixBasedStateMachine.js +0 -127
  80. package/lib/esm/TransitionMatrix.js +0 -98
  81. package/lib/esm/fa/DefaultState.js +0 -10
  82. package/lib/esm/fa/FiniteStateMachine.js +0 -405
  83. package/lib/esm/fa/IStateWithActions.js +0 -1
  84. package/lib/esm/fa/IStateWithOutputSignal.js +0 -1
  85. package/lib/esm/fa/MatrixBasedStateMachine.js +0 -127
  86. package/lib/esm/fa/TransitionMatrix.js +0 -98
  87. package/lib/esm/index.js +0 -9
  88. package/lib/esm/sfsm/ExternalWorldHub.js +0 -80
  89. package/lib/esm/sfsm/index.browser.js +0 -5
  90. package/lib/fa/DefaultState.d.ts +0 -9
  91. package/lib/fa/DefaultState.d.ts.map +0 -1
  92. package/lib/fa/DefaultState.js +0 -15
  93. package/lib/fa/DefaultState.js.map +0 -1
  94. package/lib/fa/FiniteStateMachine.d.ts +0 -227
  95. package/lib/fa/FiniteStateMachine.d.ts.map +0 -1
  96. package/lib/fa/FiniteStateMachine.js +0 -410
  97. package/lib/fa/FiniteStateMachine.js.map +0 -1
  98. package/lib/fa/IStateWithActions.d.ts +0 -18
  99. package/lib/fa/IStateWithActions.d.ts.map +0 -1
  100. package/lib/fa/IStateWithActions.js +0 -3
  101. package/lib/fa/IStateWithActions.js.map +0 -1
  102. package/lib/fa/IStateWithOutputSignal.d.ts +0 -4
  103. package/lib/fa/IStateWithOutputSignal.d.ts.map +0 -1
  104. package/lib/fa/IStateWithOutputSignal.js +0 -3
  105. package/lib/fa/IStateWithOutputSignal.js.map +0 -1
  106. package/lib/fa/MatrixBasedStateMachine.d.ts +0 -108
  107. package/lib/fa/MatrixBasedStateMachine.d.ts.map +0 -1
  108. package/lib/fa/MatrixBasedStateMachine.js +0 -132
  109. package/lib/fa/MatrixBasedStateMachine.js.map +0 -1
  110. package/lib/fa/TransitionMatrix.d.ts +0 -61
  111. package/lib/fa/TransitionMatrix.d.ts.map +0 -1
  112. package/lib/fa/TransitionMatrix.js +0 -104
  113. package/lib/fa/TransitionMatrix.js.map +0 -1
  114. package/lib/index.d.ts +0 -10
  115. package/lib/index.d.ts.map +0 -1
  116. package/lib/index.js +0 -26
  117. package/lib/index.js.map +0 -1
  118. package/lib/sfsm/ExternalWorldHub.d.ts +0 -52
  119. package/lib/sfsm/ExternalWorldHub.d.ts.map +0 -1
  120. package/lib/sfsm/ExternalWorldHub.js +0 -85
  121. package/lib/sfsm/ExternalWorldHub.js.map +0 -1
  122. 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, '&amp;')
303
+ .replace(/</g, '&lt;')
304
+ .replace(/>/g, '&gt;')
305
+ .replace(/"/g, '&quot;')
306
+ .replace(/'/g, '&apos;');
307
+ }
308
+
309
+ main();
@@ -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
+ ![Turnstile](./images/Turnstile-img.png)
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
+ ![Turnstile](./images/TurnstileBaseFA.png)
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
+
@@ -0,0 +1,110 @@
1
+ # StOP Tutorial. Part 3: Advanced Themes
2
+
3
+ ## 5. How stacked states are processed
4
+
5
+ Once an FA can contain other FAs, "processing a signal" needs a precise algorithm — not just "look up the transition," but also "where do I look, and what happens once I find (or fail to find) one." Here are the rules the `Sfsm` engine actually implements (verified directly against its source, `Sfsm.ts`):
6
+
7
+ 1. After `loadFA()`, the root FA is the only element on the stack and its active state is `"I"`. The engine now waits for a signal.
8
+ 2. When a signal `s` arrives, the engine searches for a matching transition, starting at the **head** of the stack (the innermost, currently active FA) and, if needed, continuing down through each ancestor FA towards the root:
9
+ - **2.1** — If the head FA has a transition matching its own active state and `s` (including joker fallbacks, see chapter 3), that transition is applied directly: the head's active state becomes the transition's target. This step is logged with rule `"2.1"`.
10
+ - **2.2** — If the head FA has *no* matching transition, the search continues in its parent FA, then that FA's parent, and so on down to the root.
11
+ - **2.2.1** — If a matching transition is found in some ancestor FA, every FA above it on the stack is popped (they are abandoned mid-flight), that ancestor becomes the new head, and the transition is applied there.
12
+ - **2.2.2** — If **no** FA anywhere in the stack — from the head all the way down to the root — has a matching transition, **no log entry is created**, and the engine instead applies the `byMissingTransition` policy (`'error'` throws, `'log_warning'` warns and does nothing, `'ignore'` silently does nothing).
13
+ 3. If the transition found in step 2 carries a command (its 4th element), the command is sent to the registered command receiver.
14
+ 4. If the transition's target state is itself the name of a sub-FA, that sub-FA is pushed onto the stack with active state `"I"`, and the very same signal `s` is immediately forwarded into it, restarting this whole process (step 2) one level deeper.
15
+ 5. If the transition's target state is an exit state (starts with `"E_"`):
16
+ - **5.1** — if the current FA is the only one left on the stack (the root), it simply resets its own active state back to `"I"`;
17
+ - **5.2** — otherwise, the current FA is popped off the stack, and the very same signal `s` is forwarded to the FA that is now the head, restarting this whole process (step 2) one level up.
18
+
19
+
20
+ The example of using the SFSM engine can be found in [Sfsm.log.test.ts](../../ts-stop/test/sfsm/Sfsm.log.test.ts).
21
+
22
+ ## 6. Signal Senders, Command Receivers, Controllers, and the Controller Hub
23
+
24
+ Everything so far has driven the `Sfsm` engine directly, by calling `receiveSignal()` from test code. In a real application, signals come from real devices — a coin slot, a push sensor, a button — and commands need to reach real devices too — a lock, a light, a dispenser. The library gives you three small building blocks to wire this up cleanly:
25
+
26
+ - **`SignalSender`** — an abstract base class you extend on any component that needs to emit signals into the SFSM. You implement `getSignalNames()` (the list of signal names it may ever send) and call the protected `sendSignal(name, data?)` method whenever something happens in the real world.
27
+ - **`CommandReceiver`** — an abstract base class (or, if a class already extends something else, the `ICommandReceiver` interface it mirrors) for any component that needs to react to commands coming *from* the SFSM. You implement `getCommandNames()` (the commands it can handle) and `receiveCommand(command, data?)`.
28
+ - **Controller** — not a class, but a *role*: any component that plays the `SignalSender` role, the `CommandReceiver` role, or (as most real devices do) both at once, is called a Controller.
29
+ - **`ControllerHub`** — the wiring hub that connects every Controller to one `Sfsm` instance. You register each Controller once with `registerSignalSender()` and/or `registerCommandReceiver()`, then call `connectTo(sfsm)`. From then on, the hub automatically routes every command the SFSM sends to the right Controller (by the exact command name), and forwards every signal a Controller sends into the SFSM.
30
+
31
+ Here is a minimal physical turnstile "gate" Controller — it plays both roles at once, exactly like the real simulators used elsewhere in this library's own test suite:
32
+
33
+ ```typescript
34
+ import { Sfsm, FaDefinition, SignalSender, ICommandReceiver, ControllerHub } from '@vsirotin/ts-stop/sfsm';
35
+
36
+ class TurnstileGate extends SignalSender implements ICommandReceiver {
37
+ private locked = true;
38
+
39
+ getSignalNames(): readonly string[] { return ['start', 'coin', 'push']; }
40
+ getCommandNames(): readonly string[] { return ['GATE.lock', 'GATE.unlock']; }
41
+
42
+ receiveCommand(command: string): void {
43
+ this.locked = command === 'GATE.lock';
44
+ }
45
+
46
+ isLocked(): boolean { return this.locked; }
47
+
48
+ start(): void { this.sendSignal('start'); }
49
+ insertCoin(): void { this.sendSignal('coin'); }
50
+ walkThrough(): void { this.sendSignal('push'); }
51
+ }
52
+
53
+ const turnstileFa: FaDefinition = {
54
+ Turnstile: [
55
+ ['I', 'start', 'locked'],
56
+ ['locked', 'coin', 'unlocked', 'GATE.unlock'],
57
+ ['unlocked', 'push', 'locked', 'GATE.lock']
58
+ ]
59
+ };
60
+
61
+ const sfsm = new Sfsm();
62
+ const gate = new TurnstileGate();
63
+
64
+ new ControllerHub()
65
+ .registerSignalSender(gate)
66
+ .registerCommandReceiver(gate)
67
+ .connectTo(sfsm);
68
+
69
+ sfsm.loadFA(turnstileFa);
70
+
71
+ gate.start();
72
+ gate.insertCoin();
73
+ gate.isLocked(); // false — the SFSM sent 'GATE.unlock' in response to 'coin'
74
+
75
+ gate.walkThrough();
76
+ gate.isLocked(); // true — the SFSM sent 'GATE.lock' in response to 'push'
77
+ ```
78
+
79
+ Notice that `TurnstileGate` never touches the `Sfsm` instance directly: it only knows how to emit its own signals and react to its own commands. All the wiring — "which Controller sends which signal," "which Controller handles which command" — lives in one place, the `ControllerHub`, which also gives you `getRegisteredSignals()` / `getRegisteredCommands()` for diagnostics (e.g. to validate that every signal and command mentioned in an FA definition actually has a Controller behind it).
80
+
81
+ A runnable version of this example is available as a unit test: [06-controllers-and-controller-hub.test.ts](../../ts-stop/test/sfsm/tutorial/06-controllers-and-controller-hub.test.ts).
82
+
83
+ ### 7. Name conventions
84
+
85
+ Every example so far has named the entry state simply `"I"` and exit states `"E_something"` — perfectly fine for a small, self-contained FA. Once a stacked SFSM grows to dozens of FAs, plain abbreviations like `"L"`, `"U"`, `"I"`, `"E_R"` start colliding in your head across FAs, and it becomes hard to tell, just by looking at a state name, *which* FA it belongs to.
86
+
87
+ For larger SFSMs, it is recommended to namespace state names with their own FA's name, using a dot: `<FaName>.I` for the entry state, and `<FaName>.<state>` for ordinary states — e.g. `TS.I`, `TS.L`, `TS.U` instead of bare `I`, `L`, `U`. Exit states keep their familiar `E_` marker but move it after the FA-name dot: `<FaName>.E_<name>` — e.g. `TS.E_ok` instead of bare `E_ok`.
88
+
89
+ ```json
90
+ {
91
+ "TS": [
92
+ ["TS.I", "TS.s", "TS.L"],
93
+ ["TS.L", "TS.coin", "TS.U"],
94
+ ["TS.U", "TS.push", "TS.L"]
95
+ ]
96
+ }
97
+ ```
98
+
99
+ The `Sfsm` engine recognises **both** forms everywhere, automatically:
100
+ - an entry state is whatever from-state a FA's own transitions use that is exactly `"I"` **or** ends with `".I"`;
101
+ - an exit state is any target state that starts with `"E_"` **or** contains `".E_"`.
102
+
103
+ This means:
104
+ - every FA fixture used earlier in this tutorial (bare `"I"` / `"E_..."`) keeps working exactly as written — no migration is required;
105
+ - you can freely mix both styles across FAs in the same SFSM (e.g. namespace only the FAs that are large enough to benefit from it);
106
+ - nothing else changes — this is purely a naming convention for readability, not a new engine feature: no new `SfsmOptions`, no change to how transitions, pushes, pops, or jokers are matched.
107
+
108
+ A runnable version of this example, built entirely with namespaced names, is available as a unit test: [08-namespaced-state-names.test.ts](../../ts-stop/test/sfsm/tutorial/08-namespaced-state-names.test.ts).
109
+
110
+ In future this part of tutorial will be expanded with more advanced topics, including description of best practices for building large SFSMs, and a few more examples of real-world applications.