jssm 5.145.6 → 5.147.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 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.145.6 at 6/23/2026, 6:05:53 AM
21
+ * Generated for version 5.147.0 at 6/23/2026, 6:50:50 AM
22
22
 
23
23
  -->
24
- # jssm 5.145.6
24
+ # jssm 5.147.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,395 tests at 100.0% line coverage
315
+ library.** 7,443 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,395 tests***, run 82,437 times.
448
+ ***7,443 tests***, run 82,485 times.
449
449
 
450
- - 6,637 specs with 100.0% coverage
451
- - 758 fuzz tests with 73.6% coverage
452
- - 6,967 TypeScript lines - 1.1 tests per line, 11.8 generated tests per line
450
+ - 6,685 specs with 100.0% coverage
451
+ - 758 fuzz tests with 69.6% coverage
452
+ - 7,276 TypeScript lines - 1.0 tests per line, 11.3 generated tests per line
453
453
 
454
454
  [![Actions Status](https://github.com/StoneCypher/jssm/workflows/Node%20CI/badge.svg)](https://github.com/StoneCypher/jssm/actions)
455
455
  [![NPM version](https://img.shields.io/npm/v/jssm.svg)](https://www.npmjs.com/package/jssm)
@@ -187,6 +187,92 @@
187
187
  }
188
188
  ]
189
189
  },
190
+ {
191
+ "kind": "javascript-module",
192
+ "path": "src/ts/wc/fsl_effective_properties_wc.ts",
193
+ "declarations": [
194
+ {
195
+ "kind": "class",
196
+ "description": "Read-only panel that displays the parent machine's **resolved FSL\nproperties** for the current state — the values produced by the full\noverride chain (machine `property … default …` → per-state\n`state X: { property … }`), as returned by `machine.props()`. Refreshes on\nevery transition, so consumers can watch a property's effective value change\nas the machine moves between states.\n\nBinds to the host via closest_wc (matching both `fsl-instance` and\nthe deprecated `jssm-instance`). Display-only; never drives the machine.\n\nv1 shows the FSL `property` bag (`machine.props()`). The render-time visual\nstyle resolution (shape/color used by `<fsl-viz>`) is a separate viz-pipeline\nconcern and is not surfaced here.",
197
+ "name": "FslEffectiveProperties",
198
+ "cssProperties": [
199
+ {
200
+ "description": "Gap between rows.",
201
+ "name": "--fsl-effective-properties-gap",
202
+ "default": "0.25rem"
203
+ }
204
+ ],
205
+ "members": [
206
+ {
207
+ "kind": "field",
208
+ "name": "_host",
209
+ "type": {
210
+ "text": "JssmInstanceHost | null"
211
+ },
212
+ "privacy": "private",
213
+ "default": "null",
214
+ "description": "Parent host reference; cleared on disconnect."
215
+ },
216
+ {
217
+ "kind": "field",
218
+ "name": "_sub",
219
+ "type": {
220
+ "text": "(() => void) | null"
221
+ },
222
+ "privacy": "private",
223
+ "default": "null",
224
+ "description": "Unsubscribe callback from the host machine's `transition` subscription."
225
+ },
226
+ {
227
+ "kind": "field",
228
+ "name": "_entries",
229
+ "type": {
230
+ "text": "Array<[string, string]> | null"
231
+ },
232
+ "privacy": "private",
233
+ "default": "null",
234
+ "description": "Resolved property entries (`[name, stringified value]`) for the current\nstate, or `null` before the panel has bound to a host machine."
235
+ },
236
+ {
237
+ "kind": "method",
238
+ "name": "_refresh",
239
+ "privacy": "private",
240
+ "return": {
241
+ "type": {
242
+ "text": "void"
243
+ }
244
+ },
245
+ "parameters": [
246
+ {
247
+ "name": "host",
248
+ "type": {
249
+ "text": "JssmInstanceHost"
250
+ },
251
+ "description": "The bound parent host whose machine to snapshot."
252
+ }
253
+ ],
254
+ "description": "Read the resolved property bag (`machine.props()`) into reactive entries,\ntriggering a re-render."
255
+ }
256
+ ],
257
+ "superclass": {
258
+ "name": "LitElement",
259
+ "package": "lit"
260
+ },
261
+ "tagName": "fsl-effective-properties",
262
+ "customElement": true
263
+ }
264
+ ],
265
+ "exports": [
266
+ {
267
+ "kind": "js",
268
+ "name": "FslEffectiveProperties",
269
+ "declaration": {
270
+ "name": "FslEffectiveProperties",
271
+ "module": "src/ts/wc/fsl_effective_properties_wc.ts"
272
+ }
273
+ }
274
+ ]
275
+ },
190
276
  {
191
277
  "kind": "javascript-module",
192
278
  "path": "src/ts/wc/fsl_hook_wc.ts",
@@ -435,6 +521,132 @@
435
521
  }
436
522
  ]
