pyfcstm.diagnostics.inspect

Structured model inspection for pyfcstm.

This module provides inspect_model(), a single entry point that walks a pyfcstm.model.StateMachine and produces a stable, serialization-friendly view of its structure plus five derived relational graphs (reachability, event emission, variable data flow, aspect impact, action reference). The output is the foundation that Layer 2 design-health warnings (W_* / I_* codes) and downstream LLM / evaluation tooling consume.

The view shape is the single source of truth for the pyfcstm / jsfcstm contract. Adding or renaming a field here must be mirrored on the jsfcstm side (editors/jsfcstm/src/diagnostics/inspect.ts) and in pyfcstm/diagnostics/schema.json.

The module exposes the following dataclasses:

  • StateInfo — per-state structural summary

  • TransitionInfo — per-transition structural summary

  • VariableInfo — per-variable structural summary plus guard-affect flags used by W_UNREFERENCED_VAR

  • EventInfo — per-event structural summary

  • ModelMetrics — aggregate counts and ratios

  • ModelInspect — top-level container including diagnostics

Examples:

>>> from pyfcstm.dsl import parse_with_grammar_entry
>>> from pyfcstm.model import parse_dsl_node_to_state_machine
>>> from pyfcstm.diagnostics import inspect_model
>>> source = '''
... def int counter = 0;
... state Root {
...     state Idle;
...     state Active;
...     [*] -> Idle;
...     Idle -> Active : if [counter > 0];
... }
... '''
>>> ast = parse_with_grammar_entry(source, 'state_machine_dsl')
>>> machine = parse_dsl_node_to_state_machine(ast)
>>> report = inspect_model(machine)
>>> report.metrics.n_states_leaf
2

DEFAULT_DEEP_HIERARCHY_THRESHOLD

pyfcstm.diagnostics.inspect.DEFAULT_DEEP_HIERARCHY_THRESHOLD = 6

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal. >>> int(‘0b100’, base=0) 4

DEFAULT_LARGE_COMPOSITE_THRESHOLD

pyfcstm.diagnostics.inspect.DEFAULT_LARGE_COMPOSITE_THRESHOLD = 12

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal. >>> int(‘0b100’, base=0) 4

DEFAULT_VAR_TO_LEAF_RATIO_THRESHOLD

pyfcstm.diagnostics.inspect.DEFAULT_VAR_TO_LEAF_RATIO_THRESHOLD = 2.0

Convert a string or number to a floating point number, if possible.

KNOWN_SPANLESS_CODES

pyfcstm.diagnostics.inspect.KNOWN_SPANLESS_CODES = frozenset({})

frozenset() -> empty frozenset object frozenset(iterable) -> frozenset object

Build an immutable unordered collection of unique elements.

VERIFY_SHARED_STATIC_CODES

pyfcstm.diagnostics.inspect.VERIFY_SHARED_STATIC_CODES = frozenset({'W_UNREACHABLE_STATE'})

frozenset() -> empty frozenset object frozenset(iterable) -> frozenset object

Build an immutable unordered collection of unique elements.

StateInfo

class pyfcstm.diagnostics.inspect.StateInfo(path: str, name: str, parent_path: str | None, is_leaf: bool, is_pseudo: bool, is_composite: bool, substates: Tuple[str, ...], initial_targets: Tuple[Dict[str, Any], ...], entry_actions: Tuple[str, ...], during_actions: Tuple[str, ...], exit_actions: Tuple[str, ...], aspect_before: Tuple[str, ...], aspect_after: Tuple[str, ...], has_abstract_action: bool, span: Span | None = None)[source]

Structural summary of a single state.

Parameters:
  • path (str) – Dotted hierarchical path, e.g. 'Root.SubSystem.Active'.

  • name (str) – Short name of the state (last component of path).

  • parent_path (Optional[str]) – Dotted path of the parent state, or None for the root state.

  • is_leaf (bool) – True when this state has no substates.

  • is_pseudo (bool) – True when the state was declared with pseudo state.

  • is_composite (bool) – True when this state has substates.

  • substates (Tuple[str, ...]) – Direct-child state paths, in source order.

  • initial_targets (Tuple[Mapping[str, Any], ...]) – Each item describes one [*] -> X initial transition declared inside this composite. target is the target child path, guard is the source text of the guard or None, event is the qualified event name or None, is_unconditional is True only when both guard and event are absent.

  • entry_actions (Tuple[str, ...]) – Action labels (function name or '<inline>') for enter actions on this state, in source order.

  • during_actions (Tuple[str, ...]) – Action labels for during actions.

  • exit_actions (Tuple[str, ...]) – Action labels for exit actions.

  • aspect_before (Tuple[str, ...]) – Aspect-action labels for >> during before.

  • aspect_after (Tuple[str, ...]) – Aspect-action labels for >> during after.

  • has_abstract_action (bool) – True if any of the actions above is abstract. Used by VariableInfo confidence judgements.

