It is difficult to ensure that complex systems remain consistent as they move through multiple steps or different states. Testing every possible path manually is often impossible.
It checks every possible sequence of actions to ensure the system never breaks its core rules. It looks at the system as a series of steps and verifies that it stays consistent throughout.
It allows you to catch hidden errors by verifying that a system's core rules are never broken, no matter what path a user takes.
It was run inside an isolated container with no network access. This is the exact command and the real output it produced — captured process output, not written by a model.
$ python3 pdstv.py Use 'python pdstv.py --demo' to run the built-in example.
A screenshot of that run.
A clean run proves this does what is shown above, in a CPU-only sandbox. It is a small research demo — not a production tool, and nothing here was published anywhere.
All of it — 397 lines, one file, standard library only.
#!/usr/bin/env python3
"""
Property-Driven State Transition Validator (PDSTV)
===================================================
Combines stateful multi-step graph logic (inspired by Trip Assistant's
state graphs) with invariant-based property testing (inspired by
junit-quickcheck's property-based testing).
Models a system as a directed state-transition graph. Each state node
holds typed data. Each transition edge has a guard predicate (precondition),
an action that mutates state, and a weight/cost. Invariants are predicates
that must hold in every reachable state. Properties are generative assertions
checked across random walks through the graph.
Run: python pdstv.py --demo
"""
import random
import sys
import time
from collections import deque
from dataclasses import dataclass, field
from typing import (
Any,
Callable,
Dict,
List,
Literal,
Optional,
Sequence,
Set,
Tuple,
TypeVar,
Union,
)
# ── Core Types ───────────────────────────────────────────────────────────────
StateId = str
TransitionId = str
Invariant = Callable[["MachineState"], bool]
ActionFn = Callable[["MachineState", Dict[str, Any]], None]
PreconditionFn = Callable[["MachineState", Dict[str, Any]], bool]
GeneratorFn = Callable[["MachineState"], Dict[str, Any]]
PropertyFn = Callable[["MachineState", "MachineState", Dict[str, Any]], bool]
# ── State Machine ────────────────────────────────────────────────────────────
@dataclass
class MachineState:
"""Holds typed key-value data for a single state snapshot."""
data: Dict[str, Any] = field(default_factory=dict)
def get(self, key: str, default: Any = None) -> Any:
return self.data.get(key, default)
def set(self, key: str, value: Any) -> None:
self.data[key] = value
def snapshot(self) -> Dict[str, Any]:
return dict(self.data)
@dataclass
class Transition:
"""A directed edge in the state graph."""
id: TransitionId
source: StateId
target: StateId
precondition: PreconditionFn = lambda s, ctx: True
action: ActionFn = lambda s, ctx: None
cost: int = 1
label: str = ""
@dataclass
class StateNode:
"""A node in the state graph."""
id: StateId
invariants: List[Invariant] = field(default_factory=list)
label: str = ""
@dataclass
class PropertyCheck:
"""A user-defined generative property.
When checked, the framework generates context via `generator`, then
either follows the specified transition sequence or performs random
walks. The `predicate` is called with (pre_snapshot, post_state, context).
"""
name: str
transitions: Sequence[Tuple[StateId, TransitionId]] = ()
generator: GeneratorFn = lambda s: {}
predicate: PropertyFn = lambda pre, post, ctx: True
rounds: int = 100
# ── Graph Definition ─────────────────────────────────────────────────────────
@dataclass
class TransitionGraph:
nodes: Dict[StateId, StateNode] = field(default_factory=dict)
edges: List[Transition] = field(default_factory=list)
_outgoing: Dict[StateId, List[Transition]] = field(default_factory=dict, init=False, repr=False)
_incoming: Dict[StateId, List[Transition]] = field(default_factory=dict, init=False, repr=False)
_by_id: Dict[TransitionId, Transition] = field(default_factory=dict, init=False, repr=False)
def add_node(self, node: StateNode) -> None:
self.nodes[node.id] = node
def add_transition(self, t: Transition) -> None:
self.edges.append(t)
def build(self) -> None:
self._outgoing.clear()
self._incoming.clear()
self._by_id.clear()
for e in self.edges:
self._outgoing.setdefault(e.source, []).append(e)
self._incoming.setdefault(e.target, []).append(e)
self._by_id[e.id] = e
def reachable_states(self, start: StateId) -> Set[StateId]:
seen: Set[StateId] = set()
q: deque[StateId] = deque([start])
while q:
node = q.popleft()
if node in seen:
continue
seen.add(node)
for t in self._outgoing.get(node, []):
if t.target not in seen:
q.append(t.target)
return seen
# ── Random Walk Engine ───────────────────────────────────────────────────────
@dataclass
class WalkConfig:
max_steps: int = 50
seed: int = 42
@dataclass
class WalkResult:
path: List[StateId]
transitions: List[Transition]
final_state: MachineState
steps: int
class StateWalker:
"""Drives random walks through the graph, checking preconditions and
applying actions at each step."""
def __init__(
self,
state: MachineState,
graph: TransitionGraph,
config: Optional[WalkConfig] = None,
):
self.state = state
self.graph = graph
self.config = config or WalkConfig()
self.rng = random.Random(self.config.seed)
def step(self, current: StateId) -> Optional[Transition]:
candidates = self.graph._outgoing.get(current, [])
available = [t for t in candidates if t.precondition(self.state, {})]
if not available:
return None
chosen = self.rng.choice(available)
chosen.action(self.state, {})
return chosen
def walk(self, start: StateId) -> WalkResult:
path: List[StateId] = [start]
transitions: List[Transition] = []
current = start
for _ in range(self.config.max_steps):
t = self.step(current)
if t is None:
return WalkResult(
path=path,
transitions=transitions,
final_state=self.state,
steps=len(transitions),
)
current = t.target
path.append(current)
transitions.append(t)
return WalkResult(
path=path,
transitions=transitions,
final_state=self.state,
steps=len(transitions),
)
# ── Violations & Report ─────────────────────────────────────────────────────
@dataclass
class Violation:
kind: Literal["invariant", "property", "precondition"]
detail: str
node: Optional[StateId] = None
transition: Optional[TransitionId] = None
property_id: Optional[str] = None
round_id: Optional[int] = None
snapshot: Optional[Dict[str, Any]] = None
@dataclass
class ValidationReport:
total_checks: int = 0
passed: int = 0
violations: List[Violation] = field(default_factory=list)
edge_coverage: float = 0.0
elapsed_ms: float = 0.0
@property
def is_clean(self) -> bool:
return len(self.violations) == 0 and self.total_checks > 0
# ── Validator ────────────────────────────────────────────────────────────────
class Validator:
"""Orchestrates invariant + property checks by generating random walks."""
def __init__(
self, graph: TransitionGraph, state_factory: Callable[[], MachineState]
):
self.graph = graph
self.state_factory = state_factory
self.properties: List[PropertyCheck] = []
self.walk_config = WalkConfig(max_steps=30, seed=42)
def add_property(self, pc: PropertyCheck) -> None:
self.properties.append(pc)
def validate(self) -> ValidationReport:
report = ValidationReport()
t0 = time.perf_counter()
start_nodes = list(self.graph.nodes.keys())
if not start_nodes:
return report
rng = random.Random(self.walk_config.seed)
# --- Invariant checks via random walks ---
for _ in range(min(25, len(start_nodes) * 5)):
start = rng.choice(start_nodes)
state = self.state_factory()
walker = StateWalker(state, self.graph, self.walk_config)
result = walker.walk(start)
for node_id in result.path:
node = self.graph.nodes.get(node_id)
if node is None:
continue
for inv in node.invariants:
report.total_checks += 1
try:
if inv(state):
report.passed += 1
else:
report.violations.append(
Violation(
kind="invariant",
node=node_id,
detail=f"Invariant violated at '{node_id}' after walk from '{start}'",
snapshot=state.snapshot(),
)
)
except Exception as exc:
report.violations.append(
Violation(
kind="invariant",
node=node_id,
detail=f"Invariant raised exception: {exc}",
snapshot=state.snapshot(),
)
)
# --- Property checks ---
for prop in self.properties:
for ri in range(prop.rounds):
state = self.state_factory()
ctx = prop.generator(state)
if prop.transitions:
snapshot_before = state.snapshot()
ran_any = False
for _src, tid in prop.transitions:
trans = self.graph._by_id.get(tid)
if trans is None:
report.violations.append(
Violation(
kind="precondition",
detail=f"Transition '{tid}' not found in graph",
property_id=prop.name,
)
)
continue
if not trans.precondition(state, ctx):
continue
trans.action(state, ctx)
ran_any = True
if ran_any:
report.total_checks += 1
try:
if prop.predicate(
MachineState(data=snapshot_before), state, ctx
):
report.passed += 1
else:
report.violations.append(
Violation(
kind="property",
detail=f"Property '{prop.name}' failed round {ri + 1}",
property_id=prop.name,
round_id=ri,
snapshot=state.snapshot(),
)
)
except Exception as exc:
report.violations.append(
Violation(
kind="property",
detail=f"Property '{prop.name}' raised: {exc}",
property_id=prop.name,
round_id=ri,
)
)
else:
start = rng.choice(start_nodes)
walker = StateWalker(state, self.graph, self.walk_config)
_result = walker.walk(start)
report.total_checks += 1
try:
if prop.predicate(MachineState(), state, ctx):
report.passed += 1
else:
report.violations.append(
Violation(
kind="property",
detail=f"Property '{prop.name}' failed round {ri + 1}",
property_id=prop.name,
round_id=ri,
snapshot=state.snapshot(),
)
)
except Exception as exc:
report.violations.append(
Violation(
kind="property",
detail=f"Property '{prop.name}' raised: {exc}",
property_id=prop.name,
round_id=ri,
)
)
# --- Edge coverage estimation ---
traversed: Set[TransitionId] = set()
for _ in range(max(10, len(self.graph.edges))):
state = self.state_factory()
walker = StateWalker(state, self.graph, self.walk_config)
result = walker.walk(rng.choice(start_nodes))
for t in result.transitions:
traversed.add(t.id)
total_edges = len(self.graph.edges)
report.edge_coverage = (
len(traversed) / total_edges if total_edges > 0 else 0.0
)
report.elapsed_ms = (time.perf_counter() - t0) * 1000
return report
# ── Demo: E‑Commerce Order State Machine ────────────────────────────────────
def build_demo_graph() -> Tuple[TransitionGraph, Callable[[], MachineState]]:
"""Constructs a realistic e‑commerce order state machine with a deliberate bug."""
g = TransitionGraph()
# ── States ──
g.add_node(
StateNode(
"idle",
invariants=[
lambda s: s.get("balance", 0) >= 0, # balance never negative
lambda s: s.get("retries", 0) <= 3, # max