437
523
  },
524
+ {
525
+ "kind": "javascript-module",
526
+ "path": "src/ts/wc/fsl_info_panel_wc.ts",
527
+ "declarations": [
528
+ {
529
+ "kind": "class",
530
+ "description": "Read-only state-inspector web component for a parent `<fsl-instance>`.\n\nSlotted into the host's `info-panel` slot (or nested anywhere inside it), it\ndisplays the machine's current state, the most recent transition\n(`from → to via action`), the currently-legal exit actions, and the\nterminal / complete flags. Every field refreshes on each `transition`\nevent.\n\nDisplay-only: it never drives the machine. It binds by walking up to the\nhost via closest_wc (which matches both the canonical `fsl-instance`\nand the deprecated `jssm-instance` host tags), so it works under either.",
531
+ "name": "FslInfoPanel",
532
+ "cssProperties": [
533
+ {
534
+ "description": "Vertical gap between rows.",
535
+ "name": "--fsl-info-panel-gap",
536
+ "default": "0.25rem"
537
+ }
538
+ ],
539
+ "members": [
540
+ {
541
+ "kind": "field",
542
+ "name": "_host",
543
+ "type": {
544
+ "text": "JssmInstanceHost | null"
545
+ },
546
+ "privacy": "private",
547
+ "default": "null",
548
+ "description": "Parent host reference, set in `connectedCallback` when one is found.\nCleared on disconnect so a stale deferred subscription cannot fire."
549
+ },
550
+ {
551
+ "kind": "field",
552
+ "name": "_sub",
553
+ "type": {
554
+ "text": "(() => void) | null"
555
+ },
556
+ "privacy": "private",
557
+ "default": "null",
558
+ "description": "Unsubscribe callback from the host machine's `transition` subscription."
559
+ },
560
+ {
561
+ "kind": "field",
562
+ "name": "_current",
563
+ "type": {
564
+ "text": "string | null"
565
+ },
566
+ "privacy": "private",
567
+ "default": "null",
568
+ "description": "Current state name; `null` until the panel has bound to a host machine."
569
+ },
570
+ {
571
+ "kind": "field",
572
+ "name": "_actions",
573
+ "type": {
574
+ "text": "string"
575
+ },
576
+ "privacy": "private",
577
+ "default": "''",
578
+ "description": "Space-separated legal exit actions for the current state."
579
+ },
580
+ {
581
+ "kind": "field",
582
+ "name": "_terminal",
583
+ "type": {
584
+ "text": "boolean"
585
+ },
586
+ "privacy": "private",
587
+ "default": "false",
588
+ "description": "Whether the current state has no exits."
589
+ },
590
+ {
591
+ "kind": "field",
592
+ "name": "_complete",
593
+ "type": {
594
+ "text": "boolean"
595
+ },
596
+ "privacy": "private",
597
+ "default": "false",
598
+ "description": "Whether the current state is a `complete` state."
599
+ },
600
+ {
601
+ "kind": "field",
602
+ "name": "_last",
603
+ "type": {
604
+ "text": "LastTransition | null"
605
+ },
606
+ "privacy": "private",
607
+ "default": "null",
608
+ "description": "Most recent transition, or `null` before the first one."
609
+ },
610
+ {
611
+ "kind": "method",
612
+ "name": "_refresh",
613
+ "privacy": "private",
614
+ "return": {
615
+ "type": {
616
+ "text": "void"
617
+ }
618
+ },
619
+ "parameters": [
620
+ {
621
+ "name": "host",
622
+ "type": {
623
+ "text": "JssmInstanceHost"
624
+ },
625
+ "description": "The bound parent host whose machine to snapshot."
626
+ }
627
+ ],
628
+ "description": "Read the current machine snapshot into the reactive fields, triggering a\nre-render. Called once on bind and again on every transition. The bound\nhost is passed in by the caller (which already holds a non-null reference),\nso no re-null-check is needed here."
629
+ }
630
+ ],
631
+ "superclass": {
632
+ "name": "LitElement",
633
+ "package": "lit"
634
+ },
635
+ "tagName": "fsl-info-panel",
636
+ "customElement": true
637
+ }
638
+ ],
639
+ "exports": [
640
+ {
641
+ "kind": "js",
642
+ "name": "FslInfoPanel",
643
+ "declaration": {
644
+ "name": "FslInfoPanel",
645
+ "module": "src/ts/wc/fsl_info_panel_wc.ts"
646
+ }
647
+ }
648
+ ]
649
+ },
438
650
  {
439
651
  "kind": "javascript-module",
440
652
  "path": "src/ts/wc/fsl_instance_wc.ts",
@@ -564,7 +776,7 @@
564
776
  "name": "viz"
565
777
  },