TransitionInfo

class pyfcstm.diagnostics.inspect.TransitionInfo(from_path: str, to_path: str, event: str | None, event_scope: str | None, guard: str | None, effect: str | None, effect_self_assigns: ~typing.Tuple[str, ...], is_forced: bool, forced_origin: str | None, transition_index: int | None, span: ~pyfcstm.utils.validate.Span | None = None, effect_spans: ~typing.Tuple[~pyfcstm.utils.validate.Span, ...] = <factory>, effect_self_assign_spans: ~typing.Tuple[~pyfcstm.utils.validate.Span | None, ...] = <factory>)[source]

Structural summary of a single transition.

Parameters:
  • from_path (str) – Dotted path of the source state, or the literal '[*]' for an initial transition declared at the root.

  • to_path (str) – Dotted path of the target state, or '[*]' for an exit transition.

  • event (Optional[str]) – Qualified event name (e.g. 'Root.SubA.E') or None if the transition has no event.

  • event_scope (Optional[str]) – 'local', 'chain', 'absolute', or None when there is no event.

  • guard (Optional[str]) – Normalized guard expression text, or None. Pyfcstm and jsfcstm share this inspect expression format so downstream range resolution can treat guard_text as a stable disambiguation hint.

  • effect (Optional[str]) – Source text of the effect block, or None.

  • effect_self_assigns (Tuple[str, ...]) – Variable names assigned to themselves anywhere inside the transition effect block, including nested if branches. Duplicate names are preserved so quick-fix emitters can detect ambiguous occurrences.

  • effect_self_assign_spans (Tuple[Optional[pyfcstm.utils.validate.Span], ...]) – Source spans for the self-assign statements listed in effect_self_assigns. The order matches effect_self_assigns and uses None when a statement has no source span, preventing later spans from shifting to earlier names.

  • is_forced (bool) – True when the transition was expanded from a !-prefixed forced transition.

  • forced_origin (Optional[str]) – Raw source text of the original !X -> Y declaration when is_forced is True, otherwise None.

  • transition_index (Optional[int]) – Zero-based index in parent-first model transition order, including expanded forced transitions at their declaring state before ordinary transitions and descendant-state transitions. Downstream tooling may use this as a best-effort source-range disambiguation hint when spans are not available.

VariableInfo

class pyfcstm.diagnostics.inspect.VariableInfo(name: str, type: str, init_value: str, read_in_states: ~typing.Tuple[str, ...], written_in_states: ~typing.Tuple[str, ...], read_in_guards: ~typing.Tuple[~typing.Tuple[str, str], ...], written_in_effects: ~typing.Tuple[~typing.Tuple[str, str], ...], affects_guard_directly: bool, affects_guard_indirectly: bool, abstract_actions_in_scope: ~typing.Tuple[str, ...], float_literal_assignments: ~typing.Tuple[str, ...] = <factory>, span: ~pyfcstm.utils.validate.Span | None = None, float_literal_assignment_spans: ~typing.Tuple[~pyfcstm.utils.validate.Span | None, ...] = <factory>)[source]

Structural summary of a variable definition plus guard-affect flags.

The affects_guard_directly and affects_guard_indirectly flags are precomputed here so that unreferenced-variable diagnostics can be expressed as a one-line filter against this object.

Parameters:
  • name (str) – Variable identifier.

  • type (str) – Declared type, currently 'int' or 'float'.

  • init_value (str) – Source text of the initializer expression.

  • read_in_states (Tuple[str, ...]) – State paths where the variable is read inside any action (enter / during / exit / aspect).

  • written_in_states (Tuple[str, ...]) – State paths where the variable is written inside any action.

  • read_in_guards (Tuple[Tuple[str, str], ...]) – Tuples (from_path, to_path) of transitions whose guard reads this variable.

  • written_in_effects (Tuple[Tuple[str, str], ...]) – Tuples (from_path, to_path) of transitions whose effect block writes this variable.

  • affects_guard_directly (bool) – True when the variable is read by at least one transition guard.

  • affects_guard_indirectly (bool) – True when the variable reaches a transition guard through the conservative use-def graph.

  • abstract_actions_in_scope (Tuple[str, ...]) – Function names of abstract actions that may access this variable. FCSTM variables are global, so any abstract action in the machine is conservatively visible here. Downstream diagnostics can use this to distinguish high-confidence unused variables from variables that may be touched by abstract behavior.

  • float_literal_assignments (Tuple[str, ...]) – Source text of float literal assignments to this variable from lifecycle actions or transition effects.

