orfeas_petri_flow 0.6.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +80 -0
- data/MIT-LICENSE +22 -0
- data/README.md +592 -0
- data/Rakefile +28 -0
- data/lib/petri_flow/colored/arc_expression.rb +163 -0
- data/lib/petri_flow/colored/color.rb +40 -0
- data/lib/petri_flow/colored/colored_net.rb +146 -0
- data/lib/petri_flow/colored/guard.rb +104 -0
- data/lib/petri_flow/core/arc.rb +63 -0
- data/lib/petri_flow/core/marking.rb +64 -0
- data/lib/petri_flow/core/net.rb +121 -0
- data/lib/petri_flow/core/place.rb +54 -0
- data/lib/petri_flow/core/token.rb +55 -0
- data/lib/petri_flow/core/transition.rb +88 -0
- data/lib/petri_flow/export/cpn_tools_exporter.rb +322 -0
- data/lib/petri_flow/export/json_exporter.rb +224 -0
- data/lib/petri_flow/export/pnml_exporter.rb +229 -0
- data/lib/petri_flow/export/yaml_exporter.rb +246 -0
- data/lib/petri_flow/export.rb +193 -0
- data/lib/petri_flow/generators/adapters/aasm_adapter.rb +69 -0
- data/lib/petri_flow/generators/adapters/state_machines_adapter.rb +83 -0
- data/lib/petri_flow/generators/state_machine_adapter.rb +47 -0
- data/lib/petri_flow/generators/workflow_generator.rb +176 -0
- data/lib/petri_flow/matrix/analyzer.rb +151 -0
- data/lib/petri_flow/matrix/causation.rb +126 -0
- data/lib/petri_flow/matrix/correlation.rb +79 -0
- data/lib/petri_flow/matrix/crud_event_mapping.rb +74 -0
- data/lib/petri_flow/matrix/lineage.rb +113 -0
- data/lib/petri_flow/matrix/reachability.rb +128 -0
- data/lib/petri_flow/railtie.rb +41 -0
- data/lib/petri_flow/registry.rb +85 -0
- data/lib/petri_flow/simulation/simulator.rb +188 -0
- data/lib/petri_flow/simulation/trace.rb +119 -0
- data/lib/petri_flow/tasks/petri_flow.rake +229 -0
- data/lib/petri_flow/verification/boundedness_checker.rb +127 -0
- data/lib/petri_flow/verification/invariant_checker.rb +144 -0
- data/lib/petri_flow/verification/liveness_checker.rb +153 -0
- data/lib/petri_flow/verification/reachability_analyzer.rb +152 -0
- data/lib/petri_flow/verification_runner.rb +287 -0
- data/lib/petri_flow/version.rb +5 -0
- data/lib/petri_flow/visualization/graphviz.rb +220 -0
- data/lib/petri_flow/visualization/mermaid.rb +191 -0
- data/lib/petri_flow/workflow.rb +228 -0
- data/lib/petri_flow.rb +164 -0
- metadata +174 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetriFlow
|
|
4
|
+
# Registry for storing and retrieving workflow classes.
|
|
5
|
+
# Workflows are auto-registered when they inherit from PetriFlow::Workflow.
|
|
6
|
+
#
|
|
7
|
+
# @example Register a workflow manually
|
|
8
|
+
# PetriFlow::Registry.register(RefundRequestWorkflow)
|
|
9
|
+
#
|
|
10
|
+
# @example Get all workflows
|
|
11
|
+
# PetriFlow::Registry.all
|
|
12
|
+
#
|
|
13
|
+
# @example Discover workflows in a directory
|
|
14
|
+
# PetriFlow::Registry.discover_in("app/workflows")
|
|
15
|
+
#
|
|
16
|
+
class Registry
|
|
17
|
+
class << self
|
|
18
|
+
# Register a workflow class
|
|
19
|
+
# @param workflow_class [Class] A class that inherits from PetriFlow::Workflow
|
|
20
|
+
def register(workflow_class)
|
|
21
|
+
workflows[workflow_class.name] = workflow_class
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Get a workflow class by name
|
|
25
|
+
# @param name [String, Symbol] The workflow class name
|
|
26
|
+
# @return [Class, nil] The workflow class or nil if not found
|
|
27
|
+
def get(name)
|
|
28
|
+
workflows[name.to_s]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get all registered workflow classes
|
|
32
|
+
# @return [Array<Class>] All registered workflow classes
|
|
33
|
+
def all
|
|
34
|
+
workflows.values
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get all workflow names
|
|
38
|
+
# @return [Array<String>] All registered workflow class names
|
|
39
|
+
def names
|
|
40
|
+
workflows.keys
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if a workflow exists
|
|
44
|
+
# @param name [String, Symbol] The workflow class name
|
|
45
|
+
# @return [Boolean] True if workflow is registered
|
|
46
|
+
def exists?(name)
|
|
47
|
+
workflows.key?(name.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Unregister a workflow
|
|
51
|
+
# @param name [String, Symbol] The workflow class name to remove
|
|
52
|
+
def unregister(name)
|
|
53
|
+
workflows.delete(name.to_s)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Clear all workflows (useful for testing)
|
|
57
|
+
def clear
|
|
58
|
+
@workflows = {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Count of registered workflows
|
|
62
|
+
# @return [Integer] Number of registered workflows
|
|
63
|
+
def count
|
|
64
|
+
workflows.count
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Discover and load workflows from a directory
|
|
68
|
+
# Loads all files matching *_workflow.rb pattern
|
|
69
|
+
# @param directory [String] Path to the workflows directory
|
|
70
|
+
def discover_in(directory)
|
|
71
|
+
return unless directory && Dir.exist?(directory)
|
|
72
|
+
|
|
73
|
+
Dir.glob(File.join(directory, "**", "*_workflow.rb")).sort.each do |file|
|
|
74
|
+
require file
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def workflows
|
|
81
|
+
@workflows ||= {}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetriFlow
|
|
4
|
+
module Simulation
|
|
5
|
+
# Simulator for Petri nets
|
|
6
|
+
# Simulates execution and generates traces
|
|
7
|
+
class Simulator
|
|
8
|
+
attr_reader :net, :trace, :current_step
|
|
9
|
+
|
|
10
|
+
def initialize(net)
|
|
11
|
+
@net = net
|
|
12
|
+
@trace = Trace.new
|
|
13
|
+
@current_step = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Run simulation for n steps or until no transitions enabled
|
|
17
|
+
def run(steps: 100, strategy: :random, context: {})
|
|
18
|
+
@current_step = 0
|
|
19
|
+
@trace.clear
|
|
20
|
+
@trace.record_initial_marking(@net.current_marking)
|
|
21
|
+
|
|
22
|
+
steps.times do |step|
|
|
23
|
+
@current_step = step
|
|
24
|
+
|
|
25
|
+
enabled = @net.enabled_transitions(context)
|
|
26
|
+
break if enabled.empty?
|
|
27
|
+
|
|
28
|
+
transition = select_transition(enabled, strategy)
|
|
29
|
+
fire_transition(transition, context)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@trace
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Run until a specific condition is met
|
|
36
|
+
def run_until(strategy: :random, max_steps: 1000, context: {}, &condition)
|
|
37
|
+
@current_step = 0
|
|
38
|
+
@trace.clear
|
|
39
|
+
@trace.record_initial_marking(@net.current_marking)
|
|
40
|
+
|
|
41
|
+
max_steps.times do |step|
|
|
42
|
+
@current_step = step
|
|
43
|
+
|
|
44
|
+
# Check condition
|
|
45
|
+
break if condition.call(@net.current_marking, @net)
|
|
46
|
+
|
|
47
|
+
enabled = @net.enabled_transitions(context)
|
|
48
|
+
break if enabled.empty?
|
|
49
|
+
|
|
50
|
+
transition = select_transition(enabled, strategy)
|
|
51
|
+
fire_transition(transition, context)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@trace
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Run simulation multiple times (Monte Carlo)
|
|
58
|
+
def run_multiple(runs: 100, steps: 50, strategy: :random, context: {})
|
|
59
|
+
traces = []
|
|
60
|
+
initial_marking = @net.current_marking.dup
|
|
61
|
+
|
|
62
|
+
runs.times do
|
|
63
|
+
# Reset to initial state
|
|
64
|
+
@net.set_marking(initial_marking.dup)
|
|
65
|
+
|
|
66
|
+
# Run simulation
|
|
67
|
+
trace = run(steps: steps, strategy: strategy, context: context)
|
|
68
|
+
traces << trace
|
|
69
|
+
|
|
70
|
+
# Reset for next run
|
|
71
|
+
@net.set_marking(initial_marking.dup)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
analyze_traces(traces)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Interactive simulation (step by step)
|
|
78
|
+
def step(transition_id = nil, context: {})
|
|
79
|
+
if transition_id
|
|
80
|
+
transition = @net.transition(transition_id)
|
|
81
|
+
raise "Transition #{transition_id} not found" unless transition
|
|
82
|
+
raise "Transition #{transition_id} not enabled" unless transition.enabled?(context)
|
|
83
|
+
|
|
84
|
+
fire_transition(transition, context)
|
|
85
|
+
else
|
|
86
|
+
enabled = @net.enabled_transitions(context)
|
|
87
|
+
return nil if enabled.empty?
|
|
88
|
+
|
|
89
|
+
transition = enabled.first
|
|
90
|
+
fire_transition(transition, context)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@current_step += 1
|
|
94
|
+
transition
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Reset simulation
|
|
98
|
+
def reset(initial_marking = nil)
|
|
99
|
+
@current_step = 0
|
|
100
|
+
@trace.clear
|
|
101
|
+
|
|
102
|
+
if initial_marking
|
|
103
|
+
@net.set_marking(initial_marking)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@trace.record_initial_marking(@net.current_marking)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def select_transition(enabled_transitions, strategy)
|
|
112
|
+
case strategy
|
|
113
|
+
when :random
|
|
114
|
+
enabled_transitions.sample
|
|
115
|
+
when :first
|
|
116
|
+
enabled_transitions.first
|
|
117
|
+
when :priority
|
|
118
|
+
# Transitions with higher priority (more input arcs) first
|
|
119
|
+
enabled_transitions.max_by { |t| t.input_arcs.size }
|
|
120
|
+
when :least_used
|
|
121
|
+
# Select least fired transition
|
|
122
|
+
firing_counts = @trace.transition_firing_counts
|
|
123
|
+
enabled_transitions.min_by { |t| firing_counts[t.id] || 0 }
|
|
124
|
+
else
|
|
125
|
+
enabled_transitions.first
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def fire_transition(transition, context)
|
|
130
|
+
# Record state before firing
|
|
131
|
+
marking_before = @net.current_marking.dup
|
|
132
|
+
|
|
133
|
+
# Fire transition
|
|
134
|
+
transition.fire!(context)
|
|
135
|
+
|
|
136
|
+
# Record state after firing
|
|
137
|
+
marking_after = @net.current_marking.dup
|
|
138
|
+
|
|
139
|
+
# Add to trace
|
|
140
|
+
@trace.record_transition(
|
|
141
|
+
step: @current_step,
|
|
142
|
+
transition_id: transition.id,
|
|
143
|
+
transition_name: transition.name,
|
|
144
|
+
marking_before: marking_before,
|
|
145
|
+
marking_after: marking_after
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def analyze_traces(traces)
|
|
150
|
+
{
|
|
151
|
+
total_runs: traces.size,
|
|
152
|
+
average_steps: traces.map(&:steps).sum.to_f / traces.size,
|
|
153
|
+
min_steps: traces.map(&:steps).min,
|
|
154
|
+
max_steps: traces.map(&:steps).max,
|
|
155
|
+
deadlocked_runs: traces.count { |t| t.deadlocked? },
|
|
156
|
+
transition_frequencies: aggregate_transition_frequencies(traces),
|
|
157
|
+
state_coverage: aggregate_state_coverage(traces),
|
|
158
|
+
traces: traces
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def aggregate_transition_frequencies(traces)
|
|
163
|
+
frequencies = Hash.new(0)
|
|
164
|
+
|
|
165
|
+
traces.each do |trace|
|
|
166
|
+
trace.transition_firing_counts.each do |transition_id, count|
|
|
167
|
+
frequencies[transition_id] += count
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
total_firings = frequencies.values.sum
|
|
172
|
+
frequencies.transform_values { |count| count.to_f / total_firings }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def aggregate_state_coverage(traces)
|
|
176
|
+
all_states = Set.new
|
|
177
|
+
|
|
178
|
+
traces.each do |trace|
|
|
179
|
+
trace.visited_markings.each do |marking|
|
|
180
|
+
all_states.add(marking.to_h.sort.to_s)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
all_states.size
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetriFlow
|
|
4
|
+
module Simulation
|
|
5
|
+
# Trace of a Petri net execution
|
|
6
|
+
# Records the sequence of transitions fired and markings visited
|
|
7
|
+
class Trace
|
|
8
|
+
attr_reader :transitions, :markings, :initial_marking
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@transitions = []
|
|
12
|
+
@markings = []
|
|
13
|
+
@initial_marking = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Record initial marking
|
|
17
|
+
def record_initial_marking(marking)
|
|
18
|
+
@initial_marking = marking.dup
|
|
19
|
+
@markings << marking.dup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Record a transition firing
|
|
23
|
+
def record_transition(step:, transition_id:, transition_name:, marking_before:, marking_after:)
|
|
24
|
+
@transitions << {
|
|
25
|
+
step: step,
|
|
26
|
+
transition_id: transition_id,
|
|
27
|
+
transition_name: transition_name,
|
|
28
|
+
marking_before: marking_before.dup,
|
|
29
|
+
marking_after: marking_after.dup,
|
|
30
|
+
timestamp: Time.current
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@markings << marking_after.dup
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get number of steps
|
|
37
|
+
def steps
|
|
38
|
+
@transitions.size
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if trace ended in deadlock
|
|
42
|
+
def deadlocked?
|
|
43
|
+
# If trace has transitions, check if last marking had no enabled transitions
|
|
44
|
+
# This is a heuristic - in practice we'd need to check the net state
|
|
45
|
+
false # Placeholder - would need net reference
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get transition firing counts
|
|
49
|
+
def transition_firing_counts
|
|
50
|
+
counts = Hash.new(0)
|
|
51
|
+
@transitions.each do |t|
|
|
52
|
+
counts[t[:transition_id]] += 1
|
|
53
|
+
end
|
|
54
|
+
counts
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get visited markings (unique states)
|
|
58
|
+
def visited_markings
|
|
59
|
+
@markings.uniq { |m| m.to_h.sort.to_s }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get firing sequence (just transition IDs)
|
|
63
|
+
def firing_sequence
|
|
64
|
+
@transitions.map { |t| t[:transition_id] }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get firing sequence with names
|
|
68
|
+
def firing_sequence_names
|
|
69
|
+
@transitions.map { |t| t[:transition_name] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Export trace data
|
|
73
|
+
def to_h
|
|
74
|
+
{
|
|
75
|
+
initial_marking: @initial_marking&.to_h,
|
|
76
|
+
steps: steps,
|
|
77
|
+
transitions: @transitions,
|
|
78
|
+
markings: @markings.map(&:to_h),
|
|
79
|
+
firing_sequence: firing_sequence,
|
|
80
|
+
unique_states: visited_markings.size
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Pretty print trace
|
|
85
|
+
def to_s
|
|
86
|
+
lines = []
|
|
87
|
+
lines << "Trace (#{steps} steps)"
|
|
88
|
+
lines << "Initial: #{@initial_marking&.to_h}"
|
|
89
|
+
lines << ""
|
|
90
|
+
|
|
91
|
+
@transitions.each_with_index do |transition, idx|
|
|
92
|
+
lines << "Step #{transition[:step]}: #{transition[:transition_name]} (#{transition[:transition_id]})"
|
|
93
|
+
lines << " Before: #{transition[:marking_before].to_h}"
|
|
94
|
+
lines << " After: #{transition[:marking_after].to_h}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
lines.join("\n")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Clear trace
|
|
101
|
+
def clear
|
|
102
|
+
@transitions.clear
|
|
103
|
+
@markings.clear
|
|
104
|
+
@initial_marking = nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Statistics about the trace
|
|
108
|
+
def stats
|
|
109
|
+
{
|
|
110
|
+
steps: steps,
|
|
111
|
+
unique_states: visited_markings.size,
|
|
112
|
+
unique_transitions: transition_firing_counts.size,
|
|
113
|
+
most_fired_transition: transition_firing_counts.max_by { |_, c| c }&.first,
|
|
114
|
+
firing_counts: transition_firing_counts
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# PetriFlow Verification Tasks
|
|
4
|
+
#
|
|
5
|
+
# These tasks are automatically loaded in Rails apps that include the petri_flow gem.
|
|
6
|
+
# Workflows are discovered in app/workflows/*_workflow.rb
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# rake petri_flow:verify # Verify all workflows
|
|
10
|
+
# rake petri_flow:verify:list # List registered workflows
|
|
11
|
+
# rake petri_flow:verify:workflow[Name] # Verify specific workflow
|
|
12
|
+
# rake petri_flow:generate:from_state_machine[Model,attr] # Generate from state machine
|
|
13
|
+
# rake petri_flow:generate:scan # Scan for models with state machines
|
|
14
|
+
#
|
|
15
|
+
# rake workflows:verify # Alias for petri_flow:verify
|
|
16
|
+
# rake workflows:list # Alias for petri_flow:verify:list
|
|
17
|
+
|
|
18
|
+
namespace :petri_flow do
|
|
19
|
+
desc "Verify all registered workflows and generate reports"
|
|
20
|
+
task verify: :environment do
|
|
21
|
+
ensure_workflows_loaded
|
|
22
|
+
|
|
23
|
+
if PetriFlow::Registry.count.zero?
|
|
24
|
+
puts "No workflows registered."
|
|
25
|
+
puts "Create workflows in #{workflows_path}/*_workflow.rb"
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
runner = PetriFlow::VerificationRunner.new
|
|
30
|
+
runner.run_all
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
namespace :verify do
|
|
34
|
+
desc "List all registered workflows"
|
|
35
|
+
task list: :environment do
|
|
36
|
+
ensure_workflows_loaded
|
|
37
|
+
|
|
38
|
+
puts "Registered PetriFlow Workflows:"
|
|
39
|
+
puts "-" * 40
|
|
40
|
+
|
|
41
|
+
if PetriFlow::Registry.count.zero?
|
|
42
|
+
puts " No workflows registered."
|
|
43
|
+
puts " Create workflows in #{workflows_path}/*_workflow.rb"
|
|
44
|
+
else
|
|
45
|
+
PetriFlow::Registry.all.each do |workflow_class|
|
|
46
|
+
workflow = workflow_class.new
|
|
47
|
+
places_count = workflow_class.defined_places&.size || 0
|
|
48
|
+
terminal_count = workflow_class.defined_terminal_places&.size || 0
|
|
49
|
+
puts " - #{workflow_class.name}"
|
|
50
|
+
puts " Places: #{places_count}, Terminal: #{terminal_count}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
desc "Verify a specific workflow by class name"
|
|
56
|
+
task :workflow, [:name] => :environment do |_t, args|
|
|
57
|
+
ensure_workflows_loaded
|
|
58
|
+
|
|
59
|
+
name = args[:name]
|
|
60
|
+
unless name
|
|
61
|
+
puts "Usage: rake petri_flow:verify:workflow[WorkflowClassName]"
|
|
62
|
+
puts ""
|
|
63
|
+
puts "Available workflows:"
|
|
64
|
+
PetriFlow::Registry.names.each { |n| puts " - #{n}" }
|
|
65
|
+
exit 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unless PetriFlow::Registry.exists?(name)
|
|
69
|
+
puts "Error: Workflow '#{name}' not found."
|
|
70
|
+
puts ""
|
|
71
|
+
puts "Available workflows:"
|
|
72
|
+
PetriFlow::Registry.names.each { |n| puts " - #{n}" }
|
|
73
|
+
exit 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
runner = PetriFlow::VerificationRunner.new
|
|
77
|
+
runner.run_by_name(name)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
desc "Show PetriFlow configuration"
|
|
82
|
+
task config: :environment do
|
|
83
|
+
config = Rails.application.config.petri_flow
|
|
84
|
+
|
|
85
|
+
puts "PetriFlow Configuration:"
|
|
86
|
+
puts "-" * 40
|
|
87
|
+
puts " Workflows path: #{config.workflows_path}"
|
|
88
|
+
puts " Auto-discover: #{config.auto_discover}"
|
|
89
|
+
puts " Full path: #{workflows_path}"
|
|
90
|
+
puts ""
|
|
91
|
+
|
|
92
|
+
ensure_workflows_loaded
|
|
93
|
+
puts "Registered workflows: #{PetriFlow::Registry.count}"
|
|
94
|
+
PetriFlow::Registry.names.each { |n| puts " - #{n}" }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
namespace :generate do
|
|
98
|
+
desc "Generate a PetriFlow workflow from a model's state machine"
|
|
99
|
+
task :from_state_machine, [:model_class, :state_attr] => :environment do |_t, args|
|
|
100
|
+
model_class_name = args[:model_class]
|
|
101
|
+
state_attr = (args[:state_attr] || :state).to_sym
|
|
102
|
+
|
|
103
|
+
unless model_class_name
|
|
104
|
+
puts "Usage: rake petri_flow:generate:from_state_machine[ModelClass,state_attribute]"
|
|
105
|
+
puts ""
|
|
106
|
+
puts "Examples:"
|
|
107
|
+
puts " rake petri_flow:generate:from_state_machine[Order,state]"
|
|
108
|
+
puts " rake petri_flow:generate:from_state_machine[Spree::Order,state]"
|
|
109
|
+
puts " rake petri_flow:generate:from_state_machine[User,status]"
|
|
110
|
+
puts ""
|
|
111
|
+
puts "Supported state machine libraries:"
|
|
112
|
+
PetriFlow::Generators::WorkflowGenerator.supported_libraries.each do |lib|
|
|
113
|
+
puts " - #{lib}"
|
|
114
|
+
end
|
|
115
|
+
exit 1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
begin
|
|
119
|
+
model_class = model_class_name.constantize
|
|
120
|
+
rescue NameError
|
|
121
|
+
puts "Error: Model class '#{model_class_name}' not found"
|
|
122
|
+
exit 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
unless PetriFlow::Generators::WorkflowGenerator.supports?(model_class)
|
|
126
|
+
puts "Error: #{model_class_name} does not have a supported state machine"
|
|
127
|
+
puts ""
|
|
128
|
+
puts "Supported libraries:"
|
|
129
|
+
PetriFlow::Generators::WorkflowGenerator.supported_libraries.each do |lib|
|
|
130
|
+
puts " - #{lib}"
|
|
131
|
+
end
|
|
132
|
+
exit 1
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
generator = PetriFlow::Generators::WorkflowGenerator.new(model_class, state_attr)
|
|
136
|
+
data = generator.extract
|
|
137
|
+
|
|
138
|
+
puts "=" * 60
|
|
139
|
+
puts "GENERATING WORKFLOW FROM STATE MACHINE"
|
|
140
|
+
puts "=" * 60
|
|
141
|
+
puts ""
|
|
142
|
+
puts "Model: #{model_class_name}"
|
|
143
|
+
puts "State attribute: #{state_attr}"
|
|
144
|
+
puts "Library: #{data[:library]}"
|
|
145
|
+
puts "States: #{data[:states].join(', ')}"
|
|
146
|
+
puts "Initial: #{data[:initial_state]}"
|
|
147
|
+
puts "Terminal: #{data[:terminal_states].join(', ')}"
|
|
148
|
+
puts "Transitions: #{data[:transitions].count}"
|
|
149
|
+
puts ""
|
|
150
|
+
|
|
151
|
+
filepath = generator.generate!
|
|
152
|
+
puts "Generated: #{filepath}"
|
|
153
|
+
puts ""
|
|
154
|
+
|
|
155
|
+
# Verify the generated workflow
|
|
156
|
+
puts "Verifying workflow..."
|
|
157
|
+
results = generator.verify
|
|
158
|
+
|
|
159
|
+
puts " Reachable states: #{results[:reachability][:total_reachable_states]}"
|
|
160
|
+
puts " Is safe (1-bounded): #{results[:boundedness][:is_safe]}"
|
|
161
|
+
puts " Deadlock-free: #{results[:liveness][:deadlock_free]}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
desc "List models with supported state machines"
|
|
165
|
+
task scan: :environment do
|
|
166
|
+
puts "Scanning for models with state machines..."
|
|
167
|
+
puts ""
|
|
168
|
+
|
|
169
|
+
found = []
|
|
170
|
+
Rails.application.eager_load! if defined?(Rails)
|
|
171
|
+
|
|
172
|
+
ActiveRecord::Base.descendants.each do |model|
|
|
173
|
+
next if model.abstract_class?
|
|
174
|
+
|
|
175
|
+
library = PetriFlow::Generators::WorkflowGenerator.detect_library(model)
|
|
176
|
+
if library
|
|
177
|
+
found << { model: model.name, library: library }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
if found.empty?
|
|
182
|
+
puts "No models with supported state machines found."
|
|
183
|
+
puts ""
|
|
184
|
+
puts "Supported libraries:"
|
|
185
|
+
PetriFlow::Generators::WorkflowGenerator.supported_libraries.each do |lib|
|
|
186
|
+
puts " - #{lib}"
|
|
187
|
+
end
|
|
188
|
+
else
|
|
189
|
+
puts "Found #{found.count} model(s) with state machines:"
|
|
190
|
+
puts ""
|
|
191
|
+
puts "| Model | Library |"
|
|
192
|
+
puts "|-------|---------|"
|
|
193
|
+
found.each do |f|
|
|
194
|
+
puts "| #{f[:model]} | #{f[:library]} |"
|
|
195
|
+
end
|
|
196
|
+
puts ""
|
|
197
|
+
puts "Generate with:"
|
|
198
|
+
puts " rake petri_flow:generate:from_state_machine[ModelClass,state]"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Helper methods
|
|
204
|
+
|
|
205
|
+
def ensure_workflows_loaded
|
|
206
|
+
path = workflows_path
|
|
207
|
+
PetriFlow::Registry.discover_in(path) if Dir.exist?(path)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def workflows_path
|
|
211
|
+
config = Rails.application.config.petri_flow
|
|
212
|
+
Rails.root.join(config.workflows_path).to_s
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Shorthand aliases
|
|
217
|
+
namespace :workflows do
|
|
218
|
+
desc "Verify all workflows (alias for petri_flow:verify)"
|
|
219
|
+
task verify: "petri_flow:verify"
|
|
220
|
+
|
|
221
|
+
desc "List all workflows (alias for petri_flow:verify:list)"
|
|
222
|
+
task list: "petri_flow:verify:list"
|
|
223
|
+
|
|
224
|
+
desc "Generate workflow from state machine (alias)"
|
|
225
|
+
task :from_state_machine, [:model_class, :state_attr] => "petri_flow:generate:from_state_machine"
|
|
226
|
+
|
|
227
|
+
desc "Scan for models with state machines (alias)"
|
|
228
|
+
task scan: "petri_flow:generate:scan"
|
|
229
|
+
end
|