566
778
  {
567
- "description": "Editor surface slot.",
779
+ "description": "Editor surface slot (`<fsl-editor>`, #659).",
568
780
  "name": "editor"
569
781
  },
570
782
  {
@@ -572,13 +784,37 @@
572
784
  "name": "actions"
573
785
  },
574
786
  {
575
- "description": "Slot for toolbar UI.",
787
+ "description": "Slot for toolbar UI (`<fsl-toolbar>`, #660).",
576
788
  "name": "toolbar"
577
789
  },
578
790
  {
579
- "description": "Slot for an info / status panel.",
791
+ "description": "Slot for an info / status panel (`<fsl-info-panel>`, #661).",
580
792
  "name": "info-panel"
581
793
  },
794
+ {
795
+ "description": "Slot for the visited-state timeline (`<fsl-history>`, #662).",
796
+ "name": "history"
797
+ },
798
+ {
799
+ "description": "Slot for the typed-data tree view (`<fsl-data-inspector>`, #663).",
800
+ "name": "data-inspector"
801
+ },
802
+ {
803
+ "description": "Slot for the hook-firing log (`<fsl-hook-log>`, #664).",
804
+ "name": "hook-log"
805
+ },
806
+ {
807
+ "description": "Slot for the resolved-properties panel (`<fsl-effective-properties>`, #665).",
808
+ "name": "effective-properties"
809
+ },
810
+ {
811
+ "description": "Slot for the random-walk simulation (`<fsl-simulation>`, #668).",
812
+ "name": "simulation"
813
+ },
814
+ {
815
+ "description": "Slot for the export menu (`<fsl-export>`, #667).",
816
+ "name": "export"
817
+ },
582
818
  {
583
819
  "description": "Footer slot.",
584
820
  "name": "footer"
@@ -626,6 +862,38 @@
626
862
  "default": "[]",
627
863
  "description": "Unsubscribe callbacks for every `machine.on(...)` / `machine.once(...)`\r\nsubscription installed from a `<jssm-on>` child during\r\n`connectedCallback`. Walked in `disconnectedCallback`."
628
864
  },