EventInfo

class pyfcstm.diagnostics.inspect.EventInfo(qualified_name: str, scope: str, used_by: Tuple[Tuple[str, str], ...], is_declared: bool, is_used: bool, span: Span | None = None)[source]

Structural summary of an event declaration.

Parameters:
  • qualified_name (str) – Dotted fully qualified event name (e.g. 'Root.SubA.E').

  • scope (str) – 'local', 'chain', or 'absolute'.

  • used_by (Tuple[Tuple[str, str], ...]) – (from_path, to_path) tuples for every transition that references this event.

  • is_declared (bool) – True when the event came from an explicit event declaration.

  • is_used (bool) – True when at least one transition references the event.

ActionInfo

class pyfcstm.diagnostics.inspect.ActionInfo(signature: str, state_path: str, name: str | None, stage: str, aspect: str | None, is_ref: bool, ref_target: str | None, is_attached: bool, span: Span | None = None)[source]

Structural summary of a lifecycle action declaration.

ForcedTransitionInfo

class pyfcstm.diagnostics.inspect.ForcedTransitionInfo(state_path: str, from_path: str, to_path: str, event: str | None, event_scope: str | None, guard: str | None, original_raw: str, expansion_count: int, span: Span | None = None)[source]

Structural summary of a forced transition declaration.

ModelMetrics

class pyfcstm.diagnostics.inspect.ModelMetrics(n_states_leaf: int, n_states_composite: int, n_states_pseudo: int, max_hierarchy_depth: int, n_transitions_normal: int, n_transitions_forced: int, n_events: int, n_variables: int, var_to_leaf_ratio: float, aspect_coverage: Dict[str, int], abstract_action_inventory: Tuple[str, ...])[source]

Aggregate model metrics.

Parameters:
  • n_states_leaf (int) – Number of leaf states excluding pseudo states.

  • n_states_composite (int) – Number of composite states.

  • n_states_pseudo (int) – Number of pseudo states.

  • max_hierarchy_depth (int) – Maximum depth of state nesting, counted from the root (depth 0 = root).

  • n_transitions_normal (int) – Number of transitions that did not originate from a !-forced declaration.

  • n_transitions_forced (int) – Number of transitions expanded from !-forced declarations.

  • n_events (int) – Number of distinct qualified events exposed by the inspect surface, including explicitly declared events that no transition uses.

  • n_variables (int) – Number of variable definitions.

  • var_to_leaf_ratio (float) – n_variables / max(n_states_leaf, 1).

  • aspect_coverage (Dict[str, int]) – Mapping composite_path -> n_descendant_leaves for composite states that declare >> during aspects.

  • abstract_action_inventory (Tuple[str, ...]) – Function names of every abstract action across the model, sorted for stable output.

ModelInspect

class pyfcstm.diagnostics.inspect.ModelInspect(root_state_path: str, states: ~typing.Tuple[~pyfcstm.diagnostics.inspect.StateInfo, ...], transitions: ~typing.Tuple[~pyfcstm.diagnostics.inspect.TransitionInfo, ...], variables: ~typing.Tuple[~pyfcstm.diagnostics.inspect.VariableInfo, ...], events: ~typing.Tuple[~pyfcstm.diagnostics.inspect.EventInfo, ...], actions: ~typing.Tuple[~pyfcstm.diagnostics.inspect.ActionInfo, ...], forced_transitions: ~typing.Tuple[~pyfcstm.diagnostics.inspect.ForcedTransitionInfo, ...], metrics: ~pyfcstm.diagnostics.inspect.ModelMetrics, reachability_graph: ~typing.Dict[str, ~typing.Tuple[str, ...]], event_emission_map: ~typing.Dict[str, ~typing.Tuple[str, ...]], var_dataflow: ~typing.Dict[str, ~typing.Dict[str, ~typing.Tuple[str, ...]]], aspect_impact_map: ~typing.Dict[str, ~typing.Tuple[str, ...]], action_ref_graph: ~typing.Dict[str, ~typing.Tuple[str, ...]], diagnostics: ~typing.Tuple[~pyfcstm.utils.validate.ModelDiagnostic, ...] = <factory>)[source]

Top-level structured view of a state machine model.

