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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/MIT-LICENSE +22 -0
  4. data/README.md +592 -0
  5. data/Rakefile +28 -0
  6. data/lib/petri_flow/colored/arc_expression.rb +163 -0
  7. data/lib/petri_flow/colored/color.rb +40 -0
  8. data/lib/petri_flow/colored/colored_net.rb +146 -0
  9. data/lib/petri_flow/colored/guard.rb +104 -0
  10. data/lib/petri_flow/core/arc.rb +63 -0
  11. data/lib/petri_flow/core/marking.rb +64 -0
  12. data/lib/petri_flow/core/net.rb +121 -0
  13. data/lib/petri_flow/core/place.rb +54 -0
  14. data/lib/petri_flow/core/token.rb +55 -0
  15. data/lib/petri_flow/core/transition.rb +88 -0
  16. data/lib/petri_flow/export/cpn_tools_exporter.rb +322 -0
  17. data/lib/petri_flow/export/json_exporter.rb +224 -0
  18. data/lib/petri_flow/export/pnml_exporter.rb +229 -0
  19. data/lib/petri_flow/export/yaml_exporter.rb +246 -0
  20. data/lib/petri_flow/export.rb +193 -0
  21. data/lib/petri_flow/generators/adapters/aasm_adapter.rb +69 -0
  22. data/lib/petri_flow/generators/adapters/state_machines_adapter.rb +83 -0
  23. data/lib/petri_flow/generators/state_machine_adapter.rb +47 -0
  24. data/lib/petri_flow/generators/workflow_generator.rb +176 -0
  25. data/lib/petri_flow/matrix/analyzer.rb +151 -0
  26. data/lib/petri_flow/matrix/causation.rb +126 -0
  27. data/lib/petri_flow/matrix/correlation.rb +79 -0
  28. data/lib/petri_flow/matrix/crud_event_mapping.rb +74 -0
  29. data/lib/petri_flow/matrix/lineage.rb +113 -0
  30. data/lib/petri_flow/matrix/reachability.rb +128 -0
  31. data/lib/petri_flow/railtie.rb +41 -0
  32. data/lib/petri_flow/registry.rb +85 -0
  33. data/lib/petri_flow/simulation/simulator.rb +188 -0
  34. data/lib/petri_flow/simulation/trace.rb +119 -0
  35. data/lib/petri_flow/tasks/petri_flow.rake +229 -0
  36. data/lib/petri_flow/verification/boundedness_checker.rb +127 -0
  37. data/lib/petri_flow/verification/invariant_checker.rb +144 -0
  38. data/lib/petri_flow/verification/liveness_checker.rb +153 -0
  39. data/lib/petri_flow/verification/reachability_analyzer.rb +152 -0
  40. data/lib/petri_flow/verification_runner.rb +287 -0
  41. data/lib/petri_flow/version.rb +5 -0
  42. data/lib/petri_flow/visualization/graphviz.rb +220 -0
  43. data/lib/petri_flow/visualization/mermaid.rb +191 -0
  44. data/lib/petri_flow/workflow.rb +228 -0
  45. data/lib/petri_flow.rb +164 -0
  46. metadata +174 -0
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Matrix
5
+ # Main analyzer class that combines all matrix types
6
+ # Provides comprehensive analysis of Petri net execution
7
+ class Analyzer
8
+ attr_reader :crud_mapping, :correlation, :causation, :lineage, :reachability
9
+
10
+ def initialize
11
+ @crud_mapping = CrudEventMapping.new
12
+ @correlation = Correlation.new
13
+ @causation = Causation.new
14
+ @lineage = Lineage.new
15
+ @reachability = nil
16
+ end
17
+
18
+ # Analyze events and build all matrices
19
+ def analyze_events(events)
20
+ events.each do |event|
21
+ analyze_event(event)
22
+ end
23
+
24
+ self
25
+ end
26
+
27
+ # Analyze a single event
28
+ def analyze_event(event)
29
+ event_id = event[:event_id] || event[:id]
30
+
31
+ # CRUD mapping
32
+ if event[:operation] && event[:event_type]
33
+ @crud_mapping.record_mapping(event[:operation], event[:event_type])
34
+ end
35
+
36
+ # Correlation
37
+ if event[:correlation_id]
38
+ # Find other events with same correlation_id
39
+ # (In real usage, this would query event store)
40
+ end
41
+
42
+ # Causation
43
+ if event[:caused_by_event_id]
44
+ @causation.record_causation(event[:caused_by_event_id], event_id)
45
+ end
46
+
47
+ # Lineage
48
+ if event[:changes]
49
+ event[:changes].each do |field, (old_val, new_val)|
50
+ @lineage.record_modification(
51
+ field,
52
+ event_id,
53
+ old_value: old_val,
54
+ new_value: new_val,
55
+ timestamp: event[:timestamp]
56
+ )
57
+ end
58
+ end
59
+
60
+ self
61
+ end
62
+
63
+ # Compute reachability from a Petri net
64
+ def compute_reachability(net, initial_marking)
65
+ @reachability = Reachability.compute_from_net(net, initial_marking)
66
+ self
67
+ end
68
+
69
+ # Generate comprehensive report
70
+ def generate_report
71
+ {
72
+ crud_mapping: {
73
+ summary: @crud_mapping.to_s,
74
+ table: @crud_mapping.to_table,
75
+ stats: crud_mapping_stats
76
+ },
77
+ correlation: {
78
+ summary: @correlation.to_s,
79
+ stats: @correlation.stats
80
+ },
81
+ causation: {
82
+ summary: @causation.to_s,
83
+ stats: @causation.stats,
84
+ centrality: @causation.centrality_scores
85
+ },
86
+ lineage: {
87
+ summary: @lineage.to_s,
88
+ stats: @lineage.stats
89
+ },
90
+ reachability: @reachability ? {
91
+ summary: @reachability.to_s,
92
+ stats: @reachability.stats
93
+ } : nil
94
+ }.compact
95
+ end
96
+
97
+ # Find events in causation chain
98
+ def find_causation_chain(start_event, end_event)
99
+ @causation.causation_chain(start_event, end_event)
100
+ end
101
+
102
+ # Get complete field history
103
+ def field_history(field)
104
+ @lineage.field_lineage(field)
105
+ end
106
+
107
+ # Get events in correlation group
108
+ def correlation_group(event_id)
109
+ @correlation.correlated_events(event_id)
110
+ end
111
+
112
+ # Privacy impact analysis
113
+ def privacy_impact_analysis
114
+ {
115
+ fields_tracked: @lineage.fields.size,
116
+ events_with_changes: @lineage.events.size,
117
+ total_modifications: @lineage.stats[:total_modifications],
118
+ most_modified_field: @lineage.stats[:most_modified_field]
119
+ }
120
+ end
121
+
122
+ # Event flow completeness check
123
+ def flow_completeness
124
+ total_crud_ops = crud_mapping_stats[:total_mappings]
125
+ total_events = @causation.events.size
126
+
127
+ {
128
+ crud_operations: total_crud_ops,
129
+ events_generated: total_events,
130
+ mapping_ratio: total_events.to_f / [total_crud_ops, 1].max,
131
+ completeness: total_events >= total_crud_ops ? "✓ Complete" : "⚠ Incomplete"
132
+ }
133
+ end
134
+
135
+ def to_s
136
+ "MatrixAnalyzer(CRUD: #{@crud_mapping}, Correlation: #{@correlation}, " \
137
+ "Causation: #{@causation}, Lineage: #{@lineage})"
138
+ end
139
+
140
+ private
141
+
142
+ def crud_mapping_stats
143
+ {
144
+ total_mappings: @crud_mapping.crud_ops.sum { |op| @crud_mapping.total_events_for(op) },
145
+ operations: @crud_mapping.crud_ops.size,
146
+ event_types: @crud_mapping.event_types.size
147
+ }
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Matrix
5
+ # Causation Matrix (Cau)
6
+ # Tracks which events cause other events (event flow)
7
+ class Causation
8
+ attr_reader :matrix, :events
9
+
10
+ def initialize
11
+ @matrix = Hash.new { |h, k| h[k] = Hash.new(0) }
12
+ @events = []
13
+ end
14
+
15
+ # Record that event1 causes event2
16
+ def record_causation(cause_event_id, effect_event_id)
17
+ @events << cause_event_id unless @events.include?(cause_event_id)
18
+ @events << effect_event_id unless @events.include?(effect_event_id)
19
+
20
+ @matrix[cause_event_id][effect_event_id] = 1
21
+ end
22
+
23
+ # Check if event1 directly causes event2
24
+ def causes?(cause_event_id, effect_event_id)
25
+ @matrix[cause_event_id][effect_event_id] == 1
26
+ end
27
+
28
+ # Get all events directly caused by an event
29
+ def effects_of(event_id)
30
+ return [] unless @matrix[event_id]
31
+
32
+ @matrix[event_id].select { |_, v| v == 1 }.keys
33
+ end
34
+
35
+ # Get all events that directly cause an event
36
+ def causes_of(event_id)
37
+ @matrix.select { |_, effects| effects[event_id] == 1 }.keys
38
+ end
39
+
40
+ # Compute transitive closure (Cau*)
41
+ # Returns matrix showing all reachable events (direct + indirect causation)
42
+ def transitive_closure
43
+ closure = Causation.new
44
+ @events.each { |e| closure.instance_variable_get(:@events) << e }
45
+
46
+ # Copy direct causations
47
+ @matrix.each do |cause, effects|
48
+ effects.each do |effect, value|
49
+ closure.matrix[cause][effect] = value if value == 1
50
+ end
51
+ end
52
+
53
+ # Warshall's algorithm for transitive closure
54
+ @events.each do |k|
55
+ @events.each do |i|
56
+ @events.each do |j|
57
+ if closure.matrix[i][k] == 1 && closure.matrix[k][j] == 1
58
+ closure.matrix[i][j] = 1
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ closure
65
+ end
66
+
67
+ # Find causation chain from event1 to event2
68
+ def causation_chain(start_event_id, end_event_id)
69
+ return [] unless @events.include?(start_event_id) && @events.include?(end_event_id)
70
+ return [start_event_id] if start_event_id == end_event_id
71
+
72
+ # BFS to find shortest path
73
+ queue = [[start_event_id]]
74
+ visited = Set.new([start_event_id])
75
+
76
+ while queue.any?
77
+ path = queue.shift
78
+ current = path.last
79
+
80
+ effects_of(current).each do |effect|
81
+ return path + [effect] if effect == end_event_id
82
+
83
+ unless visited.include?(effect)
84
+ visited.add(effect)
85
+ queue << (path + [effect])
86
+ end
87
+ end
88
+ end
89
+
90
+ [] # No path found
91
+ end
92
+
93
+ # Convert to 2D matrix representation
94
+ def to_matrix
95
+ rows = @events.map do |cause|
96
+ @events.map { |effect| @matrix[cause][effect] }
97
+ end
98
+ ::Matrix.rows(rows)
99
+ end
100
+
101
+ # Calculate centrality scores (which events are most influential)
102
+ def centrality_scores
103
+ scores = Hash.new(0)
104
+
105
+ @matrix.each do |cause, effects|
106
+ scores[cause] += effects.values.sum
107
+ end
108
+
109
+ scores
110
+ end
111
+
112
+ def stats
113
+ {
114
+ total_events: @events.size,
115
+ total_causations: @matrix.values.map { |h| h.values.sum }.sum,
116
+ root_events: @events.count { |e| causes_of(e).empty? },
117
+ leaf_events: @events.count { |e| effects_of(e).empty? }
118
+ }
119
+ end
120
+
121
+ def to_s
122
+ "CausationMatrix(#{@events.size} events, #{stats[:total_causations]} causations)"
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Matrix
5
+ # Correlation Matrix (C)
6
+ # Tracks which events share correlation IDs (belong to same user action)
7
+ class Correlation
8
+ attr_reader :matrix, :events
9
+
10
+ def initialize
11
+ @matrix = Hash.new { |h, k| h[k] = {} }
12
+ @events = []
13
+ end
14
+
15
+ # Record correlation between two events
16
+ def record_correlation(event1_id, event2_id, correlation_id)
17
+ @events << event1_id unless @events.include?(event1_id)
18
+ @events << event2_id unless @events.include?(event2_id)
19
+
20
+ @matrix[event1_id][event2_id] = correlation_id
21
+ @matrix[event2_id][event1_id] = correlation_id # Symmetric
22
+ end
23
+
24
+ # Check if two events are correlated
25
+ def correlated?(event1_id, event2_id)
26
+ !@matrix.dig(event1_id, event2_id).nil?
27
+ end
28
+
29
+ # Get correlation ID between two events
30
+ def get_correlation_id(event1_id, event2_id)
31
+ @matrix.dig(event1_id, event2_id)
32
+ end
33
+
34
+ # Get all events correlated with a given event
35
+ def correlated_events(event_id)
36
+ return [] unless @matrix[event_id]
37
+
38
+ @matrix[event_id].select { |_, corr_id| !corr_id.nil? }.keys
39
+ end
40
+
41
+ # Group events by correlation ID
42
+ def correlation_groups
43
+ groups = Hash.new { |h, k| h[k] = [] }
44
+
45
+ @matrix.each do |event1_id, correlations|
46
+ correlations.each do |event2_id, correlation_id|
47
+ next if correlation_id.nil?
48
+
49
+ groups[correlation_id] << event1_id unless groups[correlation_id].include?(event1_id)
50
+ groups[correlation_id] << event2_id unless groups[correlation_id].include?(event2_id)
51
+ end
52
+ end
53
+
54
+ groups.transform_values(&:uniq)
55
+ end
56
+
57
+ # Convert to 2D matrix representation
58
+ def to_matrix
59
+ size = @events.size
60
+ rows = @events.map do |event1|
61
+ @events.map { |event2| correlated?(event1, event2) ? 1 : 0 }
62
+ end
63
+ ::Matrix.rows(rows)
64
+ end
65
+
66
+ def stats
67
+ {
68
+ total_events: @events.size,
69
+ correlation_groups: correlation_groups.size,
70
+ average_group_size: correlation_groups.values.map(&:size).sum.to_f / [correlation_groups.size, 1].max
71
+ }
72
+ end
73
+
74
+ def to_s
75
+ "CorrelationMatrix(#{@events.size} events, #{correlation_groups.size} groups)"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'matrix'
4
+
5
+ module PetriFlow
6
+ module Matrix
7
+ # CRUD-to-Event Mapping Matrix (M_ce)
8
+ # Tracks which CRUD operations generate which event types
9
+ class CrudEventMapping
10
+ CRUD_OPERATIONS = %i[create update delete].freeze
11
+
12
+ attr_reader :matrix, :crud_ops, :event_types
13
+
14
+ def initialize
15
+ @matrix = {}
16
+ @crud_ops = CRUD_OPERATIONS.dup
17
+ @event_types = []
18
+ end
19
+
20
+ # Record that a CRUD operation generates an event type
21
+ def record_mapping(crud_op, event_type, count = 1)
22
+ validate_crud_op!(crud_op)
23
+
24
+ @event_types << event_type unless @event_types.include?(event_type)
25
+ @matrix[crud_op] ||= Hash.new(0)
26
+ @matrix[crud_op][event_type] += count
27
+ end
28
+
29
+ # Get mapping value for specific CRUD operation and event type
30
+ def get(crud_op, event_type)
31
+ @matrix.dig(crud_op, event_type) || 0
32
+ end
33
+
34
+ # Get total events generated by a CRUD operation
35
+ def total_events_for(crud_op)
36
+ @matrix[crud_op]&.values&.sum || 0
37
+ end
38
+
39
+ # Get all event types generated by a CRUD operation
40
+ def events_for(crud_op)
41
+ @matrix[crud_op]&.keys || []
42
+ end
43
+
44
+ # Convert to 2D array matrix
45
+ def to_matrix
46
+ rows = @crud_ops.map do |crud_op|
47
+ @event_types.map { |event_type| get(crud_op, event_type) }
48
+ end
49
+ ::Matrix.rows(rows)
50
+ end
51
+
52
+ # Pretty print the mapping matrix
53
+ def to_table
54
+ header = ["CRUD \\ Event"] + @event_types.map(&:to_s)
55
+ rows = @crud_ops.map do |crud_op|
56
+ [crud_op.to_s.upcase] + @event_types.map { |et| get(crud_op, et) }
57
+ end
58
+ { header: header, rows: rows }
59
+ end
60
+
61
+ def to_s
62
+ "CrudEventMapping(#{@crud_ops.size} operations, #{@event_types.size} event types)"
63
+ end
64
+
65
+ private
66
+
67
+ def validate_crud_op!(crud_op)
68
+ return if CRUD_OPERATIONS.include?(crud_op.to_sym)
69
+
70
+ raise ArgumentError, "Invalid CRUD operation: #{crud_op}. Must be one of #{CRUD_OPERATIONS}"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Matrix
5
+ # Data Lineage Matrix (L)
6
+ # Tracks which events modified which fields (data lineage)
7
+ class Lineage
8
+ attr_reader :matrix, :fields, :events
9
+
10
+ def initialize
11
+ @matrix = Hash.new { |h, k| h[k] = Hash.new(0) }
12
+ @fields = []
13
+ @events = []
14
+ end
15
+
16
+ # Record that an event modified a field
17
+ def record_modification(field, event_id, old_value: nil, new_value: nil, timestamp: nil)
18
+ @fields << field unless @fields.include?(field)
19
+ @events << event_id unless @events.include?(event_id)
20
+
21
+ @matrix[field][event_id] = {
22
+ modified: 1,
23
+ old_value: old_value,
24
+ new_value: new_value,
25
+ timestamp: timestamp || Time.current
26
+ }
27
+ end
28
+
29
+ # Check if an event modified a field
30
+ def modified?(field, event_id)
31
+ value = @matrix.dig(field, event_id)
32
+ value.is_a?(Hash) && value[:modified] == 1
33
+ end
34
+
35
+ # Get all events that modified a field
36
+ def events_for_field(field)
37
+ return [] unless @matrix[field]
38
+
39
+ @matrix[field].select { |_, v| v.is_a?(Hash) && v[:modified] == 1 }.keys
40
+ end
41
+
42
+ # Get all fields modified by an event
43
+ def fields_for_event(event_id)
44
+ @fields.select { |field| modified?(field, event_id) }
45
+ end
46
+
47
+ # Get complete lineage for a field (temporal history)
48
+ def field_lineage(field)
49
+ return [] unless @matrix[field]
50
+
51
+ modifications = @matrix[field]
52
+ .select { |_, v| v.is_a?(Hash) && v[:modified] == 1 }
53
+ .map do |event_id, data|
54
+ {
55
+ event_id: event_id,
56
+ old_value: data[:old_value],
57
+ new_value: data[:new_value],
58
+ timestamp: data[:timestamp]
59
+ }
60
+ end
61
+
62
+ modifications.sort_by { |m| m[:timestamp] }
63
+ end
64
+
65
+ # Reconstruct field value at a specific point in time
66
+ def reconstruct_value(field, at_time)
67
+ lineage = field_lineage(field)
68
+ return nil if lineage.empty?
69
+
70
+ # Find last modification before at_time
71
+ applicable_mods = lineage.select { |m| m[:timestamp] <= at_time }
72
+ return nil if applicable_mods.empty?
73
+
74
+ applicable_mods.last[:new_value]
75
+ end
76
+
77
+ # Get field value chain (how value evolved)
78
+ def value_chain(field)
79
+ lineage = field_lineage(field)
80
+ return [] if lineage.empty?
81
+
82
+ chain = []
83
+ lineage.each do |mod|
84
+ chain << mod[:old_value] if chain.empty? && !mod[:old_value].nil?
85
+ chain << mod[:new_value] unless mod[:new_value].nil?
86
+ end
87
+
88
+ chain.uniq
89
+ end
90
+
91
+ # Convert to 2D matrix representation
92
+ def to_matrix
93
+ rows = @fields.map do |field|
94
+ @events.map { |event| modified?(field, event) ? 1 : 0 }
95
+ end
96
+ ::Matrix.rows(rows)
97
+ end
98
+
99
+ def stats
100
+ {
101
+ total_fields: @fields.size,
102
+ total_events: @events.size,
103
+ total_modifications: @matrix.values.map { |h| h.values.count { |v| v.is_a?(Hash) && v[:modified] == 1 } }.sum,
104
+ most_modified_field: @fields.max_by { |f| events_for_field(f).size }
105
+ }
106
+ end
107
+
108
+ def to_s
109
+ "LineageMatrix(#{@fields.size} fields, #{@events.size} events)"
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Matrix
5
+ # Reachability Matrix (R)
6
+ # Tracks which markings (states) are reachable from other markings
7
+ class Reachability
8
+ attr_reader :matrix, :markings
9
+
10
+ def initialize
11
+ @matrix = Hash.new { |h, k| h[k] = Hash.new(0) }
12
+ @markings = []
13
+ end
14
+
15
+ # Record that marking_j is reachable from marking_i
16
+ def record_reachability(marking_i, marking_j)
17
+ marking_i_key = marking_key(marking_i)
18
+ marking_j_key = marking_key(marking_j)
19
+
20
+ @markings << marking_i_key unless @markings.include?(marking_i_key)
21
+ @markings << marking_j_key unless @markings.include?(marking_j_key)
22
+
23
+ @matrix[marking_i_key][marking_j_key] = 1
24
+ end
25
+
26
+ # Check if marking_j is reachable from marking_i
27
+ def reachable?(marking_i, marking_j)
28
+ marking_i_key = marking_key(marking_i)
29
+ marking_j_key = marking_key(marking_j)
30
+
31
+ @matrix[marking_i_key][marking_j_key] == 1
32
+ end
33
+
34
+ # Get all markings reachable from a given marking
35
+ def reachable_from(marking)
36
+ marking_key_val = marking_key(marking)
37
+ return [] unless @matrix[marking_key_val]
38
+
39
+ @matrix[marking_key_val].select { |_, v| v == 1 }.keys
40
+ end
41
+
42
+ # Compute reachability graph from a Petri net and initial marking
43
+ def self.compute_from_net(net, initial_marking)
44
+ reachability = new
45
+ visited = Set.new
46
+ queue = [initial_marking.dup]
47
+
48
+ reachability.record_reachability(initial_marking, initial_marking)
49
+
50
+ while queue.any?
51
+ current_marking = queue.shift
52
+ current_key = reachability.send(:marking_key, current_marking)
53
+
54
+ next if visited.include?(current_key)
55
+ visited.add(current_key)
56
+
57
+ # Set net to current marking
58
+ net.set_marking(current_marking)
59
+
60
+ # Try firing each enabled transition
61
+ net.enabled_transitions.each do |transition|
62
+ # Save current state
63
+ saved_marking = net.current_marking.dup
64
+
65
+ # Fire transition
66
+ begin
67
+ transition.fire!
68
+ new_marking = net.current_marking
69
+
70
+ # Record reachability
71
+ reachability.record_reachability(initial_marking, new_marking)
72
+ reachability.record_reachability(current_marking, new_marking)
73
+
74
+ # Add to queue if not visited
75
+ new_key = reachability.send(:marking_key, new_marking)
76
+ queue << new_marking.dup unless visited.include?(new_key)
77
+ ensure
78
+ # Restore marking for next iteration
79
+ net.set_marking(saved_marking)
80
+ end
81
+ end
82
+ end
83
+
84
+ reachability
85
+ end
86
+
87
+ # Convert to 2D matrix representation
88
+ def to_matrix
89
+ rows = @markings.map do |marking_i|
90
+ @markings.map { |marking_j| @matrix[marking_i][marking_j] }
91
+ end
92
+ ::Matrix.rows(rows)
93
+ end
94
+
95
+ # Check for deadlock states (markings with no successors)
96
+ def deadlock_states
97
+ @markings.select do |marking|
98
+ reachable_from(marking).empty?
99
+ end
100
+ end
101
+
102
+ def stats
103
+ {
104
+ total_markings: @markings.size,
105
+ total_transitions: @matrix.values.map { |h| h.values.sum }.sum,
106
+ deadlock_states: deadlock_states.size
107
+ }
108
+ end
109
+
110
+ def to_s
111
+ "ReachabilityMatrix(#{@markings.size} markings, #{stats[:total_transitions]} transitions)"
112
+ end
113
+
114
+ private
115
+
116
+ def marking_key(marking)
117
+ case marking
118
+ when Core::Marking
119
+ marking.to_h.sort.to_s
120
+ when Hash
121
+ marking.sort.to_s
122
+ else
123
+ marking.to_s
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module PetriFlow
6
+ # Rails integration for PetriFlow.
7
+ # Automatically loads rake tasks and discovers workflows in app/workflows/.
8
+ #
9
+ # @example Configure in config/application.rb or an initializer
10
+ # Rails.application.configure do
11
+ # config.petri_flow.workflows_path = "app/workflows"
12
+ # config.petri_flow.auto_discover = true
13
+ # end
14
+ #
15
+ class Railtie < Rails::Railtie
16
+ railtie_name :petri_flow
17
+
18
+ # Load rake tasks
19
+ rake_tasks do
20
+ load File.expand_path("tasks/petri_flow.rake", __dir__)
21
+ end
22
+
23
+ # Configuration defaults
24
+ config.petri_flow = ActiveSupport::OrderedOptions.new
25
+ config.petri_flow.workflows_path = "app/workflows"
26
+ config.petri_flow.auto_discover = true
27
+
28
+ # Store configuration reference
29
+ initializer "petri_flow.configuration" do |app|
30
+ PetriFlow.rails_config = app.config.petri_flow
31
+ end
32
+
33
+ # Auto-discover workflows after initialization
34
+ config.after_initialize do |app|
35
+ if app.config.petri_flow.auto_discover
36
+ workflows_dir = app.root.join(app.config.petri_flow.workflows_path)
37
+ Registry.discover_in(workflows_dir.to_s)
38
+ end
39
+ end
40
+ end
41
+ end