865
+ {
866
+ "kind": "field",
867
+ "name": "REEMITTED_EVENTS",
868
+ "type": {
869
+ "text": "readonly string[]"
870
+ },
871
+ "privacy": "private",
872
+ "static": true,
873
+ "readonly": true,
874
+ "default": "[ 'transition', 'entry', 'exit', 'terminal', 'complete', 'action', 'rejection', 'override', 'data-change', 'timeout', 'error', ]",
875
+ "description": "Library event names this WC re-emits as DOM `CustomEvent`s, fulfilling\r\nmechanism 4 of #639. Each library `machine.on(name, ...)` is bridged to\r\na `fsl-<name>` DOM event (`composed`, `bubbling`) so slotted content and\r\noutside consumers can observe machine activity declaratively.\r\n\r\n`fsl-` is the canonical prefix (matching the canonical `<fsl-*>` tag\r\nnames); the older `jssm-*` event prose in #639 predates that naming flip.\r\nEvents are NOT double-emitted under both prefixes — a symmetric listener\r\nwould otherwise run twice per machine event."
876
+ },
877
+ {
878
+ "kind": "field",
879
+ "name": "_reemit_unsubscribes",
880
+ "type": {
881
+ "text": "Array<() => void>"
882
+ },
883
+ "privacy": "private",
884
+ "default": "[]",
885
+ "description": "Unsubscribe callbacks for the host-level mechanism-4 re-emission\r\nsubscriptions installed in _install_event_reemission. Distinct\r\nfrom _on_unsubscribes (which belongs to `<jssm-on>` children)."
886
+ },
887
+ {
888
+ "kind": "field",
889
+ "name": "_pending_dom_events",
890
+ "type": {
891
+ "text": "Array<{ name: string; detail: unknown }>"
892
+ },
893
+ "privacy": "private",
894
+ "default": "[]",
895
+ "description": "Library events captured during the current transition, awaiting DOM\r\nre-dispatch once Lit commits the next render. Dispatching here (rather\r\nthan synchronously from the machine subscription) guarantees the #639\r\nordering: mechanism 1/3 reflection and mechanism 2 slot re-pick are all\r\nin place before a mechanism-4 listener runs."
896
+ },
629
897
  {
630
898
  "kind": "field",
631
899
  "name": "registry",
@@ -745,6 +1013,28 @@
745
1013
  },
746
1014
  "description": "Prefix used in synthetic `//# sourceURL=jssm-hook:<prefix><n>` annotations\r\nfor inline-body hooks compiled by this element."
747
1015
  },
1016
+ {
1017
+ "kind": "method",
1018
+ "name": "_install_event_reemission",
1019
+ "privacy": "private",
1020
+ "return": {
1021
+ "type": {
1022
+ "text": "void"
1023
+ }
1024
+ },
1025
+ "description": "Install the mechanism-4 (#639) re-emission subscriptions: one\r\n`machine.on(name, ...)` per entry in REEMITTED_EVENTS. Each\r\ncaptured library event is queued and re-dispatched as a `fsl-<name>` DOM\r\nevent after the next render commit (see updated).\r\n\r\nSubscribing is also what *enables* the gated observation events: the\r\nlibrary suppresses `transition` / `entry` / `exit` / etc. while no\r\nlisteners exist (#670), so this host subscription is the bridge that\r\nturns them on.\r\n\r\nThe subscription handler paints state reflection eagerly so that a host\r\ndriven directly via `host.machine.action(...)` (bypassing do)\r\nstill updates its `current-state` attribute and `--current-state`\r\nproperty."
1026
+ },
1027
+ {
1028
+ "kind": "method",
1029
+ "name": "_flush_pending_dom_events",
1030
+ "privacy": "private",
1031
+ "return": {
1032
+ "type": {
1033
+ "text": "void"
1034
+ }
1035
+ },
1036
+ "description": "Dispatch and clear the queue of pending DOM events. The queue is\r\nsnapshotted and reset *before* dispatching so that a listener which\r\nre-enters the machine (e.g. calls `host.machine.action(...)`\r\nsynchronously) enqueues into a fresh batch handled by the next update\r\ncycle, rather than mutating the array mid-iteration. This is the\r\ndocumented re-entrancy behavior: re-entrant transitions are deferred,\r\nnot dropped."
1037
+ },
748
1038
  {
749
1039
  "kind": "method",
750
1040
  "name": "_discover_jssm_actions",
@@ -787,6 +1077,13 @@
787
1077
  "description": "Reflect machine state onto host attributes and CSS custom properties.\r\nCalled after every transition and once during `connectedCallback`.\r\n\r\nMechanism 1 (#639): writes to host attributes.\r\nMechanism 3 (#639): writes to host inline-style custom properties."
788
1078
  }
