@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/README.md CHANGED
@@ -1,10 +1,24 @@
1
- # @vsirotin/ts-stop
1
+ [![npm](https://img.shields.io/npm/v/@vsirotin/ts-stop?sort=semver&logo=npm)](https://www.npmjs.com/package/@vsirotin/ts-stop)
2
+ [![npm type definitions](https://img.shields.io/npm/types/@vsirotin/ts-stop?logo=typescript)](https://www.npmjs.com/package/@vsirotin/ts-stop)
3
+ [![npm](https://img.shields.io/npm/dt/@vsirotin/ts-stop?logo=npm)](http://npm-stat.com/charts.html?package=@vsirotin/ts-stop)
4
+ [![npm](https://img.shields.io/npm/dw/@vsirotin/ts-stop?logo=npm)](http://npm-stat.com/charts.html?package=@vsirotin/ts-stop)
2
5
 
3
- State-Oriented Programming library for TypeScript.
4
6
 
5
- For an introduction to the concept, read the [StOP Tutorial](https://github.com/vsirotin/StOP/blob/main/docs/Tutorial/01-StOP-Tutorial.md).
7
+ ![badge-nodejs](https://img.shields.io/badge/platform-jsNode-F8DB5D.svg?style=flat)
8
+ ![badge-browser](https://img.shields.io/badge/platform-jsBrowser-F8DB5D.svg?style=flat)
9
+
10
+ # StOP SDK - State Oriented Programming Software Development Kit
11
+
12
+ State Oriented Programming (StOP) is a programming paradigm that focuses on explicit state management and transformation.
13
+
14
+ This SDK provides a TypeScript/JavaScript library for implementing StOP concepts with [tutorial](ts/ts-stop/tutorial), [tools](ts/ts-stop/scripts), [skills for AI-agents](ts/ts-stop/skills) and [examples](ts/ts-stop/test).
15
+
16
+ Version 2.x of the SDK is a complete rewrite of the original 1.x version, with strong orientation for modern AI-agent development.
17
+
18
+ Therefore, the delivered units contain as well a set of skills for AI-agents. These skills and tutorials are now delivered together with code, to allow agents to read needed information without explicitly visiting the GitHub Project on the Internet.
19
+
20
+ This approach, library and tools are proven by the author in his commercial projects.
6
21
 
7
- For usage and API documentation, see the Angular 17 demo in [ts/ts-example](https://github.com/vsirotin/StOP/blob/main/ts/ts-example).
8
22
 
9
23
  ## License
10
24
 
@@ -16,4 +30,5 @@ This project is dual-licensed:
16
30
  **Quick Summary:**
17
31
  - ✅ Free for individuals and small teams (< 10 employees)
18
32
  - ✅ Free for non-commercial and low-revenue projects (< $10K/year)
19
- - 📧 Commercial licensing available - contact the author
33
+ - 📧 Commercial licensing available - contact the author (don't worry, the commercial terms are reasonable :-)
34
+
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: sfsm-compare-json-uml-diagram
3
+ description: Compare an SFSM FA definition JSON file against a draw.io UML state diagram and report every discrepancy. Use when validating that a diagram matches its source JSON, or vice versa. Combines drawio-to-json and compare-compact-jsons for round-trip verification.
4
+ metadata:
5
+ author: vsirotin
6
+ version: "2.0"
7
+ ---
8
+
9
+ # Compare SFSM Compact JSON ↔ UML State Diagram (draw.io)
10
+
11
+ This skill validates the correspondence between an SFSM FA definition in compact JSON format and a draw.io UML state diagram. It combines two scripts to produce a detailed diff report without modifying either file:
12
+ 1. **`drawio-to-json.js`** extracts JSON from the diagram (via SFSM metadata in cell styles or shape-based heuristics)
13
+ 2. **`compare-compact-jsons.js`** performs an intelligent comparison of the original and extracted JSON
14
+
15
+ ## Usage
16
+
17
+ Use this skill when you need to:
18
+ - **Validate** that a draw.io diagram matches its source JSON (e.g., after generating it with `sfsm-json-to-uml-diagram`)
19
+ - **Verify** a round-trip workflow: JSON → Diagram → JSON → Compare
20
+ - **Audit** a manually edited diagram to detect any data loss or drift
21
+ - **Catch discrepancies** early (missing transitions, target mismatches, incorrect commands, etc.)
22
+
23
+ ## Workflow
24
+
25
+ The skill requests two file paths from you (unless you provide them upfront):
26
+ - **JSON file**: A compact-format SFSM definition
27
+ - **Diagram file**: A `.drawio` file (ideally generated by `json-to-drawio.js`)
28
+
29
+ It then runs:
30
+ 1. `drawio-to-json.js <diagram.drawio> <extracted.json>` — extract JSON from the diagram
31
+ 2. `compare-compact-jsons.js <original.json> <extracted.json>` — compare and report differences
32
+
33
+ ## Output
34
+
35
+ The comparison report shows, per FA:
36
+ - ✅ Number of matching transitions
37
+ - ❌ Missing transitions (in diagram, not in JSON)
38
+ - ❌ Extra transitions (in diagram, not in JSON)
39
+ - ❌ Mismatches (same `from/signal` pair, different `to` or `command`)
40
+
41
+ If both sides match exactly, the report confirms alignment with no discrepancies.
42
+
43
+ ## Input/Output
44
+
45
+ - **Input**: Compact JSON file path (requested if not provided) and diagram `.drawio` file path (requested if not provided)
46
+ - **Output**: A report printed to console (no files are modified)
47
+
48
+ ## Commands
49
+
50
+ ```bash
51
+ cd ts/ts-stop
52
+ node scripts/drawio-to-json.js <input.drawio> <extracted.json>
53
+ node scripts/compare-compact-jsons.js <original.json> <extracted.json>
54
+ ```
55
+
56
+ ## Important note
57
+
58
+ **No files are modified by this skill.** If the comparison reveals discrepancies and you decide which side is authoritative, use:
59
+ - `sfsm-json-to-uml-diagram` to regenerate the diagram from the JSON if the JSON is authoritative
60
+ - `sfsm-uml-diagram-to-json` to extract the diagram's corrected JSON if the diagram is authoritative
61
+
62
+ See [04-tools.md § 7.5.5](../../tutorial/04-tools.md#755-compare-compact-jsons--validate-diagram-vs-json-correspondence) for detailed documentation.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: sfsm-json-to-uml-diagram
3
+ description: Convert an SFSM FA definition JSON file (compact or extended format) to a professional draw.io UML state diagram with hierarchical ELK.js layout. Use when asked to visualize or diagram an SFSM JSON definition. The generated diagram may require small manual repositioning for visual perfection.
4
+ metadata:
5
+ author: vsirotin
6
+ version: "2.0"
7
+ ---
8
+
9
+ # SFSM JSON → UML State Diagram (draw.io)
10
+
11
+ This skill wraps the `ts/ts-stop/scripts/json-to-drawio.js` script to convert an SFSM FA definition into a professional, hierarchical draw.io UML state diagram.
12
+
13
+ ## Usage
14
+
15
+ Invoke the script directly or use this skill when you need to:
16
+ - **Visualize** an SFSM JSON definition as a UML state diagram
17
+ - **Generate** a draw.io file for inspection, sharing, or further manual editing
18
+ - **Export** an FA definition to a visual format
19
+
20
+ ## What the script produces
21
+
22
+ - **Hierarchical layout**: Each FA rendered as a swimlane container; sub-FAs as nested light-grey boxes (100×60 px).
23
+ - **ELK.js automatic layout**: Professional hierarchical positioning of states within each FA using the Eclipse Layout Kernel.
24
+ - **Vertical stacking**: Multiple FAs stacked without overlap with 30px spacing.
25
+ - **Complete metadata**: All SFSM state roles, FA names, signals, and commands embedded in cell styles for round-trip conversion back to JSON.
26
+ - **UML state types**: Initial states (black dot), plain states (rounded rectangles), sub-FA containers (swimlanes), and exit/final states (thick-bordered circles).
27
+
28
+ ## Important note
29
+
30
+ **Generated diagrams may require small manual adjustments** for visual perfection. Common improvements:
31
+ - Reposition state or container nodes for better spacing or clarity
32
+ - Adjust swimlane heights or container sizes
33
+ - Tweak edge routing or labels
34
+
35
+ These manual edits preserve the underlying SFSM metadata (stored in cell styles), so the diagram remains convertible back to JSON with zero data loss.
36
+
37
+ ## Input/Output
38
+
39
+ - **Input**: Path to a compact or extended-format SFSM JSON file
40
+ - **Output**: A `.drawio` file ready for inspection in draw.io or manual editing
41
+
42
+ ## Command
43
+
44
+ ```bash
45
+ cd ts/ts-stop
46
+ npm run build # if not already built
47
+ node scripts/json-to-drawio.js <input.json> <output.drawio>
48
+ ```
49
+
50
+ See [04-tools.md § 7.5.3](../../tutorial/04-tools.md#753-json-to-drawio--convert-compact-fa-json-to-uml-state-diagram) for detailed documentation.
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: sfsm-uml-diagram-to-json
3
+ description: Extract an SFSM FA definition JSON file in compact format from a draw.io UML state diagram. Use when asked to convert, export, or reverse-engineer a diagram back into SFSM/StOP JSON.
4
+ metadata:
5
+ author: vsirotin
6
+ version: "2.0"
7
+ ---
8
+
9
+ # UML State Diagram (draw.io) → SFSM Compact JSON
10
+
11
+ This skill wraps the `ts/ts-stop/scripts/drawio-to-json.js` script to extract or reverse-convert a draw.io diagram into a compact-format SFSM FA definition ready for `Sfsm.loadFA()`.
12
+
13
+ ## Usage
14
+
15
+ Invoke the script directly or use this skill when you need to:
16
+ - **Convert** a draw.io diagram back to SFSM JSON format
17
+ - **Reverse-engineer** a manually drawn state diagram into a machine-readable FA definition
18
+ - **Extract** SFSM data from a diagram produced by the `sfsm-json-to-uml-diagram` skill (lossless round-trip)
19
+ - **Export** a collaborator's visual diagram as a JSON file for programmatic use
20
+
21
+ ## How it works
22
+
23
+ The script reads a `.drawio` file and extracts every state, transition, and SFSM-specific metadata:
24
+ - **Preferred**: Diagrams generated by `json-to-drawio.js` include custom metadata (sfsmRole, sfsmFa, sfsmKey, sfsmSignal, sfsmCommand) embedded in cell styles — these are read directly and trusted as ground truth.
25
+ - **Fallback**: Hand-drawn diagrams without metadata are parsed using shape-based heuristics (ellipses → states, swimlanes → sub-FAs, thick-bordered circles → exit states, etc.).
26
+
27
+ Output is a compact-format JSON object: `{ "<FaName>": [[from, signal, to], [from, signal, to, command], ...], ... }`
28
+
29
+ ## Input/Output
30
+
31
+ - **Input**: A `.drawio` file (ideally generated by `json-to-drawio.js`)
32
+ - **Output**: A `.json` file in compact SFSM format
33
+
34
+ ## Command
35
+
36
+ ```bash
37
+ cd ts/ts-stop
38
+ node scripts/drawio-to-json.js <input.drawio> <output.json>
39
+ ```
40
+
41
+ ## Validation tip
42
+
43
+ After converting a diagram to JSON, compare the result against the original JSON using the `compare-compact-jsons.js` script or the `sfsm-compare-json-uml-diagram` skill to verify the round-trip was lossless:
44
+
45
+ ```bash
46
+ node scripts/compare-compact-jsons.js <original.json> <output.json>
47
+ ```
48
+
49
+ See [04-tools.md § 7.5.4](../../tutorial/04-tools.md#754-drawio-to-json--extract-compact-fa-json-from-uml-diagram) for detailed documentation.
package/lib/esm/index.js CHANGED
@@ -1,9 +1,2 @@
1
- /**
2
- * Public API Surface of ts-stop
3
- */
4
- export * from './fa/DefaultState';
5
- export * from './fa/FiniteStateMachine';
6
- export * from './fa/IStateWithActions';
7
- export * from './fa/IStateWithOutputSignal';
8
- export * from './fa/MatrixBasedStateMachine';
9
- export * from './fa/TransitionMatrix';
1
+ // Re-export everything from sfsm
2
+ export * from './sfsm/index';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Abstract base class for the command-receiving role of an external-world component
3
+ * (a "Controller" in ControllerHub terminology).
4
+ *
5
+ * Concrete subclasses declare the command names they handle via getCommandNames()
6
+ * (read by ControllerHub for routing and diagnostics — no need to repeat the list
7
+ * again at the registration call site) and implement receiveCommand() to react to
8
+ * commands dispatched by the SFSM.
9
+ *
10
+ * A component that both sends signals and receives commands should extend
11
+ * SignalSender and additionally implement this class's contract directly
12
+ * (getCommandNames / receiveCommand) — TypeScript does not allow extending two
13
+ * classes, but structurally implementing this abstract class's shape is sufficient
14
+ * for ControllerHub.registerCommandReceiver().
15
+ */
16
+ export class CommandReceiver {
17
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Wiring hub between the SFSM and a collection of Controllers — components that
3
+ * play the SignalSender and/or CommandReceiver role for the "external world".
4
+ *
5
+ * - registerSignalSender(sender) — wires a SignalSender; its signal names are
6
+ * read from sender.getSignalNames(), so they never need to be repeated here.
7
+ * - registerCommandReceiver(receiver) — wires a CommandReceiver; its command names
8
+ * are read from receiver.getCommandNames(). Throws if a command name is already
9
+ * registered by another receiver.
10
+ * - connectTo(sfsm) — wires everything: sets itself as the SFSM's command receiver
11
+ * and calls connectSignalTarget(sfsm) on every registered signal sender.
12
+ *
13
+ * A single component that plays both roles (most real devices do) extends
14
+ * SignalSender and additionally implements the CommandReceiver contract on the
15
+ * same class, then is registered once with each method:
16
+ *
17
+ * new ControllerHub()
18
+ * .registerSignalSender(turnstileService)
19
+ * .registerSignalSender(turnstileDevice)
20
+ * .registerCommandReceiver(turnstileDevice)
21
+ * .connectTo(sfsm);
22
+ */
23
+ export class ControllerHub {
24
+ constructor() {
25
+ this.commandRoutes = new Map();
26
+ this.signalSenders = [];
27
+ }
28
+ /**
29
+ * Register a signal sender. Its signal names (getSignalNames()) are used for
30
+ * diagnostics via getRegisteredSignals().
31
+ * Fluent — returns this.
32
+ */
33
+ registerSignalSender(sender) {
34
+ this.signalSenders.push(sender);
35
+ return this;
36
+ }
37
+ /**
38
+ * Register a command receiver for all command names it declares via getCommandNames().
39
+ * Throws if any of those command names is already registered.
40
+ * Fluent — returns this.
41
+ */
42
+ registerCommandReceiver(receiver) {
43
+ for (const cmd of receiver.getCommandNames()) {
44
+ if (this.commandRoutes.has(cmd)) {
45
+ throw new Error(`ControllerHub: command "${cmd}" is already registered`);
46
+ }
47
+ this.commandRoutes.set(cmd, receiver);
48
+ }
49
+ return this;
50
+ }
51
+ /**
52
+ * Wire this hub to the given SFSM instance:
53
+ * 1. Sets this hub as the SFSM's command receiver.
54
+ * 2. Calls connectSignalTarget(sfsm) on every registered signal sender.
55
+ * Fluent — returns this.
56
+ */
57
+ connectTo(sfsm) {
58
+ sfsm.setCommandReceiver(this);
59
+ for (const sender of this.signalSenders) {
60
+ sender.connectSignalTarget(sfsm);
61
+ }
62
+ return this;
63
+ }
64
+ /** ICommandReceiver implementation — dispatches command to the registered receiver. */
65
+ receiveCommand(command, data) {
66
+ const receiver = this.commandRoutes.get(command);
67
+ if (!receiver) {
68
+ const registered = [...this.commandRoutes.keys()].join(', ');
69
+ throw new Error(`ControllerHub: no receiver registered for command "${command}". ` +
70
+ `Registered commands: [${registered}]`);
71
+ }
72
+ receiver.receiveCommand(command, data);
73
+ }
74
+ /** Returns all command names that have a registered receiver (for diagnostics / validation). */
75
+ getRegisteredCommands() {
76
+ return [...this.commandRoutes.keys()];
77
+ }
78
+ /** Returns all signal names that have a registered sender (for diagnostics / validation). */
79
+ getRegisteredSignals() {
80
+ return this.signalSenders.flatMap(s => s.getSignalNames());
81
+ }
82
+ }
@@ -67,7 +67,8 @@ export class FaResolver {
67
67
  name: rootName,
68
68
  transitions: rootValue,
69
69
  subFaNames: new Set(),
70
- node
70
+ node,
71
+ entryState: this.resolveEntryState(rootValue)
71
72
  });
72
73
  }
73
74
  else {
@@ -85,7 +86,13 @@ export class FaResolver {
85
86
  .map(t => t[2])
86
87
  .filter(target => definedFaNames.has(target)));
87
88
  const node = { ts: transitions };
88
- this.index.set(name, { name, transitions, subFaNames, node });
89
+ this.index.set(name, {
90
+ name,
91
+ transitions,
92
+ subFaNames,
93
+ node,
94
+ entryState: this.resolveEntryState(transitions)
95
+ });
89
96
  }
90
97
  }
91
98
  indexNode(name, node) {
@@ -102,7 +109,8 @@ export class FaResolver {
102
109
  name,
103
110
  transitions: node.ts,
104
111
  subFaNames,
105
- node
112
+ node,
113
+ entryState: this.resolveEntryState(node.ts)
106
114
  });
107
115
  }
108
116
  /** A state value is a sub-FA if it has a "ts" array property. */
@@ -112,4 +120,17 @@ export class FaResolver {
112
120
  'ts' in value &&
113
121
  Array.isArray(value.ts));
114
122
  }
123
+ /**
124
+ * Auto-detects the literal entry-state name used by a FA's own transitions:
125
+ * the bare `"I"`, or a namespaced `"<FaName>.I"` (e.g. `"TS.I"`). Falls back
126
+ * to `"I"` if the FA has no transition whose from-state matches either form.
127
+ */
128
+ resolveEntryState(transitions) {
129
+ for (const t of transitions) {
130
+ if (t[0] === 'I' || t[0].endsWith('.I')) {
131
+ return t[0];
132
+ }
133
+ }
134
+ return 'I';
135
+ }
115
136
  }
@@ -12,10 +12,24 @@ import { FaResolver } from './FaResolver';
12
12
  * processing a signal (e.g. from within a command receiver callback), the
13
13
  * incoming signal is queued and processed after the current step completes.
14
14
  * This prevents stack corruption and ensures deterministic ordering.
15
+ *
16
+ * Jokers: a transition may use a reserved "joker" string (default '*',
17
+ * configurable via SfsmOptions.jokerSignal / jokerState) in its signal or
18
+ * from-state slot to act as a fallback. A joker-signal transition matches
19
+ * any signal for its from-state; a joker-state transition matches any
20
+ * from-state for its signal. Exact, literal transitions always take
21
+ * priority over joker matches. See docs/Tutorial/Tutorial.md for examples.
22
+ *
23
+ * Entry/exit naming convention: every FA's entry state is either the bare
24
+ * "I", or a namespaced "<FaName>.I" (e.g. "TS.I") — auto-detected per FA from
25
+ * its own transitions. Every exit state either starts with "E_", or contains
26
+ * ".E_" (e.g. "TS.E_ok"). Both forms are recognised everywhere and may be
27
+ * freely mixed across FAs; see Tutorial.md §8.1 for when to use the
28
+ * namespaced form.
15
29
  */
16
30
  export class Sfsm {
17
31
  constructor(options = {}) {
18
- var _a, _b;
32
+ var _a, _b, _c, _d;
19
33
  this.resolver = null;
20
34
  this.stack = [];
21
35
  this.commandReceiver = null;
@@ -24,7 +38,9 @@ export class Sfsm {
24
38
  this.signalQueue = [];
25
39
  this.options = {
26
40
  byMissingData: (_a = options.byMissingData) !== null && _a !== void 0 ? _a : 'error',
27
- byMissingTransition: (_b = options.byMissingTransition) !== null && _b !== void 0 ? _b : 'error'
41
+ byMissingTransition: (_b = options.byMissingTransition) !== null && _b !== void 0 ? _b : 'error',
42
+ jokerSignal: (_c = options.jokerSignal) !== null && _c !== void 0 ? _c : '*',
43
+ jokerState: (_d = options.jokerState) !== null && _d !== void 0 ? _d : '*'
28
44
  };
29
45
  }
30
46
  /** Register the single command receiver for this SFSM instance. */
@@ -40,7 +56,7 @@ export class Sfsm {
40
56
  loadFA(definition) {
41
57
  this.resolver = new FaResolver(definition);
42
58
  const rootName = this.resolver.getRootName();
43
- this.stack = [{ faName: rootName, currentState: 'I' }];
59
+ this.stack = [{ faName: rootName, currentState: this.resolver.get(rootName).entryState }];
44
60
  this.log = [];
45
61
  this.processing = false;
46
62
  this.signalQueue = [];
@@ -92,12 +108,30 @@ export class Sfsm {
92
108
  /**
93
109
  * Rule 2: find a matching transition, searching from head down the stack.
94
110
  * Returns { frameIndex, transition } or null if not found anywhere.
111
+ *
112
+ * Within each frame, matches are tried in priority order so that exact,
113
+ * literal transitions always win over joker (wildcard) ones:
114
+ * 1. exact from-state + exact signal
115
+ * 2. exact from-state + joker signal
116
+ * 3. joker from-state + exact signal
117
+ * 4. joker from-state + joker signal
95
118
  */
96
119
  findTransition(signal) {
120
+ const jokerSignal = this.options.jokerSignal;
121
+ const jokerState = this.options.jokerState;
97
122
  for (let i = this.stack.length - 1; i >= 0; i--) {
98
123
  const frame = this.stack[i];
99
124
  const fa = this.resolver.get(frame.faName);
100
- const match = fa.transitions.find(t => t[0] === frame.currentState && t[1] === signal);
125
+ let match = fa.transitions.find(t => t[0] === frame.currentState && t[1] === signal);
126
+ if (!match && jokerSignal !== undefined) {
127
+ match = fa.transitions.find(t => t[0] === frame.currentState && t[1] === jokerSignal);
128
+ }
129
+ if (!match && jokerState !== undefined) {
130
+ match = fa.transitions.find(t => t[0] === jokerState && t[1] === signal);
131
+ }
132
+ if (!match && jokerState !== undefined && jokerSignal !== undefined) {
133
+ match = fa.transitions.find(t => t[0] === jokerState && t[1] === jokerSignal);
134
+ }
101
135
  if (match) {
102
136
  return { frameIndex: i, toState: match[2], command: match[3] };
103
137
  }
@@ -160,15 +194,15 @@ export class Sfsm {
160
194
  // Rule 3: new state is itself a sub-FA — push it
161
195
  const headFa = this.resolver.get(this.stack[frameIndex].faName);
162
196
  if (headFa.subFaNames.has(toState)) {
163
- this.stack.push({ faName: toState, currentState: 'I' });
197
+ this.stack.push({ faName: toState, currentState: this.resolver.get(toState).entryState });
164
198
  this.applySignal(signal, data); // forward signal to sub-FA
165
199
  return;
166
200
  }
167
201
  // Rule 4: new state is an exit state
168
- if (toState.startsWith('E_')) {
202
+ if (this.isExitState(toState)) {
169
203
  if (this.stack.length === 1) {
170
204
  // Rule 4.1 — root FA resets to I
171
- this.stack[0].currentState = 'I';
205
+ this.stack[0].currentState = this.resolver.get(this.stack[0].faName).entryState;
172
206
  }
173
207
  else {
174
208
  // Rule 4.2 — pop current FA, forward signal to new head
@@ -178,6 +212,13 @@ export class Sfsm {
178
212
  }
179
213
  }
180
214
  // ── Meta resolution helpers ──────────────────────────────────────────────
215
+ /**
216
+ * A state is an exit/final state if it starts with "E_" (default
217
+ * convention) or contains ".E_" (namespaced convention, e.g. "TS.E_ok").
218
+ */
219
+ isExitState(state) {
220
+ return state.startsWith('E_') || state.includes('.E_');
221
+ }
181
222
  resolveStateMeta(frameIndex, state) {
182
223
  var _a;
183
224
  const fa = this.resolver.get(this.stack[frameIndex].faName);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Abstract base class for the signal-sending role of an external-world component
3
+ * (a "Controller" in ControllerHub terminology).
4
+ *
5
+ * Concrete subclasses:
6
+ * - declare the signal names they may emit via getSignalNames()
7
+ * (read by ControllerHub for wiring and diagnostics — no need to repeat the
8
+ * list again at the registration call site);
9
+ * - call the protected sendSignal() to push a signal to the connected target
10
+ * (normally the SFSM).
11
+ *
12
+ * ControllerHub calls connectSignalTarget() automatically during connectTo().
13
+ */
14
+ export class SignalSender {
15
+ constructor() {
16
+ this.signalTarget = null;
17
+ }
18
+ /** Called by ControllerHub to connect the SFSM (or any receiver) as the signal target. */
19
+ connectSignalTarget(target) {
20
+ this.signalTarget = target;
21
+ }
22
+ /** Sends a signal to the connected target (normally the SFSM). */
23
+ sendSignal(name, data) {
24
+ var _a;
25
+ (_a = this.signalTarget) === null || _a === void 0 ? void 0 : _a.receiveSignal(name, data);
26
+ }
27
+ }
@@ -1,4 +1,6 @@
1
- export { ExternalWorldHub } from './ExternalWorldHub';
1
+ export { ControllerHub } from './ControllerHub';
2
+ export { SignalSender } from './SignalSender';
3
+ export { CommandReceiver } from './CommandReceiver';
2
4
  export { FaResolver } from './FaResolver';
3
5
  export { Sfsm } from './Sfsm';
4
6
  export { reduceFA } from './FaReducer';
package/lib/index.d.ts CHANGED
@@ -1,10 +1,2 @@
1
- /**
2
- * Public API Surface of ts-stop
3
- */
4
- export * from './fa/DefaultState';
5
- export * from './fa/FiniteStateMachine';
6
- export * from './fa/IStateWithActions';
7
- export * from './fa/IStateWithOutputSignal';
8
- export * from './fa/MatrixBasedStateMachine';
9
- export * from './fa/TransitionMatrix';
1
+ export * from './sfsm/index';
10
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAC"}
package/lib/index.js CHANGED
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * Public API Surface of ts-stop
4
- */
5
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
3
  if (k2 === undefined) k2 = k;
7
4
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -17,10 +14,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
15
  };
19
16
  Object.defineProperty(exports, "__esModule", { value: true });
20
- __exportStar(require("./fa/DefaultState"), exports);
21
- __exportStar(require("./fa/FiniteStateMachine"), exports);
22
- __exportStar(require("./fa/IStateWithActions"), exports);
23
- __exportStar(require("./fa/IStateWithOutputSignal"), exports);
24
- __exportStar(require("./fa/MatrixBasedStateMachine"), exports);
25
- __exportStar(require("./fa/TransitionMatrix"), exports);
17
+ // Re-export everything from sfsm
18
+ __exportStar(require("./sfsm/index"), exports);
26
19
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;GAEG;;;;;;;;;;;;;;;;AAEH,oDAAkC;AAClC,0DAAwC;AACxC,yDAAuC;AACvC,8DAA4C;AAC5C,+DAA6C;AAC7C,wDAAsC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,iCAAiC;AACjC,+CAA6B"}
@@ -0,0 +1,23 @@
1
+ import { ICommandReceiver } from './interfaces';
2
+ /**
3
+ * Abstract base class for the command-receiving role of an external-world component
4
+ * (a "Controller" in ControllerHub terminology).
5
+ *
6
+ * Concrete subclasses declare the command names they handle via getCommandNames()
7
+ * (read by ControllerHub for routing and diagnostics — no need to repeat the list
8
+ * again at the registration call site) and implement receiveCommand() to react to
9
+ * commands dispatched by the SFSM.
10
+ *
11
+ * A component that both sends signals and receives commands should extend
12
+ * SignalSender and additionally implement this class's contract directly
13
+ * (getCommandNames / receiveCommand) — TypeScript does not allow extending two
14
+ * classes, but structurally implementing this abstract class's shape is sufficient
15
+ * for ControllerHub.registerCommandReceiver().
16
+ */
17
+ export declare abstract class CommandReceiver implements ICommandReceiver {
18
+ /** Returns all command names this receiver handles. */
19
+ abstract getCommandNames(): readonly string[];
20
+ /** Called by ControllerHub when the SFSM dispatches a matching command. */
21
+ abstract receiveCommand(command: string, data?: unknown): void;
22
+ }
23
+ //# sourceMappingURL=CommandReceiver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandReceiver.d.ts","sourceRoot":"","sources":["../../src/sfsm/CommandReceiver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,8BAAsB,eAAgB,YAAW,gBAAgB;IAE7D,uDAAuD;IACvD,QAAQ,CAAC,eAAe,IAAI,SAAS,MAAM,EAAE;IAE7C,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI;CACjE"}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CommandReceiver = void 0;
4
+ /**
5
+ * Abstract base class for the command-receiving role of an external-world component
6
+ * (a "Controller" in ControllerHub terminology).
7
+ *
8
+ * Concrete subclasses declare the command names they handle via getCommandNames()
9
+ * (read by ControllerHub for routing and diagnostics — no need to repeat the list
10
+ * again at the registration call site) and implement receiveCommand() to react to
11
+ * commands dispatched by the SFSM.
12
+ *
13
+ * A component that both sends signals and receives commands should extend
14
+ * SignalSender and additionally implement this class's contract directly
15
+ * (getCommandNames / receiveCommand) — TypeScript does not allow extending two
16
+ * classes, but structurally implementing this abstract class's shape is sufficient
17
+ * for ControllerHub.registerCommandReceiver().
18
+ */
19
+ class CommandReceiver {
20
+ }
21
+ exports.CommandReceiver = CommandReceiver;
22
+ //# sourceMappingURL=CommandReceiver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandReceiver.js","sourceRoot":"","sources":["../../src/sfsm/CommandReceiver.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAsB,eAAe;CAOpC;AAPD,0CAOC"}