Parameters:
  • root_state_path (str) – Dotted path of the root state.

  • states (Tuple[StateInfo, ...]) – All states walked from the root in pre-order.

  • transitions (Tuple[TransitionInfo, ...]) – All transitions, including expanded forced transitions, in source order.

  • variables (Tuple[VariableInfo, ...]) – All def variables, in declaration order.

  • events (Tuple[EventInfo, ...]) – All qualified events exposed by the inspect surface, including explicitly declared events that no transition uses, sorted by qualified name.

  • metrics (ModelMetrics) – Aggregate model metrics.

  • reachability_graph (Dict[str, Tuple[str, ...]]) – Mapping from every state path to state paths reachable through normal transitions and composite initial edges. Guards are ignored; [*] entry/exit markers are not exposed.

  • event_emission_map (Dict[str, Tuple[str, ...]]) – Mapping event qualified name → list of source state paths that can emit it.

  • var_dataflow (Dict[str, Dict[str, Tuple[str, ...]]]) – Mapping variable name → {'reads': [...], 'writes': [...]} of state paths.

  • aspect_impact_map (Dict[str, Tuple[str, ...]]) – Mapping composite path → descendant leaf paths actually reached by its aspect actions.

  • action_ref_graph (Dict[str, Tuple[str, ...]]) – Mapping named-action function path → list of ref edges out of it.

  • diagnostics (Tuple[ModelDiagnostic, ...]) – Layer 1 E_* plus design-health W_* / I_* diagnostics derived from the inspect payload.

to_json() Dict[str, Any][source]

Serialize this inspection report to a plain JSON-friendly dict.

Tuples are converted to lists; frozen dataclasses to dicts. ModelDiagnostic instances are serialized via their public attributes (code, severity, message, span, refs).

Returns:

A dict that round-trips through json.dumps() without loss.

Return type:

Dict[str, Any]

Examples:

>>> from pyfcstm.dsl import parse_with_grammar_entry
>>> from pyfcstm.model import parse_dsl_node_to_state_machine
>>> ast = parse_with_grammar_entry('state Root;', 'state_machine_dsl')
>>> machine = parse_dsl_node_to_state_machine(ast)
>>> report = inspect_model(machine)
>>> report.to_json()['root_state_path']
'Root'

inspect_model

pyfcstm.diagnostics.inspect.inspect_model(machine: StateMachine, *, deep_hierarchy_threshold: int = 6, large_composite_threshold: int = 12, var_to_leaf_ratio_threshold: float = 2.0, enable_verify: bool = False, max_complexity_tier: str = 'structural', max_call_count_scaling: str = 'linear_in_transitions', smt_timeout_ms: int | None = None) ModelInspect[source]

Build a structured inspection report for a state machine model.

The report combines the structural payload, the five derived view graphs, and design-health diagnostics that can be computed from the inspect surface.

Parameters:
  • machine (pyfcstm.model.StateMachine) – The state machine model to inspect.

  • deep_hierarchy_threshold (int) – Maximum accepted hierarchy depth.

  • large_composite_threshold (int) – Maximum accepted number of direct child states in one composite.

  • var_to_leaf_ratio_threshold (float) – Maximum accepted variable to non-pseudo leaf-state ratio.

  • enable_verify (bool, optional) – Whether to run inspect-eligible pyfcstm.verify algorithms and append their diagnostics. The default False preserves the Layer 2 inspect contract.

  • max_complexity_tier (str, optional) – Maximum verify complexity tier accepted by the inspect adapter when enable_verify is true. "structural" keeps the default to graph-only verification.

  • max_call_count_scaling (str, optional) – Maximum verify call-count scaling accepted by the inspect adapter when enable_verify is true.

  • smt_timeout_ms (Optional[int], optional) – Optional solver timeout forwarded to SMT-local verify algorithms. None preserves the raw verify default of no configured timeout.

Returns:

Structured view of the model.

Return type:

ModelInspect

Examples:

>>> from pyfcstm.dsl import parse_with_grammar_entry
>>> from pyfcstm.model import parse_dsl_node_to_state_machine
>>> source = '''
... state Root {
...     state A;
...     state B;
...     [*] -> A;
...     A -> B;
... }
... '''
>>> ast = parse_with_grammar_entry(source, 'state_machine_dsl')
>>> machine = parse_dsl_node_to_state_machine(ast)
>>> report = inspect_model(machine)
>>> report.reachability_graph['Root.A']
('Root.B',)
>>> verify_report = inspect_model(machine, enable_verify=True)
>>> len(verify_report.diagnostics) >= len(report.diagnostics)
True