789
1079
  ],
1080
+ "events": [
1081
+ {
1082
+ "type": {
1083
+ "text": "CustomEvent"
1084
+ }
1085
+ }
1086
+ ],
790
1087
  "attributes": [
791
1088
  {
792
1089
  "name": "fsl",
@@ -1123,6 +1420,32 @@
1123
1420
  }
1124
1421
  ],
1125
1422
  "description": "Registers a canonical custom-element tag and its synonym tag.\r\n\r\n`customElements.define` requires a distinct constructor per tag name, so\r\ncallers pass the canonical class and a thin subclass for the synonym.\r\nThe function is idempotent: if either tag is already registered it skips\r\nthat `define` call rather than throwing."
1423
+ },
1424
+ {
1425
+ "kind": "function",
1426
+ "name": "define_canonical",
1427
+ "return": {
1428
+ "type": {
1429
+ "text": "void"
1430
+ }
1431
+ },
1432
+ "parameters": [
1433
+ {
1434
+ "name": "canonical_tag",
1435
+ "type": {
1436
+ "text": "string"
1437
+ },
1438
+ "description": "The `fsl-*` tag name (e.g. `\"fsl-info-panel\"`)."
1439
+ },
1440
+ {
1441
+ "name": "CanonicalClass",
1442
+ "type": {
1443
+ "text": "CustomElementConstructor"
1444
+ },
1445
+ "description": "Constructor to register under `canonical_tag`."
1446
+ }
1447
+ ],
1448
+ "description": "Registers a single canonical `fsl-*` custom-element tag, with no `jssm-*`\r\nsynonym.\r\n\r\nThis is the registration path for **new** web components. The `jssm-*`\r\nprefix is a deprecated backward-compatibility alias retained only for the\r\ncomponents that shipped under that name (`<jssm-viz>`, `<jssm-instance>`,\r\n`<jssm-bind>`); new components are `fsl-*`-only for fsl.tools brand\r\nalignment, and the legacy synonyms are slated for removal in v6. Use\r\ndefine_with_synonym only when maintaining one of those pre-existing\r\ndual-named components.\r\n\r\nIdempotent: skips the `define` call when the tag is already registered."
1126
1449
  }
1127
1450
  ],
1128
1451
  "exports": [
@@ -1165,6 +1488,22 @@
1165
1488
  "name": "SynonymClass",
1166
1489
  "module": "src/ts/wc/wc_tag_helpers.ts"
1167
1490
  }
1491
+ },
1492
+ {
1493
+ "kind": "js",
1494
+ "name": "define_canonical",
1495
+ "declaration": {
1496
+ "name": "define_canonical",
1497
+ "module": "src/ts/wc/wc_tag_helpers.ts"
1498
+ }
1499
+ },
1500
+ {
1501
+ "kind": "custom-element-definition",
1502
+ "name": "canonical_tag",
1503
+ "declaration": {
1504
+ "name": "CanonicalClass",
1505
+ "module": "src/ts/wc/wc_tag_helpers.ts"
1506
+ }
1168
1507
  }
1169
1508
  ]
