jssm 5.144.7 → 5.145.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/cdn/instance.js +125 -24
- package/dist/cdn/viz.js +166 -47
- package/dist/cli/fsl-export-system-prompt.cjs +2 -2
- package/dist/cli/fsl-render.cjs +1 -1
- package/dist/cli/fsl.cjs +1 -1
- package/dist/cli/lib.cjs +1 -1
- package/dist/cli/lib.mjs +1 -1
- package/dist/deno/jssm.d.ts +18 -0
- package/dist/deno/jssm.js +1 -1
- package/dist/jssm.es5.cjs +1 -1
- package/dist/jssm.es5.iife.js +1 -1
- package/dist/jssm.es6.mjs +1 -1
- package/dist/jssm_viz.cjs +1 -1
- package/dist/jssm_viz.iife.cjs +1 -1
- package/dist/jssm_viz.mjs +1 -1
- package/jssm.es5.d.cts +18 -0
- package/jssm.es6.d.ts +18 -0
- package/jssm_viz.es5.d.cts +18 -0
- package/jssm_viz.es6.d.ts +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,10 +18,10 @@ Please edit the file it's derived from, instead: `./src/md/readme_base.md`
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
* Generated for version 5.
|
|
21
|
+
* Generated for version 5.145.0 at 6/22/2026, 4:40:44 PM
|
|
22
22
|
|
|
23
23
|
-->
|
|
24
|
-
# jssm 5.
|
|
24
|
+
# jssm 5.145.0
|
|
25
25
|
|
|
26
26
|
[**Try the live editor**](https://stonecypher.github.io/jssm-viz-demo/graph_explorer.html) ·
|
|
27
27
|
[Documentation](https://stonecypher.github.io/jssm/docs/) ·
|
|
@@ -312,7 +312,7 @@ That decision shows up everywhere downstream:
|
|
|
312
312
|
or run `npm run benny` against your own machine.
|
|
313
313
|
|
|
314
314
|
- **More thoroughly tested than any other JavaScript state-machine
|
|
315
|
-
library.** 7,
|
|
315
|
+
library.** 7,345 tests at 100.0% line coverage
|
|
316
316
|
([report](https://coveralls.io/github/StoneCypher/jssm)), plus
|
|
317
317
|
fuzz testing via `fast-check`, with parser test data across ten natural
|
|
318
318
|
languages and Emoji.
|
|
@@ -445,11 +445,11 @@ If your contribution is missing here, please open an issue.
|
|
|
445
445
|
|
|
446
446
|
<br/>
|
|
447
447
|
|
|
448
|
-
***7,
|
|
448
|
+
***7,345 tests***, run 82,387 times.
|
|
449
449
|
|
|
450
|
-
- 6,
|
|
451
|
-
- 758 fuzz tests with 73.
|
|
452
|
-
- 6,
|
|
450
|
+
- 6,587 specs with 100.0% coverage
|
|
451
|
+
- 758 fuzz tests with 73.6% coverage
|
|
452
|
+
- 6,962 TypeScript lines - 1.1 tests per line, 11.8 generated tests per line
|
|
453
453
|
|
|
454
454
|
[](https://github.com/StoneCypher/jssm/actions)
|
|
455
455
|
[](https://www.npmjs.com/package/jssm)
|
package/dist/cdn/instance.js
CHANGED
|
@@ -23512,11 +23512,48 @@ var constants = /*#__PURE__*/Object.freeze({
|
|
|
23512
23512
|
* Useful for runtime diagnostics and for embedding in serialized machine
|
|
23513
23513
|
* snapshots so that deserializers can detect version-skew.
|
|
23514
23514
|
*/
|
|
23515
|
-
const version = "5.
|
|
23515
|
+
const version = "5.145.0";
|
|
23516
23516
|
|
|
23517
23517
|
// whargarbl lots of these return arrays could/should be sets
|
|
23518
23518
|
const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
|
|
23519
23519
|
const empty_string_set = new Set();
|
|
23520
|
+
// The spatial fields (besides `handler`, which every hook needs) that each
|
|
23521
|
+
// hook kind requires, mirroring exactly what `set_hook` reads per case. Used
|
|
23522
|
+
// to validate a HookDescription so a mis-shaped one is rejected rather than
|
|
23523
|
+
// silently registering a dead hook — e.g. an `exit` hook given `to` instead of
|
|
23524
|
+
// `from` would otherwise intern `undefined` and never fire (#734). Typed as a
|
|
23525
|
+
// `Record` over the kind union so the table is exhaustive at compile time:
|
|
23526
|
+
// adding a hook kind without listing its fields is a build error.
|
|
23527
|
+
const hook_required_fields = {
|
|
23528
|
+
'hook': ['from', 'to'],
|
|
23529
|
+
'named': ['from', 'to', 'action'],
|
|
23530
|
+
'global action': ['action'],
|
|
23531
|
+
'any action': [],
|
|
23532
|
+
'standard transition': [],
|
|
23533
|
+
'main transition': [],
|
|
23534
|
+
'forced transition': [],
|
|
23535
|
+
'any transition': [],
|
|
23536
|
+
'entry': ['to'],
|
|
23537
|
+
'exit': ['from'],
|
|
23538
|
+
'after': ['from'],
|
|
23539
|
+
'post hook': ['from', 'to'],
|
|
23540
|
+
'post named': ['from', 'to', 'action'],
|
|
23541
|
+
'post global action': ['action'],
|
|
23542
|
+
'post any action': [],
|
|
23543
|
+
'post standard transition': [],
|
|
23544
|
+
'post main transition': [],
|
|
23545
|
+
'post forced transition': [],
|
|
23546
|
+
'post any transition': [],
|
|
23547
|
+
'post entry': ['to'],
|
|
23548
|
+
'post exit': ['from'],
|
|
23549
|
+
'pre everything': [],
|
|
23550
|
+
'everything': [],
|
|
23551
|
+
'pre post everything': [],
|
|
23552
|
+
'post everything': [],
|
|
23553
|
+
};
|
|
23554
|
+
// The spatial fields a hook descriptor can carry, checked against the per-kind
|
|
23555
|
+
// requirements above.
|
|
23556
|
+
const hook_spatial_fields = ['from', 'to', 'action'];
|
|
23520
23557
|
/*********
|
|
23521
23558
|
*
|
|
23522
23559
|
* An internal method meant to take a series of declarations and fold them into
|
|
@@ -23974,10 +24011,16 @@ class Machine {
|
|
|
23974
24011
|
this._state_labels.set(key, labelled[0].value);
|
|
23975
24012
|
}
|
|
23976
24013
|
});
|
|
23977
|
-
//
|
|
23978
|
-
//
|
|
23979
|
-
//
|
|
23980
|
-
//
|
|
24014
|
+
// Duplicate-edge guard for the construction loop below, keyed
|
|
24015
|
+
// from -> (to -> Set<slot>). A "slot" distinguishes edges that share a
|
|
24016
|
+
// (from, to) pair: an action's name for an actioned edge, or '' for the one
|
|
24017
|
+
// permitted plain action-less edge. Multiple edges between the same pair
|
|
24018
|
+
// are allowed when they carry distinct actions (#325; the self-loop case is
|
|
24019
|
+
// #531), since they dispatch unambiguously through `action(name)`. A
|
|
24020
|
+
// probability-bearing action-less edge is exempt from the guard entirely,
|
|
24021
|
+
// so a weighted fan-out may name the same target more than once. The
|
|
24022
|
+
// nested Map+Set keeps the check O(1) per edge rather than an O(out-degree)
|
|
24023
|
+
// scan (which made construction O(V*E) on dense graphs). #673
|
|
23981
24024
|
const seen_edges = new Map();
|
|
23982
24025
|
// walk the transitions. single-lookup cursor fetches: each endpoint was
|
|
23983
24026
|
// previously a get followed by a has on the same key (four hashes per
|
|
@@ -24001,23 +24044,35 @@ class Machine {
|
|
|
24001
24044
|
cursor_to = { name: tr.to, from: [], to: [], complete: complete.includes(tr.to) };
|
|
24002
24045
|
this._new_state(cursor_to);
|
|
24003
24046
|
}
|
|
24004
|
-
//
|
|
24005
|
-
//
|
|
24006
|
-
//
|
|
24007
|
-
|
|
24008
|
-
|
|
24009
|
-
|
|
24010
|
-
|
|
24011
|
-
|
|
24012
|
-
|
|
24013
|
-
if (
|
|
24014
|
-
|
|
24015
|
-
|
|
24016
|
-
else {
|
|
24017
|
-
seen_to.add(tr.to);
|
|
24047
|
+
// record (from -> to) adjacency once per distinct target, even when
|
|
24048
|
+
// several edges connect the pair, so the `to`/`from` arrays stay sets of
|
|
24049
|
+
// state names. #673
|
|
24050
|
+
let to_slots = seen_edges.get(tr.from);
|
|
24051
|
+
if (to_slots === undefined) {
|
|
24052
|
+
to_slots = new Map();
|
|
24053
|
+
seen_edges.set(tr.from, to_slots);
|
|
24054
|
+
}
|
|
24055
|
+
let slots = to_slots.get(tr.to);
|
|
24056
|
+
if (slots === undefined) {
|
|
24057
|
+
slots = new Set();
|
|
24058
|
+
to_slots.set(tr.to, slots);
|
|
24018
24059
|
cursor_from.to.push(tr.to);
|
|
24019
24060
|
cursor_to.from.push(tr.from);
|
|
24020
24061
|
}
|
|
24062
|
+
// duplicate-edge guard. A probability-bearing action-less edge is exempt
|
|
24063
|
+
// (a weighted fan-out may repeat a target); every other edge claims a slot
|
|
24064
|
+
// — its action name, or '' for the one plain action-less edge — and a
|
|
24065
|
+
// repeated slot throws. Distinct actions between the same pair coexist
|
|
24066
|
+
// (#325/#531).
|
|
24067
|
+
const edge_exempt = (!tr.action) && (tr.probability !== undefined);
|
|
24068
|
+
if (!edge_exempt) {
|
|
24069
|
+
const slot = tr.action ? tr.action : '';
|
|
24070
|
+
if (slots.has(slot)) {
|
|
24071
|
+
throw new JssmError(this, `already has ${JSON.stringify(tr.from)} to ${JSON.stringify(tr.to)}`
|
|
24072
|
+
+ (tr.action ? ` on action ${JSON.stringify(tr.action)}` : ''));
|
|
24073
|
+
}
|
|
24074
|
+
slots.add(slot);
|
|
24075
|
+
}
|
|
24021
24076
|
// add the edge; note its id
|
|
24022
24077
|
this._edges.push(tr);
|
|
24023
24078
|
const thisEdgeId = this._edges.length - 1;
|
|
@@ -24043,14 +24098,23 @@ class Machine {
|
|
|
24043
24098
|
from_mapping = new Map();
|
|
24044
24099
|
this._edge_map.set(tr.from, from_mapping);
|
|
24045
24100
|
}
|
|
24046
|
-
//
|
|
24047
|
-
|
|
24101
|
+
// first-declared wins: when several edges share a (from, to) pair (parallel
|
|
24102
|
+
// action edges, #325), lookup_transition_for resolves to the first one
|
|
24103
|
+
// declared, so it agrees with edges_between(...)[0].
|
|
24104
|
+
if (!from_mapping.has(tr.to)) {
|
|
24105
|
+
from_mapping.set(tr.to, thisEdgeId);
|
|
24106
|
+
}
|
|
24048
24107
|
// numeric mirror of the (from, to) endpoint mapping. intern() rather
|
|
24049
24108
|
// than id_of(): idempotent, and returns number (not number|undefined)
|
|
24050
24109
|
// since both endpoints were just created above if missing.
|
|
24051
24110
|
const from_id = this._state_interner.intern(tr.from);
|
|
24052
24111
|
const to_id = this._state_interner.intern(tr.to);
|
|
24053
|
-
|
|
24112
|
+
// first-declared wins (see _edge_map above): the transition fast-path that
|
|
24113
|
+
// reads this index resolves parallel (from, to) pairs to the first edge.
|
|
24114
|
+
const pair = pair_key(from_id, to_id);
|
|
24115
|
+
if (!this._edge_id_by_pair.has(pair)) {
|
|
24116
|
+
this._edge_id_by_pair.set(pair, thisEdgeId);
|
|
24117
|
+
}
|
|
24054
24118
|
this._edge_to_ids[thisEdgeId] = to_id;
|
|
24055
24119
|
// outbound adjacency: every edge originating at tr.from, regardless of action/target.
|
|
24056
24120
|
// _edge_map above keys a single edge per (from, to) and overwrites on collision, which
|
|
@@ -25774,7 +25838,44 @@ class Machine {
|
|
|
25774
25838
|
* calling this directly.
|
|
25775
25839
|
* @param HookDesc - A hook descriptor specifying kind, states, and handler.
|
|
25776
25840
|
*/
|
|
25841
|
+
/**
|
|
25842
|
+
* Validate a {@link HookDescription} before registration. Every hook needs
|
|
25843
|
+
* a `handler` function, and each kind's identifying spatial fields
|
|
25844
|
+
* (`from`/`to`/`action`) must be exactly those `set_hook` reads for that
|
|
25845
|
+
* kind — present when required, absent otherwise. This turns a mis-shaped
|
|
25846
|
+
* descriptor into a thrown error instead of a silently dead hook keyed on
|
|
25847
|
+
* `undefined` (e.g. an `exit` hook handed `to` instead of `from`, #734).
|
|
25848
|
+
*
|
|
25849
|
+
* @param HookDesc - The descriptor about to be registered.
|
|
25850
|
+
* @throws JssmError if the kind is unknown, the handler is not a function, a
|
|
25851
|
+
* required field is missing, or an inapplicable field is present.
|
|
25852
|
+
*
|
|
25853
|
+
* @example
|
|
25854
|
+
* const m = sm`a -> b;`;
|
|
25855
|
+
* // an exit hook is keyed by `from`, so supplying `to` is rejected:
|
|
25856
|
+
* expect(() => m.set_hook({ kind: 'exit', to: 'a', handler: () => true })).toThrow();
|
|
25857
|
+
*/
|
|
25858
|
+
_validate_hook_description(HookDesc) {
|
|
25859
|
+
const required = hook_required_fields[HookDesc.kind];
|
|
25860
|
+
if (required === undefined) {
|
|
25861
|
+
throw new JssmError(this, `unknown hook kind ${JSON.stringify(HookDesc.kind)}`);
|
|
25862
|
+
}
|
|
25863
|
+
if (typeof HookDesc.handler !== 'function') {
|
|
25864
|
+
throw new JssmError(this, `${HookDesc.kind} hook requires a handler function`);
|
|
25865
|
+
}
|
|
25866
|
+
for (const field of hook_spatial_fields) {
|
|
25867
|
+
const needed = required.includes(field);
|
|
25868
|
+
const present = HookDesc[field] !== undefined;
|
|
25869
|
+
if (needed && !present) {
|
|
25870
|
+
throw new JssmError(this, `${HookDesc.kind} hook requires '${field}'`);
|
|
25871
|
+
}
|
|
25872
|
+
if (!needed && present) {
|
|
25873
|
+
throw new JssmError(this, `${HookDesc.kind} hook does not take '${field}'`);
|
|
25874
|
+
}
|
|
25875
|
+
}
|
|
25876
|
+
}
|
|
25777
25877
|
set_hook(HookDesc) {
|
|
25878
|
+
this._validate_hook_description(HookDesc);
|
|
25778
25879
|
switch (HookDesc.kind) {
|
|
25779
25880
|
case 'hook': {
|
|
25780
25881
|
// Numeric pair key (#729). intern() rather than id_of(): a hook may
|
|
@@ -25916,8 +26017,8 @@ class Machine {
|
|
|
25916
26017
|
this._post_everything_hook = HookDesc.handler;
|
|
25917
26018
|
this._has_post_hooks = true;
|
|
25918
26019
|
break;
|
|
25919
|
-
default:
|
|
25920
|
-
|
|
26020
|
+
// No default: `_validate_hook_description` above rejects any unknown kind
|
|
26021
|
+
// before we reach here, so the switch is exhaustive over the known kinds.
|
|
25921
26022
|
}
|
|
25922
26023
|
// The hooked-state styling layer (tier 2.5 of resolve_state_config) depends
|
|
25923
26024
|
// on which states carry hooks, so registering a hook can change the composed
|
package/dist/cdn/viz.js
CHANGED
|
@@ -23537,11 +23537,48 @@ var constants = /*#__PURE__*/Object.freeze({
|
|
|
23537
23537
|
* Useful for runtime diagnostics and for embedding in serialized machine
|
|
23538
23538
|
* snapshots so that deserializers can detect version-skew.
|
|
23539
23539
|
*/
|
|
23540
|
-
const version = "5.
|
|
23540
|
+
const version = "5.145.0";
|
|
23541
23541
|
|
|
23542
23542
|
// whargarbl lots of these return arrays could/should be sets
|
|
23543
23543
|
const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
|
|
23544
23544
|
const empty_string_set = new Set();
|
|
23545
|
+
// The spatial fields (besides `handler`, which every hook needs) that each
|
|
23546
|
+
// hook kind requires, mirroring exactly what `set_hook` reads per case. Used
|
|
23547
|
+
// to validate a HookDescription so a mis-shaped one is rejected rather than
|
|
23548
|
+
// silently registering a dead hook — e.g. an `exit` hook given `to` instead of
|
|
23549
|
+
// `from` would otherwise intern `undefined` and never fire (#734). Typed as a
|
|
23550
|
+
// `Record` over the kind union so the table is exhaustive at compile time:
|
|
23551
|
+
// adding a hook kind without listing its fields is a build error.
|
|
23552
|
+
const hook_required_fields = {
|
|
23553
|
+
'hook': ['from', 'to'],
|
|
23554
|
+
'named': ['from', 'to', 'action'],
|
|
23555
|
+
'global action': ['action'],
|
|
23556
|
+
'any action': [],
|
|
23557
|
+
'standard transition': [],
|
|
23558
|
+
'main transition': [],
|
|
23559
|
+
'forced transition': [],
|
|
23560
|
+
'any transition': [],
|
|
23561
|
+
'entry': ['to'],
|
|
23562
|
+
'exit': ['from'],
|
|
23563
|
+
'after': ['from'],
|
|
23564
|
+
'post hook': ['from', 'to'],
|
|
23565
|
+
'post named': ['from', 'to', 'action'],
|
|
23566
|
+
'post global action': ['action'],
|
|
23567
|
+
'post any action': [],
|
|
23568
|
+
'post standard transition': [],
|
|
23569
|
+
'post main transition': [],
|
|
23570
|
+
'post forced transition': [],
|
|
23571
|
+
'post any transition': [],
|
|
23572
|
+
'post entry': ['to'],
|
|
23573
|
+
'post exit': ['from'],
|
|
23574
|
+
'pre everything': [],
|
|
23575
|
+
'everything': [],
|
|
23576
|
+
'pre post everything': [],
|
|
23577
|
+
'post everything': [],
|
|
23578
|
+
};
|
|
23579
|
+
// The spatial fields a hook descriptor can carry, checked against the per-kind
|
|
23580
|
+
// requirements above.
|
|
23581
|
+
const hook_spatial_fields = ['from', 'to', 'action'];
|
|
23545
23582
|
/*********
|
|
23546
23583
|
*
|
|
23547
23584
|
* An internal method meant to take a series of declarations and fold them into
|
|
@@ -23999,10 +24036,16 @@ class Machine {
|
|
|
23999
24036
|
this._state_labels.set(key, labelled[0].value);
|
|
24000
24037
|
}
|
|
24001
24038
|
});
|
|
24002
|
-
//
|
|
24003
|
-
//
|
|
24004
|
-
//
|
|
24005
|
-
//
|
|
24039
|
+
// Duplicate-edge guard for the construction loop below, keyed
|
|
24040
|
+
// from -> (to -> Set<slot>). A "slot" distinguishes edges that share a
|
|
24041
|
+
// (from, to) pair: an action's name for an actioned edge, or '' for the one
|
|
24042
|
+
// permitted plain action-less edge. Multiple edges between the same pair
|
|
24043
|
+
// are allowed when they carry distinct actions (#325; the self-loop case is
|
|
24044
|
+
// #531), since they dispatch unambiguously through `action(name)`. A
|
|
24045
|
+
// probability-bearing action-less edge is exempt from the guard entirely,
|
|
24046
|
+
// so a weighted fan-out may name the same target more than once. The
|
|
24047
|
+
// nested Map+Set keeps the check O(1) per edge rather than an O(out-degree)
|
|
24048
|
+
// scan (which made construction O(V*E) on dense graphs). #673
|
|
24006
24049
|
const seen_edges = new Map();
|
|
24007
24050
|
// walk the transitions. single-lookup cursor fetches: each endpoint was
|
|
24008
24051
|
// previously a get followed by a has on the same key (four hashes per
|
|
@@ -24026,23 +24069,35 @@ class Machine {
|
|
|
24026
24069
|
cursor_to = { name: tr.to, from: [], to: [], complete: complete.includes(tr.to) };
|
|
24027
24070
|
this._new_state(cursor_to);
|
|
24028
24071
|
}
|
|
24029
|
-
//
|
|
24030
|
-
//
|
|
24031
|
-
//
|
|
24032
|
-
|
|
24033
|
-
|
|
24034
|
-
|
|
24035
|
-
|
|
24036
|
-
|
|
24037
|
-
|
|
24038
|
-
if (
|
|
24039
|
-
|
|
24040
|
-
|
|
24041
|
-
else {
|
|
24042
|
-
seen_to.add(tr.to);
|
|
24072
|
+
// record (from -> to) adjacency once per distinct target, even when
|
|
24073
|
+
// several edges connect the pair, so the `to`/`from` arrays stay sets of
|
|
24074
|
+
// state names. #673
|
|
24075
|
+
let to_slots = seen_edges.get(tr.from);
|
|
24076
|
+
if (to_slots === undefined) {
|
|
24077
|
+
to_slots = new Map();
|
|
24078
|
+
seen_edges.set(tr.from, to_slots);
|
|
24079
|
+
}
|
|
24080
|
+
let slots = to_slots.get(tr.to);
|
|
24081
|
+
if (slots === undefined) {
|
|
24082
|
+
slots = new Set();
|
|
24083
|
+
to_slots.set(tr.to, slots);
|
|
24043
24084
|
cursor_from.to.push(tr.to);
|
|
24044
24085
|
cursor_to.from.push(tr.from);
|
|
24045
24086
|
}
|
|
24087
|
+
// duplicate-edge guard. A probability-bearing action-less edge is exempt
|
|
24088
|
+
// (a weighted fan-out may repeat a target); every other edge claims a slot
|
|
24089
|
+
// — its action name, or '' for the one plain action-less edge — and a
|
|
24090
|
+
// repeated slot throws. Distinct actions between the same pair coexist
|
|
24091
|
+
// (#325/#531).
|
|
24092
|
+
const edge_exempt = (!tr.action) && (tr.probability !== undefined);
|
|
24093
|
+
if (!edge_exempt) {
|
|
24094
|
+
const slot = tr.action ? tr.action : '';
|
|
24095
|
+
if (slots.has(slot)) {
|
|
24096
|
+
throw new JssmError(this, `already has ${JSON.stringify(tr.from)} to ${JSON.stringify(tr.to)}`
|
|
24097
|
+
+ (tr.action ? ` on action ${JSON.stringify(tr.action)}` : ''));
|
|
24098
|
+
}
|
|
24099
|
+
slots.add(slot);
|
|
24100
|
+
}
|
|
24046
24101
|
// add the edge; note its id
|
|
24047
24102
|
this._edges.push(tr);
|
|
24048
24103
|
const thisEdgeId = this._edges.length - 1;
|
|
@@ -24068,14 +24123,23 @@ class Machine {
|
|
|
24068
24123
|
from_mapping = new Map();
|
|
24069
24124
|
this._edge_map.set(tr.from, from_mapping);
|
|
24070
24125
|
}
|
|
24071
|
-
//
|
|
24072
|
-
|
|
24126
|
+
// first-declared wins: when several edges share a (from, to) pair (parallel
|
|
24127
|
+
// action edges, #325), lookup_transition_for resolves to the first one
|
|
24128
|
+
// declared, so it agrees with edges_between(...)[0].
|
|
24129
|
+
if (!from_mapping.has(tr.to)) {
|
|
24130
|
+
from_mapping.set(tr.to, thisEdgeId);
|
|
24131
|
+
}
|
|
24073
24132
|
// numeric mirror of the (from, to) endpoint mapping. intern() rather
|
|
24074
24133
|
// than id_of(): idempotent, and returns number (not number|undefined)
|
|
24075
24134
|
// since both endpoints were just created above if missing.
|
|
24076
24135
|
const from_id = this._state_interner.intern(tr.from);
|
|
24077
24136
|
const to_id = this._state_interner.intern(tr.to);
|
|
24078
|
-
|
|
24137
|
+
// first-declared wins (see _edge_map above): the transition fast-path that
|
|
24138
|
+
// reads this index resolves parallel (from, to) pairs to the first edge.
|
|
24139
|
+
const pair = pair_key(from_id, to_id);
|
|
24140
|
+
if (!this._edge_id_by_pair.has(pair)) {
|
|
24141
|
+
this._edge_id_by_pair.set(pair, thisEdgeId);
|
|
24142
|
+
}
|
|
24079
24143
|
this._edge_to_ids[thisEdgeId] = to_id;
|
|
24080
24144
|
// outbound adjacency: every edge originating at tr.from, regardless of action/target.
|
|
24081
24145
|
// _edge_map above keys a single edge per (from, to) and overwrites on collision, which
|
|
@@ -25799,7 +25863,44 @@ class Machine {
|
|
|
25799
25863
|
* calling this directly.
|
|
25800
25864
|
* @param HookDesc - A hook descriptor specifying kind, states, and handler.
|
|
25801
25865
|
*/
|
|
25866
|
+
/**
|
|
25867
|
+
* Validate a {@link HookDescription} before registration. Every hook needs
|
|
25868
|
+
* a `handler` function, and each kind's identifying spatial fields
|
|
25869
|
+
* (`from`/`to`/`action`) must be exactly those `set_hook` reads for that
|
|
25870
|
+
* kind — present when required, absent otherwise. This turns a mis-shaped
|
|
25871
|
+
* descriptor into a thrown error instead of a silently dead hook keyed on
|
|
25872
|
+
* `undefined` (e.g. an `exit` hook handed `to` instead of `from`, #734).
|
|
25873
|
+
*
|
|
25874
|
+
* @param HookDesc - The descriptor about to be registered.
|
|
25875
|
+
* @throws JssmError if the kind is unknown, the handler is not a function, a
|
|
25876
|
+
* required field is missing, or an inapplicable field is present.
|
|
25877
|
+
*
|
|
25878
|
+
* @example
|
|
25879
|
+
* const m = sm`a -> b;`;
|
|
25880
|
+
* // an exit hook is keyed by `from`, so supplying `to` is rejected:
|
|
25881
|
+
* expect(() => m.set_hook({ kind: 'exit', to: 'a', handler: () => true })).toThrow();
|
|
25882
|
+
*/
|
|
25883
|
+
_validate_hook_description(HookDesc) {
|
|
25884
|
+
const required = hook_required_fields[HookDesc.kind];
|
|
25885
|
+
if (required === undefined) {
|
|
25886
|
+
throw new JssmError(this, `unknown hook kind ${JSON.stringify(HookDesc.kind)}`);
|
|
25887
|
+
}
|
|
25888
|
+
if (typeof HookDesc.handler !== 'function') {
|
|
25889
|
+
throw new JssmError(this, `${HookDesc.kind} hook requires a handler function`);
|
|
25890
|
+
}
|
|
25891
|
+
for (const field of hook_spatial_fields) {
|
|
25892
|
+
const needed = required.includes(field);
|
|
25893
|
+
const present = HookDesc[field] !== undefined;
|
|
25894
|
+
if (needed && !present) {
|
|
25895
|
+
throw new JssmError(this, `${HookDesc.kind} hook requires '${field}'`);
|
|
25896
|
+
}
|
|
25897
|
+
if (!needed && present) {
|
|
25898
|
+
throw new JssmError(this, `${HookDesc.kind} hook does not take '${field}'`);
|
|
25899
|
+
}
|
|
25900
|
+
}
|
|
25901
|
+
}
|
|
25802
25902
|
set_hook(HookDesc) {
|
|
25903
|
+
this._validate_hook_description(HookDesc);
|
|
25803
25904
|
switch (HookDesc.kind) {
|
|
25804
25905
|
case 'hook': {
|
|
25805
25906
|
// Numeric pair key (#729). intern() rather than id_of(): a hook may
|
|
@@ -25941,8 +26042,8 @@ class Machine {
|
|
|
25941
26042
|
this._post_everything_hook = HookDesc.handler;
|
|
25942
26043
|
this._has_post_hooks = true;
|
|
25943
26044
|
break;
|
|
25944
|
-
default:
|
|
25945
|
-
|
|
26045
|
+
// No default: `_validate_hook_description` above rejects any unknown kind
|
|
26046
|
+
// before we reach here, so the switch is exhaustive over the known kinds.
|
|
25946
26047
|
}
|
|
25947
26048
|
// The hooked-state styling layer (tier 2.5 of resolve_state_config) depends
|
|
25948
26049
|
// on which states carry hooks, so registering a hook can change the composed
|
|
@@ -29182,37 +29283,55 @@ function colored_label(tr, which, color) {
|
|
|
29182
29283
|
function states_to_edges_string(u_jssm, l_states, state_index, state_kinds) {
|
|
29183
29284
|
const strike = new Set();
|
|
29184
29285
|
const kind_of = (s) => { var _a; return (_a = state_kinds.get(s)) !== null && _a !== void 0 ? _a : 'base'; };
|
|
29286
|
+
// Render one solo directed edge `s -> ex` for transition `tr`.
|
|
29287
|
+
const solo_edge = (s, ex, tr) => {
|
|
29288
|
+
const ex_kind = kind_of(ex);
|
|
29289
|
+
const tailColor = text_color(ex_kind, '_solo');
|
|
29290
|
+
const labelInline = colored_label(tr, 'taillabel', tailColor);
|
|
29291
|
+
const label = transition_label(tr);
|
|
29292
|
+
const maybeLabel = label ? `taillabel="${doublequote(label)}";` : '';
|
|
29293
|
+
const arrowHead = arrow_for(tr);
|
|
29294
|
+
const edgeInline = `${maybeLabel}arrowhead=${arrowHead};color="${line_color(ex_kind, tr.kind, '_solo')}"`;
|
|
29295
|
+
return `${node_of(s, state_index)}->${node_of(ex, state_index)} [${labelInline}${edgeInline}];`;
|
|
29296
|
+
};
|
|
29185
29297
|
return l_states.map((s) => u_jssm.list_exits(s).map((ex) => {
|
|
29186
29298
|
if (strike.has(`${s}|${ex}`)) {
|
|
29187
29299
|
return '';
|
|
29188
29300
|
}
|
|
29189
|
-
const
|
|
29190
|
-
if (
|
|
29301
|
+
const forward = u_jssm.edges_between(s, ex);
|
|
29302
|
+
if (forward.length === 0) {
|
|
29191
29303
|
return '';
|
|
29192
29304
|
} // belt-and-suspenders; list_exits should always have a corresponding transition
|
|
29193
|
-
const
|
|
29194
|
-
|
|
29195
|
-
|
|
29196
|
-
|
|
29197
|
-
//
|
|
29198
|
-
|
|
29199
|
-
|
|
29200
|
-
|
|
29201
|
-
|
|
29202
|
-
|
|
29203
|
-
|
|
29204
|
-
|
|
29205
|
-
|
|
29206
|
-
|
|
29207
|
-
|
|
29208
|
-
|
|
29209
|
-
|
|
29210
|
-
|
|
29211
|
-
|
|
29212
|
-
|
|
29305
|
+
const reverse = (s !== ex) ? u_jssm.edges_between(ex, s) : [];
|
|
29306
|
+
// Bidirectional merge stays the default for the common case: exactly one
|
|
29307
|
+
// edge each way between two distinct states draws as a single `dir=both`
|
|
29308
|
+
// edge with head/tail labels. Parallel edges (#325) or self-loops fall
|
|
29309
|
+
// through to one directed line per edge.
|
|
29310
|
+
if (forward.length === 1 && reverse.length === 1) {
|
|
29311
|
+
const edge_tr = forward[0];
|
|
29312
|
+
const pair_tr = reverse[0];
|
|
29313
|
+
const s_kind = kind_of(s);
|
|
29314
|
+
const ex_kind = kind_of(ex);
|
|
29315
|
+
// colored-HTML labels (per-direction, with text colors)
|
|
29316
|
+
const headColor = text_color(s_kind, '_1');
|
|
29317
|
+
const tailColor = text_color(ex_kind, '_2');
|
|
29318
|
+
const labelInline = colored_label(pair_tr, 'headlabel', headColor) +
|
|
29319
|
+
colored_label(edge_tr, 'taillabel', tailColor);
|
|
29320
|
+
// plain `headlabel="..."` / `taillabel="..."` fallback
|
|
29321
|
+
const label = transition_label(edge_tr);
|
|
29322
|
+
const rlabel = transition_label(pair_tr);
|
|
29323
|
+
const maybeLabel = label ? `taillabel="${doublequote(label)}";` : '';
|
|
29324
|
+
const maybeRLabel = rlabel ? `headlabel="${doublequote(rlabel)}";` : '';
|
|
29325
|
+
const arrowHead = arrow_for(edge_tr);
|
|
29326
|
+
const arrowTail = arrow_for(pair_tr);
|
|
29327
|
+
const edgeInline = `${maybeLabel}${maybeRLabel}arrowhead=${arrowHead};arrowtail=${arrowTail};dir=both;color="${line_color(ex_kind, edge_tr.kind, '_1')}:${line_color(s_kind, pair_tr.kind, '_2')}"`;
|
|
29213
29328
|
strike.add(`${ex}|${s}`);
|
|
29329
|
+
return `${node_of(s, state_index)}->${node_of(ex, state_index)} [${labelInline}${edgeInline}];`;
|
|
29214
29330
|
}
|
|
29215
|
-
|
|
29331
|
+
// one directed line per forward edge — parallel action edges (#325) and
|
|
29332
|
+
// self-loops (#531) draw each transition separately. The reverse edges,
|
|
29333
|
+
// if any, render when the loop reaches the (ex, s) pair (not struck here).
|
|
29334
|
+
return forward.map((tr) => solo_edge(s, ex, tr)).join(' ');
|
|
29216
29335
|
}).join(' ')).join(' ');
|
|
29217
29336
|
}
|
|
29218
29337
|
/**
|
|
@@ -108,7 +108,7 @@ function parseFslArgs(argv, spec) {
|
|
|
108
108
|
return { positional, flags, helpText };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const getVersion = () => "5.
|
|
111
|
+
const getVersion = () => "5.145.0";
|
|
112
112
|
const SPEC = {
|
|
113
113
|
flags: {
|
|
114
114
|
help: { short: "h", boolean: true },
|
|
@@ -214,6 +214,6 @@ async function cli(argv) {
|
|
|
214
214
|
async function main() {
|
|
215
215
|
const argv = process.argv.slice(2);
|
|
216
216
|
const code = await cli(argv);
|
|
217
|
-
process.
|
|
217
|
+
process.exitCode = code;
|
|
218
218
|
}
|
|
219
219
|
void main();
|