jssm 5.143.35 → 5.144.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.
- package/README.md +7 -7
- package/dist/cdn/instance.js +361 -6
- package/dist/cdn/viz.js +361 -6
- package/dist/cli/fsl-export-system-prompt.cjs +219 -0
- 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 +153 -1
- package/dist/deno/jssm.js +1 -1
- package/dist/deno/jssm_intern.d.ts +21 -1
- package/dist/deno/jssm_types.d.ts +82 -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 +223 -0
- package/jssm.es6.d.ts +223 -0
- package/jssm_viz.es5.d.cts +223 -0
- package/jssm_viz.es6.d.ts +223 -0
- package/package.json +3 -2
- package/dist/deno/README.md +0 -461
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.144.0 at 6/21/2026, 3:13:56 PM
|
|
22
22
|
|
|
23
23
|
-->
|
|
24
|
-
# jssm 5.
|
|
24
|
+
# jssm 5.144.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,305 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,305 tests***, run 82,347 times.
|
|
449
449
|
|
|
450
|
-
- 6,
|
|
451
|
-
- 758 fuzz tests with
|
|
452
|
-
- 6,
|
|
450
|
+
- 6,547 specs with 100.0% coverage
|
|
451
|
+
- 758 fuzz tests with 74.4% coverage
|
|
452
|
+
- 6,801 TypeScript lines - 1.1 tests per line, 12.1 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
|
@@ -22922,6 +22922,9 @@ const base_active_state_style$5 = {
|
|
|
22922
22922
|
textColor: 'white',
|
|
22923
22923
|
backgroundColor: 'dodgerblue4'
|
|
22924
22924
|
};
|
|
22925
|
+
const base_hooked_state_style$5 = {
|
|
22926
|
+
shape: 'component'
|
|
22927
|
+
};
|
|
22925
22928
|
const base_terminal_state_style$5 = {
|
|
22926
22929
|
textColor: 'white',
|
|
22927
22930
|
backgroundColor: 'crimson'
|
|
@@ -22938,6 +22941,7 @@ const base_theme = {
|
|
|
22938
22941
|
start: base_start_state_style$5,
|
|
22939
22942
|
end: base_end_state_style$5,
|
|
22940
22943
|
terminal: base_terminal_state_style$5,
|
|
22944
|
+
hooked: base_hooked_state_style$5,
|
|
22941
22945
|
active: base_active_state_style$5};
|
|
22942
22946
|
|
|
22943
22947
|
const base_state_style$4 = {
|
|
@@ -23370,6 +23374,30 @@ function pair_key(a, b) {
|
|
|
23370
23374
|
? (a * a) + a + b
|
|
23371
23375
|
: (b * b) + a;
|
|
23372
23376
|
}
|
|
23377
|
+
/**
|
|
23378
|
+
* Inverse of {@link pair_key}: recovers the ordered pair `(a, b)` that was
|
|
23379
|
+
* packed into a Szudzik key. Exact for any key produced by `pair_key` over
|
|
23380
|
+
* non-negative integer inputs, so `un_pair_key(pair_key(a, b))` round-trips
|
|
23381
|
+
* to `[a, b]`. Used to walk interned, pair-keyed maps (e.g. the hook tables)
|
|
23382
|
+
* back to their original `(from_id, to_id)` ids for {@link Interner.name_of}.
|
|
23383
|
+
*
|
|
23384
|
+
* Behavior is only defined for keys `pair_key` actually emits; a NaN key (the
|
|
23385
|
+
* unknown-name sentinel) yields `[NaN, NaN]`, never a spurious real pair.
|
|
23386
|
+
*
|
|
23387
|
+
* @param z - A key produced by `pair_key`.
|
|
23388
|
+
* @returns The ordered pair `[a, b]` such that `pair_key(a, b) === z`.
|
|
23389
|
+
*
|
|
23390
|
+
* @example
|
|
23391
|
+
* un_pair_key(27); // [2, 5]
|
|
23392
|
+
* un_pair_key(32); // [5, 2] — order preserved
|
|
23393
|
+
*
|
|
23394
|
+
* @see pair_key
|
|
23395
|
+
*/
|
|
23396
|
+
function un_pair_key(z) {
|
|
23397
|
+
const s = Math.floor(Math.sqrt(z));
|
|
23398
|
+
const l = z - (s * s);
|
|
23399
|
+
return (l < s) ? [l, s] : [s, l - s];
|
|
23400
|
+
}
|
|
23373
23401
|
|
|
23374
23402
|
/*******
|
|
23375
23403
|
*
|
|
@@ -23486,7 +23514,7 @@ var constants = /*#__PURE__*/Object.freeze({
|
|
|
23486
23514
|
* Useful for runtime diagnostics and for embedding in serialized machine
|
|
23487
23515
|
* snapshots so that deserializers can detect version-skew.
|
|
23488
23516
|
*/
|
|
23489
|
-
const version = "5.
|
|
23517
|
+
const version = "5.144.1";
|
|
23490
23518
|
|
|
23491
23519
|
// whargarbl lots of these return arrays could/should be sets
|
|
23492
23520
|
const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
|
|
@@ -25889,6 +25917,12 @@ class Machine {
|
|
|
25889
25917
|
default:
|
|
25890
25918
|
throw new JssmError(this, `Unknown hook type ${HookDesc.kind}, should be impossible`);
|
|
25891
25919
|
}
|
|
25920
|
+
// The hooked-state styling layer (tier 2.5 of resolve_state_config) depends
|
|
25921
|
+
// on which states carry hooks, so registering a hook can change the composed
|
|
25922
|
+
// style of a state. The static config cache assumes tiers 1–5 are fixed
|
|
25923
|
+
// after construction; invalidate it so styling stays correct when a hook is
|
|
25924
|
+
// added after a style has already been computed and memoized.
|
|
25925
|
+
this._static_state_config_cache.clear();
|
|
25892
25926
|
// fire the registration event for inspector tools (#638)
|
|
25893
25927
|
this._fire('hook-registration', { description: HookDesc });
|
|
25894
25928
|
}
|
|
@@ -26061,6 +26095,9 @@ class Machine {
|
|
|
26061
26095
|
throw new JssmError(this, `Unknown hook type ${HookDesc.kind}, should be impossible`);
|
|
26062
26096
|
}
|
|
26063
26097
|
if (removed) {
|
|
26098
|
+
// See set_hook: the hooked-state styling layer depends on which states
|
|
26099
|
+
// carry hooks, so removing one can change a state's composed style.
|
|
26100
|
+
this._static_state_config_cache.clear();
|
|
26064
26101
|
this._fire('hook-removal', { description: HookDesc });
|
|
26065
26102
|
}
|
|
26066
26103
|
return removed;
|
|
@@ -27387,12 +27424,319 @@ class Machine {
|
|
|
27387
27424
|
get active_state_style() {
|
|
27388
27425
|
return this._active_state_style;
|
|
27389
27426
|
}
|
|
27390
|
-
|
|
27427
|
+
/********
|
|
27428
|
+
*
|
|
27429
|
+
* Generate the uniform observational-hook registry — every currently
|
|
27430
|
+
* registered hook projected onto a normalized `(kind, target, phase)` row
|
|
27431
|
+
* (megaspec §12, → #1357). The registry is *generated* on demand by
|
|
27432
|
+
* walking the concrete per-kind storage tables rather than maintained as a
|
|
27433
|
+
* second copy, so it can never drift from the tables {@link Machine.set_hook}
|
|
27434
|
+
* actually dispatches into. It is the single source of truth behind the
|
|
27435
|
+
* introspection accessors ({@link Machine.has_hook}, {@link Machine.hooks_on})
|
|
27436
|
+
* and the `hooked_state` viz styling.
|
|
27437
|
+
*
|
|
27438
|
+
* Targets are normalized: edge hooks become `{ scope: 'edge', from, to }`
|
|
27439
|
+
* (named hooks add `action`), entry/exit/after become `{ scope: 'state' }`,
|
|
27440
|
+
* global-action hooks become `{ scope: 'action' }`, and the `any-*`,
|
|
27441
|
+
* transition-class, and `everything` observers become `{ scope: 'global' }`.
|
|
27442
|
+
*
|
|
27443
|
+
* ```typescript
|
|
27444
|
+
* const m = sm`a 'go' -> b;`;
|
|
27445
|
+
* m.hook_entry('b', () => true);
|
|
27446
|
+
* m.hook_registry();
|
|
27447
|
+
* // => [ { kind: 'entry', phase: 'pre', target: { scope: 'state', state: 'b' } } ]
|
|
27448
|
+
* ```
|
|
27449
|
+
*
|
|
27450
|
+
* @returns Every registered hook as a {@link HookRegistryEntry}, in a stable
|
|
27451
|
+
* table-walk order (pre-phase tables first, then post-phase).
|
|
27452
|
+
*
|
|
27391
27453
|
*/
|
|
27392
|
-
|
|
27393
|
-
|
|
27394
|
-
|
|
27395
|
-
|
|
27454
|
+
hook_registry() {
|
|
27455
|
+
const entries = [];
|
|
27456
|
+
// The hot-path hook tables are keyed by interned integer ids (states and
|
|
27457
|
+
// actions) and, for edges, by `pair_key(from_id, to_id)`. Decode each key
|
|
27458
|
+
// back to its original name so the registry speaks states/actions, never
|
|
27459
|
+
// ids. The lone exception is `_after_hooks`, deliberately string-keyed.
|
|
27460
|
+
const state_name = (id) => this._state_interner.name_of(id);
|
|
27461
|
+
const action_name = (id) => this._action_interner.name_of(id);
|
|
27462
|
+
// edge tables: pair_key(from_id, to_id) -> handler
|
|
27463
|
+
const push_edges = (table, kind, phase) => {
|
|
27464
|
+
table.forEach((_handler, pk) => {
|
|
27465
|
+
const [fid, tid] = un_pair_key(pk);
|
|
27466
|
+
entries.push({ kind, phase, target: { scope: 'edge', from: state_name(fid), to: state_name(tid) } });
|
|
27467
|
+
});
|
|
27468
|
+
};
|
|
27469
|
+
// named-edge tables: pair_key(from_id, to_id) -> action_id -> handler
|
|
27470
|
+
const push_named = (table, kind, phase) => {
|
|
27471
|
+
table.forEach((byAction, pk) => {
|
|
27472
|
+
const [fid, tid] = un_pair_key(pk);
|
|
27473
|
+
const from = state_name(fid), to = state_name(tid);
|
|
27474
|
+
byAction.forEach((_handler, aid) => {
|
|
27475
|
+
entries.push({ kind, phase, target: { scope: 'edge', from, to, action: action_name(aid) } });
|
|
27476
|
+
});
|
|
27477
|
+
});
|
|
27478
|
+
};
|
|
27479
|
+
// entry/exit tables: interned state_id -> handler
|
|
27480
|
+
const push_states = (table, kind, phase) => {
|
|
27481
|
+
table.forEach((_handler, sid) => {
|
|
27482
|
+
entries.push({ kind, phase, target: { scope: 'state', state: state_name(sid) } });
|
|
27483
|
+
});
|
|
27484
|
+
};
|
|
27485
|
+
// the `after` table is the lone string-keyed exception: state name -> handler
|
|
27486
|
+
const push_states_by_name = (table, kind, phase) => {
|
|
27487
|
+
table.forEach((_handler, state) => {
|
|
27488
|
+
entries.push({ kind, phase, target: { scope: 'state', state: state } });
|
|
27489
|
+
});
|
|
27490
|
+
};
|
|
27491
|
+
// global-action tables: interned action_id -> handler
|
|
27492
|
+
const push_actions = (table, kind, phase) => {
|
|
27493
|
+
table.forEach((_handler, aid) => {
|
|
27494
|
+
entries.push({ kind, phase, target: { scope: 'action', action: action_name(aid) } });
|
|
27495
|
+
});
|
|
27496
|
+
};
|
|
27497
|
+
const push_global = (handler, kind, phase) => {
|
|
27498
|
+
if (handler !== undefined) {
|
|
27499
|
+
entries.push({ kind, phase, target: { scope: 'global' } });
|
|
27500
|
+
}
|
|
27501
|
+
};
|
|
27502
|
+
// FSL boundary hooks: subject name -> { onEnter?, onExit? }, fired post-
|
|
27503
|
+
// commit. Each present direction becomes its own row, all phase 'post'.
|
|
27504
|
+
const push_boundary = (table, enterKind, exitKind, target_of) => {
|
|
27505
|
+
table.forEach((bh, subject) => {
|
|
27506
|
+
if (bh.onEnter !== undefined) {
|
|
27507
|
+
entries.push({ kind: enterKind, phase: 'post', target: target_of(subject) });
|
|
27508
|
+
}
|
|
27509
|
+
if (bh.onExit !== undefined) {
|
|
27510
|
+
entries.push({ kind: exitKind, phase: 'post', target: target_of(subject) });
|
|
27511
|
+
}
|
|
27512
|
+
});
|
|
27513
|
+
};
|
|
27514
|
+
// pre-phase, edge- and state-keyed tables
|
|
27515
|
+
push_edges(this._hooks, 'hook', 'pre');
|
|
27516
|
+
push_named(this._named_hooks, 'named', 'pre');
|
|
27517
|
+
push_states(this._entry_hooks, 'entry', 'pre');
|
|
27518
|
+
push_states(this._exit_hooks, 'exit', 'pre');
|
|
27519
|
+
push_states_by_name(this._after_hooks, 'after', 'pre');
|
|
27520
|
+
push_actions(this._global_action_hooks, 'global action', 'pre');
|
|
27521
|
+
// pre-phase, global singletons
|
|
27522
|
+
push_global(this._any_action_hook, 'any action', 'pre');
|
|
27523
|
+
push_global(this._standard_transition_hook, 'standard transition', 'pre');
|
|
27524
|
+
push_global(this._main_transition_hook, 'main transition', 'pre');
|
|
27525
|
+
push_global(this._forced_transition_hook, 'forced transition', 'pre');
|
|
27526
|
+
push_global(this._any_transition_hook, 'any transition', 'pre');
|
|
27527
|
+
push_global(this._pre_everything_hook, 'pre everything', 'pre');
|
|
27528
|
+
push_global(this._everything_hook, 'everything', 'pre');
|
|
27529
|
+
// post-phase, edge- and state-keyed tables
|
|
27530
|
+
push_edges(this._post_hooks, 'post hook', 'post');
|
|
27531
|
+
push_named(this._post_named_hooks, 'post named', 'post');
|
|
27532
|
+
push_states(this._post_entry_hooks, 'post entry', 'post');
|
|
27533
|
+
push_states(this._post_exit_hooks, 'post exit', 'post');
|
|
27534
|
+
push_actions(this._post_global_action_hooks, 'post global action', 'post');
|
|
27535
|
+
// post-phase, global singletons
|
|
27536
|
+
push_global(this._post_any_action_hook, 'post any action', 'post');
|
|
27537
|
+
push_global(this._post_standard_transition_hook, 'post standard transition', 'post');
|
|
27538
|
+
push_global(this._post_main_transition_hook, 'post main transition', 'post');
|
|
27539
|
+
push_global(this._post_forced_transition_hook, 'post forced transition', 'post');
|
|
27540
|
+
push_global(this._post_any_transition_hook, 'post any transition', 'post');
|
|
27541
|
+
push_global(this._pre_post_everything_hook, 'pre post everything', 'post');
|
|
27542
|
+
push_global(this._post_everything_hook, 'post everything', 'post');
|
|
27543
|
+
// FSL boundary hooks (post-commit): group and plain-state subjects
|
|
27544
|
+
push_boundary(this._group_hooks, 'group enter', 'group exit', (group) => ({ scope: 'group', group }));
|
|
27545
|
+
push_boundary(this._state_hooks, 'state enter', 'state exit', (state) => ({ scope: 'state', state: state }));
|
|
27546
|
+
return entries;
|
|
27547
|
+
}
|
|
27548
|
+
/********
|
|
27549
|
+
*
|
|
27550
|
+
* Does a single registry entry reference the state `state`? An entry
|
|
27551
|
+
* references a state when it is a `'state'`-scoped hook on that state, or an
|
|
27552
|
+
* `'edge'`-scoped hook whose `from` or `to` is that state. `'action'`- and
|
|
27553
|
+
* `'global'`-scoped entries reference no particular state. This is the
|
|
27554
|
+
* predicate behind both per-state introspection and the `hooked_state`
|
|
27555
|
+
* styling layer.
|
|
27556
|
+
*
|
|
27557
|
+
* @param entry The registry entry to test.
|
|
27558
|
+
* @param state The state name to test membership of.
|
|
27559
|
+
* @returns `true` when the entry observes that state.
|
|
27560
|
+
*
|
|
27561
|
+
*/
|
|
27562
|
+
static _entry_touches_state(entry, state) {
|
|
27563
|
+
const t = entry.target;
|
|
27564
|
+
if (t.scope === 'state') {
|
|
27565
|
+
return t.state === state;
|
|
27566
|
+
}
|
|
27567
|
+
if (t.scope === 'edge') {
|
|
27568
|
+
return t.from === state || t.to === state;
|
|
27569
|
+
}
|
|
27570
|
+
return false;
|
|
27571
|
+
}
|
|
27572
|
+
/********
|
|
27573
|
+
*
|
|
27574
|
+
* Does a single registry entry match a `{ from, to, action? }` edge query?
|
|
27575
|
+
* Only `'edge'`-scoped entries can match. When the query omits `action`
|
|
27576
|
+
* the entry's action (if any) is ignored; when the query supplies `action`
|
|
27577
|
+
* it must match exactly.
|
|
27578
|
+
*
|
|
27579
|
+
* @param entry The registry entry to test.
|
|
27580
|
+
* @param from The edge origin to match.
|
|
27581
|
+
* @param to The edge destination to match.
|
|
27582
|
+
* @param action Optional named action to match exactly.
|
|
27583
|
+
* @returns `true` when the entry observes that edge.
|
|
27584
|
+
*
|
|
27585
|
+
*/
|
|
27586
|
+
static _entry_matches_edge(entry, from, to, action) {
|
|
27587
|
+
const t = entry.target;
|
|
27588
|
+
if (t.scope !== 'edge') {
|
|
27589
|
+
return false;
|
|
27590
|
+
}
|
|
27591
|
+
if (t.from !== from || t.to !== to) {
|
|
27592
|
+
return false;
|
|
27593
|
+
}
|
|
27594
|
+
if (action !== undefined) {
|
|
27595
|
+
return t.action === action;
|
|
27596
|
+
}
|
|
27597
|
+
return true;
|
|
27598
|
+
}
|
|
27599
|
+
/********
|
|
27600
|
+
*
|
|
27601
|
+
* Does a single registry entry match an action name? Both `'action'`-scoped
|
|
27602
|
+
* hooks (global-action hooks) and named-edge hooks carrying that action
|
|
27603
|
+
* count as matches.
|
|
27604
|
+
*
|
|
27605
|
+
* @param entry The registry entry to test.
|
|
27606
|
+
* @param action The action name to match.
|
|
27607
|
+
* @returns `true` when the entry observes that action.
|
|
27608
|
+
*
|
|
27609
|
+
*/
|
|
27610
|
+
static _entry_matches_action(entry, action) {
|
|
27611
|
+
const t = entry.target;
|
|
27612
|
+
if (t.scope === 'action') {
|
|
27613
|
+
return t.action === action;
|
|
27614
|
+
}
|
|
27615
|
+
if (t.scope === 'edge') {
|
|
27616
|
+
return t.action === action;
|
|
27617
|
+
}
|
|
27618
|
+
return false;
|
|
27619
|
+
}
|
|
27620
|
+
/********
|
|
27621
|
+
*
|
|
27622
|
+
* Does a single registry entry match a named state group? Only
|
|
27623
|
+
* `'group'`-scoped entries (FSL group-boundary hooks) match. Group hooks
|
|
27624
|
+
* are matched by group name only — they deliberately do not propagate to
|
|
27625
|
+
* member states, so a member-state query never returns them.
|
|
27626
|
+
*
|
|
27627
|
+
* @param entry The registry entry to test.
|
|
27628
|
+
* @param group The group name to match.
|
|
27629
|
+
* @returns `true` when the entry observes that group's boundary.
|
|
27630
|
+
*
|
|
27631
|
+
*/
|
|
27632
|
+
static _entry_matches_group(entry, group) {
|
|
27633
|
+
const t = entry.target;
|
|
27634
|
+
if (t.scope === 'group') {
|
|
27635
|
+
return t.group === group;
|
|
27636
|
+
}
|
|
27637
|
+
return false;
|
|
27638
|
+
}
|
|
27639
|
+
/********
|
|
27640
|
+
*
|
|
27641
|
+
* Return every registry entry observing the given target (megaspec §12).
|
|
27642
|
+
* The `query` selects the target shape:
|
|
27643
|
+
*
|
|
27644
|
+
* - a bare **state name** matches entry/exit/after hooks on that state, its
|
|
27645
|
+
* state-boundary hooks, and every edge hook touching it (`from` or `to`),
|
|
27646
|
+
* - a `{ from, to, action? }` **edge** matches edge hooks on that
|
|
27647
|
+
* transition (optionally narrowed to the named action),
|
|
27648
|
+
* - a `{ action }` **action** matches global-action and named-edge hooks
|
|
27649
|
+
* carrying that action,
|
|
27650
|
+
* - a `{ group }` **group** matches that group's boundary hooks (group hooks
|
|
27651
|
+
* are matched by name only and do not propagate to member states).
|
|
27652
|
+
*
|
|
27653
|
+
* ```typescript
|
|
27654
|
+
* const m = sm`a 'go' -> b;`;
|
|
27655
|
+
* m.hook_entry('b', () => true);
|
|
27656
|
+
* m.hooks_on('b').length; // 1
|
|
27657
|
+
* m.hooks_on({ from: 'a', to: 'b' }); // [] (no edge hook registered)
|
|
27658
|
+
* ```
|
|
27659
|
+
*
|
|
27660
|
+
* @param query The {@link HookQuery} naming the target to inspect.
|
|
27661
|
+
* @returns The matching {@link HookRegistryEntry} rows (possibly empty).
|
|
27662
|
+
*
|
|
27663
|
+
*/
|
|
27664
|
+
hooks_on(query) {
|
|
27665
|
+
const registry = this.hook_registry();
|
|
27666
|
+
if (typeof query === 'string') {
|
|
27667
|
+
return registry.filter(e => Machine._entry_touches_state(e, query));
|
|
27668
|
+
}
|
|
27669
|
+
// An edge query is distinguished by carrying `from` (it may *also* carry
|
|
27670
|
+
// `action`, which narrows the edge — so this must be tested before the
|
|
27671
|
+
// action-only case, whose discriminator `action` an edge query can share).
|
|
27672
|
+
if ('from' in query) {
|
|
27673
|
+
return registry.filter(e => Machine._entry_matches_edge(e, query.from, query.to, query.action));
|
|
27674
|
+
}
|
|
27675
|
+
if ('group' in query) {
|
|
27676
|
+
return registry.filter(e => Machine._entry_matches_group(e, query.group));
|
|
27677
|
+
}
|
|
27678
|
+
return registry.filter(e => Machine._entry_matches_action(e, query.action));
|
|
27679
|
+
}
|
|
27680
|
+
/********
|
|
27681
|
+
*
|
|
27682
|
+
* Is at least one observational hook bound to the given target (megaspec
|
|
27683
|
+
* §12)? The `query` is read exactly as in {@link Machine.hooks_on}. An
|
|
27684
|
+
* optional `phase` narrows the test to pre- or post-transition hooks only;
|
|
27685
|
+
* omitted, either phase satisfies it.
|
|
27686
|
+
*
|
|
27687
|
+
* ```typescript
|
|
27688
|
+
* const m = sm`a -> b;`;
|
|
27689
|
+
* m.has_hook('b'); // false
|
|
27690
|
+
* m.hook_entry('b', () => true);
|
|
27691
|
+
* m.has_hook('b'); // true
|
|
27692
|
+
* m.has_hook('b', 'post'); // false (the entry hook is pre-phase)
|
|
27693
|
+
* ```
|
|
27694
|
+
*
|
|
27695
|
+
* @param query The {@link HookQuery} naming the target to inspect.
|
|
27696
|
+
* @param phase Optional {@link HookPhase} to restrict the test to.
|
|
27697
|
+
* @returns `true` when a matching hook exists.
|
|
27698
|
+
*
|
|
27699
|
+
*/
|
|
27700
|
+
has_hook(query, phase) {
|
|
27701
|
+
const matches = this.hooks_on(query);
|
|
27702
|
+
if (phase === undefined) {
|
|
27703
|
+
return matches.length > 0;
|
|
27704
|
+
}
|
|
27705
|
+
return matches.some(e => e.phase === phase);
|
|
27706
|
+
}
|
|
27707
|
+
/********
|
|
27708
|
+
*
|
|
27709
|
+
* Does the given state carry any observational hook — i.e. should it receive
|
|
27710
|
+
* the `hooked_state` viz styling? True when an entry/exit/after hook is
|
|
27711
|
+
* bound to the state, any edge hook touches it, or the state has its own
|
|
27712
|
+
* boundary hook. Group-boundary hooks do *not* count here — they are
|
|
27713
|
+
* matched by group only and never propagate to member states. Powers the
|
|
27714
|
+
* `hooked` styling layer in {@link Machine.resolve_state_config}; replaces
|
|
27715
|
+
* the long-stubbed `has_hooks` placeholder (megaspec §12).
|
|
27716
|
+
*
|
|
27717
|
+
* ```typescript
|
|
27718
|
+
* const m = sm`a -> b;`;
|
|
27719
|
+
* m.state_has_hooks('a'); // false
|
|
27720
|
+
* m.hook_exit('a', () => true);
|
|
27721
|
+
* m.state_has_hooks('a'); // true
|
|
27722
|
+
* ```
|
|
27723
|
+
*
|
|
27724
|
+
* @param state The state to test.
|
|
27725
|
+
* @returns `true` when the state is observed by at least one hook.
|
|
27726
|
+
*
|
|
27727
|
+
*/
|
|
27728
|
+
state_has_hooks(state) {
|
|
27729
|
+
// Boundary hooks are a separate mechanism that sets neither _has_hooks nor
|
|
27730
|
+
// _has_post_hooks, so the fast-out must also consult the boundary tables —
|
|
27731
|
+
// otherwise a state whose only hook is a boundary hook reports unhooked.
|
|
27732
|
+
if (!this._has_hooks
|
|
27733
|
+
&& !this._has_post_hooks
|
|
27734
|
+
&& (this._state_hooks.size === 0)
|
|
27735
|
+
&& (this._group_hooks.size === 0)) {
|
|
27736
|
+
return false;
|
|
27737
|
+
}
|
|
27738
|
+
return this.hook_registry().some(e => Machine._entry_touches_state(e, state));
|
|
27739
|
+
}
|
|
27396
27740
|
/********
|
|
27397
27741
|
*
|
|
27398
27742
|
* Returns the list of resolved theme implementations for this machine, in
|
|
@@ -27530,6 +27874,17 @@ class Machine {
|
|
|
27530
27874
|
});
|
|
27531
27875
|
// tier 2 — default_state_config (implicit root over all states)
|
|
27532
27876
|
acc = merge_state_config(acc, this._state_style);
|
|
27877
|
+
// tier 2.5 — hooked-state styling, applied when the state carries any
|
|
27878
|
+
// observational or boundary hook. Sits above the root default and below
|
|
27879
|
+
// the per-kind/group/per-state tiers, preserving the historical layer
|
|
27880
|
+
// order the pre-cascade `style_for` used. See {@link state_has_hooks}.
|
|
27881
|
+
if (this.state_has_hooks(state)) {
|
|
27882
|
+
acc = merge_state_config(acc, base_theme.hooked);
|
|
27883
|
+
themes.forEach(theme => { if (theme.hooked) {
|
|
27884
|
+
acc = merge_state_config(acc, theme.hooked);
|
|
27885
|
+
} });
|
|
27886
|
+
acc = merge_state_config(acc, this._hooked_state_style);
|
|
27887
|
+
}
|
|
27533
27888
|
// tier 3 — static per-kind defaults, selected by structural kind
|
|
27534
27889
|
if (this.state_is_terminal(state)) {
|
|
27535
27890
|
acc = merge_state_config(acc, base_theme.terminal);
|