1170
1509
  }
@@ -23512,7 +23512,7 @@ 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.145.6";
23515
+ const version = "5.147.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;
@@ -29386,10 +29386,16 @@ function resolve_fsl_source(host, fsl_attr) {
29386
29386
  * @cssproperty [--current-state] - The machine's current state name as a CSS string token.
29387
29387
  * @slot title - Heading area for the instance.
29388
29388
  * @slot viz - Visualization slot; fallback is a placeholder string.
29389
- * @slot editor - Editor surface slot.
29389
+ * @slot editor - Editor surface slot (`<fsl-editor>`, #659).
29390
29390
  * @slot actions - Slot for action buttons / UI.
29391
- * @slot toolbar - Slot for toolbar UI.
29392
- * @slot info-panel - Slot for an info / status panel.
29391
+ * @slot toolbar - Slot for toolbar UI (`<fsl-toolbar>`, #660).
29392
+ * @slot info-panel - Slot for an info / status panel (`<fsl-info-panel>`, #661).
29393
+ * @slot history - Slot for the visited-state timeline (`<fsl-history>`, #662).
29394
+ * @slot data-inspector - Slot for the typed-data tree view (`<fsl-data-inspector>`, #663).
29395
+ * @slot hook-log - Slot for the hook-firing log (`<fsl-hook-log>`, #664).
29396
+ * @slot effective-properties - Slot for the resolved-properties panel (`<fsl-effective-properties>`, #665).
29397
+ * @slot simulation - Slot for the random-walk simulation (`<fsl-simulation>`, #668).
29398
+ * @slot export - Slot for the export menu (`<fsl-export>`, #667).
29393
29399
  * @slot footer - Footer slot.
29394
29400
  */
29395
29401
  class FslInstance extends i {
@@ -29423,6 +29429,20 @@ class FslInstance extends i {
29423
29429
  * `connectedCallback`. Walked in `disconnectedCallback`.
29424
29430
  */
29425
29431
  this._on_unsubscribes = [];
29432
+ /**
29433
+ * Unsubscribe callbacks for the host-level mechanism-4 re-emission
29434
+ * subscriptions installed in {@link _install_event_reemission}. Distinct
29435
+ * from {@link _on_unsubscribes} (which belongs to `<jssm-on>` children).
29436
+ */
29437
+ this._reemit_unsubscribes = [];
29438
+ /**
29439
+ * Library events captured during the current transition, awaiting DOM
29440
+ * re-dispatch once Lit commits the next render. Dispatching here (rather
29441
+ * than synchronously from the machine subscription) guarantees the #639
29442
+ * ordering: mechanism 1/3 reflection and mechanism 2 slot re-pick are all
29443
+ * in place before a mechanism-4 listener runs.
29444
+ */
29445
+ this._pending_dom_events = [];
29426
29446
  /**
29427
29447
  * Per-instance registry of named hook handlers consulted before
29428
29448
  * `globalThis` when resolving `<fsl-hook handler="name">` /
@@ -29503,8 +29523,9 @@ class FslInstance extends i {
29503
29523
  // Step 4: shadow DOM render is automatic via Lit; requesting an update
29504
29524
  // here ensures the first paint sees the freshly painted attributes.
29505
29525
  this.requestUpdate();
29506
- // TODO #638: subscribe to machine.on('transition', ...) once available
29507
- // and dispatch DOM CustomEvents from this element.
29526
+ // #639 mechanism 4: subscribe to library events and re-emit them as
29527
+ // DOM CustomEvents from this host (#638 supplies the event API).
29528
+ this._install_event_reemission();
29508
29529
  // #641: <jssm-hook> declarative discovery.
29509
29530
  this._install_declarative_hooks();
29510
29531
  // #643: <jssm-on> declarative event observation.
@@ -29583,14 +29604,87 @@ class FslInstance extends i {
29583
29604
  const host_id = this.getAttribute('id');
29584
29605
  return host_id !== null && host_id.length > 0 ? `${host_id}-` : '';
29585
29606
  }
29607
+ /**
29608
+ * Install the mechanism-4 (#639) re-emission subscriptions: one
29609
+ * `machine.on(name, ...)` per entry in {@link REEMITTED_EVENTS}. Each
29610
+ * captured library event is queued and re-dispatched as a `fsl-<name>` DOM
29611
+ * event after the next render commit (see {@link updated}).
29612
+ *
29613
+ * Subscribing is also what *enables* the gated observation events: the
29614
+ * library suppresses `transition` / `entry` / `exit` / etc. while no
29615
+ * listeners exist (#670), so this host subscription is the bridge that
29616
+ * turns them on.
29617
+ *
29618
+ * The subscription handler paints state reflection eagerly so that a host
29619
+ * driven directly via `host.machine.action(...)` (bypassing {@link do})
29620
+ * still updates its `current-state` attribute and `--current-state`
29621
+ * property.
29622
+ */
29623
+ _install_event_reemission() {
29624
+ const machine = this._machine;
29625
+ for (const name of FslInstance.REEMITTED_EVENTS) {
29626
+ // `as any` collapses the per-event detail typing — the WC is a
29627
+ // schema-erased entry point; per-event payload typing belongs upstream.
29628
+ const off = machine.on(name, (detail) => {
29629
+ this._pending_dom_events.push({ name, detail });
29630
+ this._paint_state_reflection();
29631
+ this.requestUpdate();
29632
+ });
29633
+ this._reemit_unsubscribes.push(off);
29634
+ }
29635
+ }
29636
+ /**
29637
+ * Lit lifecycle. After every committed render, flush any library events
29638
+ * captured since the last commit as DOM `CustomEvent`s. Deferring to this
29639
+ * point is what gives mechanism-4 listeners the #639 ordering guarantee:
29640
+ * host attributes (mechanism 1), CSS custom properties (mechanism 3), and
29641
+ * the state-specific slot (mechanism 2) are all current by the time a
29642
+ * `fsl-*` listener runs.
29643
+ *
29644
+ * @param changed - Lit's changed-property map (forwarded to super).
29645
+ */
29646
+ updated(changed) {
29647
+ super.updated(changed);
29648
+ this._flush_pending_dom_events();
29649
+ }
29650
+ /**
29651
+ * Dispatch and clear the queue of pending DOM events. The queue is
29652
+ * snapshotted and reset *before* dispatching so that a listener which
29653
+ * re-enters the machine (e.g. calls `host.machine.action(...)`
29654
+ * synchronously) enqueues into a fresh batch handled by the next update
29655
+ * cycle, rather than mutating the array mid-iteration. This is the
29656
+ * documented re-entrancy behavior: re-entrant transitions are deferred,
29657
+ * not dropped.
29658
+ */
29659
+ _flush_pending_dom_events() {
29660
+ if (this._pending_dom_events.length === 0) {
29661
+ return;
29662
+ }
29663
+ const batch = this._pending_dom_events;
29664
+ this._pending_dom_events = [];
29665
+ for (const ev of batch) {
29666
+ this.dispatchEvent(new CustomEvent(`fsl-${ev.name}`, {
29667
+ detail: ev.detail,
29668
+ bubbles: true,
29669
+ composed: true,
29670
+ }));
29671
+ }
29672
+ }
29586
29673
  /**
29587
29674
  * Lifecycle hook. Cleans up everything the WC installed at connect: hook
29588
29675
  * registrations from `<jssm-hook>`, event subscriptions from `<jssm-on>`,
29589
- * and DOM listeners from `<jssm-action>` / `data-jssm-action`.
29676
+ * mechanism-4 re-emission subscriptions, and DOM listeners from
29677
+ * `<jssm-action>` / `data-jssm-action`.
29590
29678
  */
29591
29679
  disconnectedCallback() {
29592
29680
  super.disconnectedCallback();
29593
- // TODO #638: unsubscribe from any direct machine.on(...) handlers added by host.
29681
+ // #639 mechanism 4: release host-level re-emission subscriptions and drop
29682
+ // any events that were queued but not yet flushed.
29683
+ for (const off of this._reemit_unsubscribes) {
29684
+ off();
29685
+ }
29686
+ this._reemit_unsubscribes = [];
29687
+ this._pending_dom_events = [];
29594
29688
  // #641: remove installed hooks.
29595
29689
  if (this._machine !== undefined) {
29596
29690
  const machine = this._machine;
@@ -29746,6 +29840,24 @@ class FslInstance extends i {
29746
29840
  <section class="info-panel">
29747
29841
  <slot name="info-panel"></slot>
29748
29842
  </section>
29843
+ <section class="history">
29844
+ <slot name="history"></slot>
29845
+ </section>
29846
+ <section class="data-inspector">
29847
+ <slot name="data-inspector"></slot>
29848
+ </section>
29849
+ <section class="hook-log">
29850
+ <slot name="hook-log"></slot>
29851
+ </section>
29852
+ <section class="effective-properties">
29853
+ <slot name="effective-properties"></slot>
29854
+ </section>
29855
+ <section class="simulation">
29856
+ <slot name="simulation"></slot>
29857
+ </section>
29858
+ <section class="export">
29859
+ <slot name="export"></slot>
29860
+ </section>
29749
29861
  <section class="state-section">
29750
29862
  <slot name=${state_slot_name}></slot>
29751
29863
  </section>
@@ -29769,6 +29881,21 @@ FslInstance.styles = i$3 `
29769
29881
  font-style: italic;
29770
29882
  }
29771
29883
  `;
29884
+ /**
29885
+ * Library event names this WC re-emits as DOM `CustomEvent`s, fulfilling
29886
+ * mechanism 4 of #639. Each library `machine.on(name, ...)` is bridged to
29887
+ * a `fsl-<name>` DOM event (`composed`, `bubbling`) so slotted content and
29888
+ * outside consumers can observe machine activity declaratively.
29889
+ *
29890
+ * `fsl-` is the canonical prefix (matching the canonical `<fsl-*>` tag
29891
+ * names); the older `jssm-*` event prose in #639 predates that naming flip.
29892
+ * Events are NOT double-emitted under both prefixes — a symmetric listener
29893
+ * would otherwise run twice per machine event.
29894
+ */
29895
+ FslInstance.REEMITTED_EVENTS = [
29896
+ 'transition', 'entry', 'exit', 'terminal', 'complete',
29897
+ 'action', 'rejection', 'override', 'data-change', 'timeout', 'error',
29898
+ ];
29772
29899
  /**
29773
29900
  * Lit reactive properties declaration. We declare `fsl` here (rather
29774
29901
  * than via a decorator) so the attribute observation stays explicit and
@@ -29779,7 +29906,15 @@ FslInstance.properties = {
29779
29906
  fsl: { type: String, reflect: false },
29780
29907
  };
29781
29908
 
29782
- /** Thin subclass so `<jssm-instance>` registers under a distinct constructor. */
29909
+ /**
29910
+ * Thin subclass so `<jssm-instance>` registers under a distinct constructor.
29911
+ *
29912
+ * @deprecated The `jssm-*` tag and the `JssmInstance` class alias are
29913
+ * deprecated since v5 in favor of the canonical `<fsl-instance>` /
29914
+ * {@link FslInstance}, for fsl.tools brand alignment. They remain functional
29915
+ * but are slated for removal in v6 (tracked in `v6_breaking_changes.json` on
29916
+ * the `v6` branch). New components are `fsl-*`-only.
29917
+ */
29783
29918
  class JssmInstance extends FslInstance {
29784
29919
  }
29785
29920
  define_with_synonym('fsl-instance', 'jssm-instance', FslInstance, JssmInstance);