@typeonce/effect-machine-devtools 0.25.0 → 0.26.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.
@@ -19,6 +19,7 @@ import {
19
19
  renderChart
20
20
  } from "./chart-renderer.js"
21
21
  import { type InputField, projectInputSchema } from "./input-form.js"
22
+ import { analyzeVisualization, type VisualizerAnalysis } from "./visualizer-analysis.js"
22
23
  import {
23
24
  branchTargetApi,
24
25
  type IncomingTransition,
@@ -186,7 +187,7 @@ const renderActivity = (activity: VisualizationActivity, navigate: StateNavigato
186
187
  const title = createElement("div", "card-title")
187
188
  title.append(createElement("strong", undefined, activityTitle(activity)))
188
189
  const flags = createElement("div", "card-flags")
189
- flags.append(badge(activity.type, "activity"))
190
+ flags.append(badge(activity.type, `activity badge-activity-${activity.type}`))
190
191
  header.append(title, flags)
191
192
  card.append(header)
192
193
 
@@ -394,6 +395,8 @@ export const renderVisualizer = (
394
395
  diagnostics: ReadonlyArray<Diagnostic> = []
395
396
  ): void => {
396
397
  const model = makeVisualizerModel(visualization)
398
+ const analysis = analyzeVisualization(visualization)
399
+ const analysisCount = analysis.warnings.length + analysis.topologyNotes.length
397
400
  const transitionsById = new Map(visualization.transitions.map((transition) => [transition.id, transition]))
398
401
  const eventSchemas = new Map(visualization.inputs.events.map(({ event, schema }) => [event, schema]))
399
402
  const relatedFrom = new Set<string>()
@@ -405,6 +408,7 @@ export const renderVisualizer = (
405
408
  let selectedFrame: MachineWalkthrough.Frame | undefined
406
409
  let walkthrough: MachineWalkthrough.Session | undefined
407
410
  let chartView: ChartView | undefined
411
+ let inspectorView: "selection" | "analysis" | undefined
408
412
 
409
413
  const activePaths = (): ReadonlyArray<string> =>
410
414
  walkthrough === undefined ? model.activePaths : MachineWalkthrough.current(walkthrough).after.activePaths
@@ -417,7 +421,7 @@ export const renderVisualizer = (
417
421
  chartPanel.setAttribute("aria-label", `${model.machineId} topology`)
418
422
  const inspector = createElement("aside", "inspector")
419
423
  inspector.setAttribute("aria-live", "polite")
420
- inspector.setAttribute("aria-label", "Selection details")
424
+ inspector.setAttribute("aria-label", "Machine inspector")
421
425
  inspector.hidden = true
422
426
  const inspectorClose = createElement("button", "inspector-close", "Close")
423
427
  inspectorClose.type = "button"
@@ -448,16 +452,28 @@ export const renderVisualizer = (
448
452
  const walkthroughButton = createElement("button", "toolbar-button", "Start simulation")
449
453
  walkthroughButton.type = "button"
450
454
  walkthroughButton.disabled = model.roots.length === 0
455
+ const analysisButton = createElement("button", "toolbar-button analysis-button")
456
+ analysisButton.type = "button"
457
+ analysisButton.hidden = analysisCount === 0
458
+ analysisButton.setAttribute("aria-pressed", "false")
459
+ analysisButton.append(
460
+ createElement("span", undefined, "Analysis"),
461
+ createElement("span", "analysis-button-count", String(analysisCount))
462
+ )
451
463
 
452
464
  const hideInspector = (): void => {
453
465
  inspectorContent.replaceChildren()
454
466
  inspector.hidden = true
467
+ inspectorView = undefined
455
468
  detailsButton.textContent = "View details"
469
+ analysisButton.setAttribute("aria-pressed", "false")
456
470
  }
457
471
 
458
- const showInspector = (): void => {
472
+ const showInspector = (view: "selection" | "analysis"): void => {
459
473
  inspector.hidden = false
460
- detailsButton.textContent = "Hide details"
474
+ inspectorView = view
475
+ detailsButton.textContent = view === "selection" ? "Hide details" : "View details"
476
+ analysisButton.setAttribute("aria-pressed", view === "analysis" ? "true" : "false")
461
477
  }
462
478
 
463
479
  const closeChoicePicker = (): void => {
@@ -467,7 +483,7 @@ export const renderVisualizer = (
467
483
 
468
484
  const renderInspection = (inspection: StateInspection): void => {
469
485
  inspectorContent.replaceChildren()
470
- showInspector()
486
+ showInspector("selection")
471
487
  const header = createElement("header", "inspector-header")
472
488
  const breadcrumbs = createElement("nav", "breadcrumbs")
473
489
  breadcrumbs.setAttribute("aria-label", "State path")
@@ -528,7 +544,7 @@ export const renderVisualizer = (
528
544
 
529
545
  const renderTransitionInspection = (transition: VisualizationTransition): void => {
530
546
  inspectorContent.replaceChildren()
531
- showInspector()
547
+ showInspector("selection")
532
548
  const header = createElement("header", "inspector-header")
533
549
  const titleRow = createElement("div", "inspector-title-row")
534
550
  const flags = createElement("div", "card-flags")
@@ -560,6 +576,82 @@ export const renderVisualizer = (
560
576
  }
561
577
  }
562
578
 
579
+ const renderAnalysis = (findings: VisualizerAnalysis): void => {
580
+ inspectorContent.replaceChildren()
581
+ showInspector("analysis")
582
+
583
+ const header = createElement("header", "inspector-header analysis-header")
584
+ const titleRow = createElement("div", "inspector-title-row")
585
+ titleRow.append(
586
+ createElement("h2", undefined, "Analysis"),
587
+ badge(String(findings.warnings.length + findings.topologyNotes.length), "count")
588
+ )
589
+ header.append(
590
+ titleRow,
591
+ createElement(
592
+ "p",
593
+ "analysis-summary",
594
+ "Static checks from the captured machine definition. Machine callbacks are not executed."
595
+ )
596
+ )
597
+ inspectorContent.append(header)
598
+
599
+ const warnings = createElement("section", "inspector-section analysis-section")
600
+ warnings.append(inspectionSection("Unhandled events", findings.warnings.length))
601
+ if (findings.warnings.length === 0) {
602
+ warnings.append(createElement("p", "section-empty", "Every declared public event has a registered handler."))
603
+ } else {
604
+ for (const warning of findings.warnings) {
605
+ const card = createElement("article", "inspection-card analysis-card")
606
+ const cardHeader = createElement("div", "card-header")
607
+ const title = createElement("div", "card-title")
608
+ title.append(createElement("strong", undefined, warning.event))
609
+ cardHeader.append(title, badge("no handler", "warning"))
610
+ card.append(
611
+ cardHeader,
612
+ createElement(
613
+ "p",
614
+ "analysis-message",
615
+ "This public event is declared, but no state registers a handler for it."
616
+ )
617
+ )
618
+ warnings.append(card)
619
+ }
620
+ }
621
+ inspectorContent.append(warnings)
622
+
623
+ const topology = createElement("section", "inspector-section analysis-section")
624
+ topology.append(inspectionSection("Topology notes", findings.topologyNotes.length))
625
+ if (findings.topologyNotes.length === 0) {
626
+ topology.append(createElement("p", "section-empty", "Every state appears on a static path from initial."))
627
+ } else {
628
+ for (const note of findings.topologyNotes) {
629
+ const card = createElement("article", "inspection-card analysis-card")
630
+ const cardHeader = createElement("div", "card-header")
631
+ const title = createElement("div", "card-title")
632
+ title.append(stateLink(note.path, note.label, navigateToState))
633
+ cardHeader.append(title, badge(note.type, "state"))
634
+ card.append(
635
+ cardHeader,
636
+ createElement(
637
+ "p",
638
+ "analysis-message",
639
+ note.descendantCount === 0
640
+ ? "No statically known path reaches this state from the machine's initial configuration."
641
+ : `No statically known path reaches this state or its ${note.descendantCount} descendants from the machine's initial configuration.`
642
+ ),
643
+ createElement(
644
+ "p",
645
+ "analysis-qualification",
646
+ "Runtime-resolved targets or a resumed snapshot may still reach it."
647
+ )
648
+ )
649
+ topology.append(card)
650
+ }
651
+ }
652
+ inspectorContent.append(topology)
653
+ }
654
+
563
655
  const renderPathGroup = (label: string, paths: ReadonlyArray<string>): HTMLElement => {
564
656
  const group = createElement("div", "trace-path-group")
565
657
  group.append(createElement("span", "trace-label", label))
@@ -572,7 +664,7 @@ export const renderVisualizer = (
572
664
 
573
665
  const renderWalkthroughTrace = (frame: MachineWalkthrough.Frame): void => {
574
666
  inspectorContent.replaceChildren()
575
- showInspector()
667
+ showInspector("selection")
576
668
  const header = createElement("header", "inspector-header trace-header")
577
669
  const eyebrow = createElement("div", "inspector-eyebrow")
578
670
  eyebrow.append(badge("walkthrough", "active"))
@@ -916,7 +1008,7 @@ export const renderVisualizer = (
916
1008
  inspectorClose.addEventListener("click", hideInspector)
917
1009
  choicePickerClose.addEventListener("click", closeChoicePicker)
918
1010
  detailsButton.addEventListener("click", () => {
919
- if (!inspector.hidden) {
1011
+ if (!inspector.hidden && inspectorView === "selection") {
920
1012
  hideInspector()
921
1013
  } else if (selectedFrame !== undefined) {
922
1014
  renderWalkthroughTrace(selectedFrame)
@@ -928,6 +1020,13 @@ export const renderVisualizer = (
928
1020
  if (transition !== undefined) renderTransitionInspection(transition)
929
1021
  }
930
1022
  })
1023
+ analysisButton.addEventListener("click", () => {
1024
+ if (!inspector.hidden && inspectorView === "analysis") {
1025
+ hideInspector()
1026
+ } else {
1027
+ renderAnalysis(analysis)
1028
+ }
1029
+ })
931
1030
  revealActiveButton.addEventListener("click", () => chartView?.revealStates(activePaths()))
932
1031
 
933
1032
  const zoomOutButton = createElement("button", "toolbar-button zoom-button", "−")
@@ -971,7 +1070,7 @@ export const renderVisualizer = (
971
1070
  const runtimeText = createElement("span")
972
1071
  runtime.append(runtimeDot, runtimeText)
973
1072
  const toolbarActions = createElement("div", "toolbar-actions")
974
- toolbarActions.append(clearButton, detailsButton, walkthroughButton, revealActiveButton)
1073
+ toolbarActions.append(clearButton, detailsButton, walkthroughButton, revealActiveButton, analysisButton)
975
1074
  const zoomControls = createElement("div", "zoom-controls")
976
1075
  zoomControls.setAttribute("role", "group")
977
1076
  zoomControls.setAttribute("aria-label", "Chart zoom")
@@ -1002,10 +1101,11 @@ export const renderVisualizer = (
1002
1101
  revealActiveButton.disabled = active.size === 0
1003
1102
  clearButton.hidden = simulating
1004
1103
  detailsButton.hidden = simulating
1104
+ analysisButton.hidden = simulating || analysisCount === 0
1005
1105
  chartPanel.classList.toggle("is-simulating", simulating)
1006
1106
  if (simulating) hideInspector()
1007
1107
  renderWalkthroughDock()
1008
- if (!simulating && !inspector.hidden) {
1108
+ if (!simulating && !inspector.hidden && inspectorView === "selection") {
1009
1109
  if (selectedFrame !== undefined) {
1010
1110
  renderWalkthroughTrace(selectedFrame)
1011
1111
  } else if (selectedPath !== undefined) {