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,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
|