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
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "lib"
6
+ t.libs << "test"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ t.verbose = true
9
+ end
10
+
11
+ task default: :test
12
+
13
+ desc "Run console with gem loaded"
14
+ task :console do
15
+ require "irb"
16
+ require "petri_flow"
17
+ ARGV.clear
18
+ IRB.start
19
+ end
20
+
21
+ desc "Generate example outputs"
22
+ task :examples do
23
+ Dir.glob("examples/**/*.rb").each do |example|
24
+ puts "Running #{example}..."
25
+ load example
26
+ puts ""
27
+ end
28
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Colored
5
+ # Represents an arc expression for colored Petri nets
6
+ # Arc expressions transform token data as it passes through the arc
7
+ class ArcExpression
8
+ attr_reader :name, :transformation
9
+
10
+ def initialize(name: nil, &transformation)
11
+ @name = name
12
+ @transformation = transformation
13
+ end
14
+
15
+ # Execute the arc expression on token data
16
+ # @param token_data [Hash] The token's data
17
+ # @param context [Hash] Additional context (metadata, state, etc.)
18
+ # @return [Hash] Transformed token data
19
+ def execute(token_data, context = {})
20
+ return token_data unless @transformation
21
+
22
+ @transformation.call(token_data, context)
23
+ end
24
+
25
+ # Compose arc expressions (chain transformations)
26
+ def then(other_expression)
27
+ ArcExpression.new(name: "#{@name} then #{other_expression.name}") do |data, context|
28
+ intermediate = execute(data, context)
29
+ other_expression.execute(intermediate, context)
30
+ end
31
+ end
32
+
33
+ def to_s
34
+ "ArcExpression(#{@name || 'anonymous'})"
35
+ end
36
+
37
+ def inspect
38
+ "#<PetriFlow::Colored::ArcExpression name=#{@name}>"
39
+ end
40
+ end
41
+
42
+ # Common arc expression factories
43
+ module ArcExpressions
44
+ # Identity expression (pass-through)
45
+ def self.identity
46
+ ArcExpression.new(name: "identity") { |data, _| data }
47
+ end
48
+
49
+ # Map specific fields
50
+ def self.map_fields(field_mappings)
51
+ ArcExpression.new(name: "map_fields") do |data, _|
52
+ result = data.dup
53
+ field_mappings.each do |old_field, new_field|
54
+ result[new_field] = result.delete(old_field) if result.key?(old_field)
55
+ end
56
+ result
57
+ end
58
+ end
59
+
60
+ # Add metadata
61
+ def self.add_metadata(metadata)
62
+ ArcExpression.new(name: "add_metadata") do |data, _|
63
+ data.merge(metadata: metadata)
64
+ end
65
+ end
66
+
67
+ # Transform CRUD to Event (for Lyra integration)
68
+ def self.crud_to_event(operation)
69
+ ArcExpression.new(name: "crud_to_event(#{operation})") do |data, context|
70
+ {
71
+ event_type: "#{data[:model_class]}#{operation.to_s.capitalize}",
72
+ event_id: SecureRandom.uuid,
73
+ data: data[:attributes] || {},
74
+ changes: data[:changes] || {},
75
+ metadata: {
76
+ correlation_id: data[:correlation_id],
77
+ action_id: data[:action_id],
78
+ user_id: data[:user_id],
79
+ timestamp: Time.current
80
+ }
81
+ }
82
+ end
83
+ end
84
+
85
+ # Detect PII in data
86
+ def self.detect_pii(pii_detector = nil)
87
+ ArcExpression.new(name: "detect_pii") do |data, context|
88
+ pii_detected = if pii_detector
89
+ pii_detector.call(data)
90
+ else
91
+ # Simple pattern-based detection
92
+ detect_pii_patterns(data)
93
+ end
94
+
95
+ data.merge(pii_detected: pii_detected)
96
+ end
97
+ end
98
+
99
+ # Apply privacy policy transformations
100
+ def self.apply_privacy_policy(policy)
101
+ ArcExpression.new(name: "apply_policy(#{policy})") do |data, context|
102
+ # Mask sensitive fields based on policy
103
+ masked_data = data.dup
104
+ policy_rules = context.dig(:policies, policy) || {}
105
+
106
+ policy_rules.each do |field, rule|
107
+ if masked_data[field] && rule[:mask]
108
+ masked_data[field] = mask_value(masked_data[field], rule[:mask])
109
+ end
110
+ end
111
+
112
+ masked_data
113
+ end
114
+ end
115
+
116
+ # Extract specific fields
117
+ def self.extract_fields(*fields)
118
+ ArcExpression.new(name: "extract_fields(#{fields.join(',')})") do |data, _|
119
+ data.slice(*fields)
120
+ end
121
+ end
122
+
123
+ # Merge with context data
124
+ def self.merge_context(*keys)
125
+ ArcExpression.new(name: "merge_context") do |data, context|
126
+ additional_data = keys.each_with_object({}) do |key, hash|
127
+ hash[key] = context[key] if context.key?(key)
128
+ end
129
+ data.merge(additional_data)
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def self.detect_pii_patterns(data)
136
+ pii = {}
137
+ data.each do |key, value|
138
+ next unless value.is_a?(String)
139
+
140
+ pii[key] = :email if key.to_s.match?(/email/i) || value.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
141
+ pii[key] = :phone if key.to_s.match?(/phone/i) || value.match?(/\A\d{10,}\z/)
142
+ pii[key] = :ssn if key.to_s.match?(/ssn|social/i)
143
+ pii[key] = :credit_card if value.match?(/\A\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\z/)
144
+ end
145
+ pii
146
+ end
147
+
148
+ def self.mask_value(value, mask_type)
149
+ case mask_type
150
+ when :full
151
+ "***MASKED***"
152
+ when :partial
153
+ return value if value.length < 4
154
+ value[0..1] + "*" * (value.length - 4) + value[-2..-1]
155
+ when :hash
156
+ Digest::SHA256.hexdigest(value.to_s)[0..15]
157
+ else
158
+ value
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Colored
5
+ # Represents a color (data type) for tokens in a colored Petri net
6
+ class Color
7
+ attr_reader :name, :attributes, :validator
8
+
9
+ def initialize(name:, attributes: {}, validator: nil)
10
+ @name = name
11
+ @attributes = attributes
12
+ @validator = validator
13
+ end
14
+
15
+ # Validate data conforms to this color definition
16
+ def valid?(data)
17
+ return true unless @validator
18
+
19
+ @validator.call(data)
20
+ end
21
+
22
+ # Create a token of this color
23
+ def create_token(data = {})
24
+ raise ColorValidationError, "Invalid data for color #{@name}" unless valid?(data)
25
+
26
+ Core::Token.new(color: @name, data: data)
27
+ end
28
+
29
+ def to_s
30
+ "Color(#{@name})"
31
+ end
32
+
33
+ def inspect
34
+ "#<PetriFlow::Colored::Color name=#{@name} attributes=#{@attributes.keys}>"
35
+ end
36
+ end
37
+
38
+ class ColorValidationError < StandardError; end
39
+ end
40
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Colored
5
+ # Represents a Colored Petri Net (CPN)
6
+ # Extends basic Petri net with colored tokens, guards, and arc expressions
7
+ class ColoredNet < Core::Net
8
+ attr_reader :colors, :colored_places, :token_pools
9
+
10
+ def initialize(name: "ColoredPetriNet")
11
+ super(name: name)
12
+ @colors = {}
13
+ @colored_places = {}
14
+ @token_pools = Hash.new { |h, k| h[k] = [] }
15
+ end
16
+
17
+ # Register a color (token type)
18
+ def add_color(name, attributes: {}, validator: nil)
19
+ color = Color.new(name: name, attributes: attributes, validator: validator)
20
+ @colors[name] = color
21
+ color
22
+ end
23
+
24
+ # Add a colored place
25
+ def add_colored_place(id:, name: nil, color: nil, initial_tokens: [])
26
+ place = add_place(id: id, name: name, initial_tokens: 0)
27
+ @colored_places[id] = {
28
+ color: color,
29
+ tokens: initial_tokens
30
+ }
31
+ @token_pools[id] = initial_tokens.dup
32
+ place
33
+ end
34
+
35
+ # Add a colored transition with guard
36
+ def add_colored_transition(id:, name: nil, guard: nil)
37
+ guard_obj = case guard
38
+ when Guard
39
+ guard
40
+ when Proc
41
+ Guard.new(&guard)
42
+ else
43
+ nil
44
+ end
45
+
46
+ add_transition(id: id, name: name, guard: guard_obj)
47
+ end
48
+
49
+ # Add an arc with expression
50
+ def add_colored_arc(source_id:, target_id:, weight: 1, expression: nil)
51
+ expr_obj = case expression
52
+ when ArcExpression
53
+ expression
54
+ when Proc
55
+ ArcExpression.new(&expression)
56
+ else
57
+ nil
58
+ end
59
+
60
+ add_arc(
61
+ source_id: source_id,
62
+ target_id: target_id,
63
+ weight: weight,
64
+ expression: expr_obj
65
+ )
66
+ end
67
+
68
+ # Get tokens from a colored place
69
+ def tokens_at_place(place_id)
70
+ @token_pools[place_id] || []
71
+ end
72
+
73
+ # Add a token to a colored place
74
+ def add_token_to_place(place_id, token)
75
+ raise "Place #{place_id} not found" unless @places[place_id]
76
+
77
+ @token_pools[place_id] << token
78
+ @places[place_id].add_tokens(1)
79
+ end
80
+
81
+ # Remove a token from a colored place
82
+ def remove_token_from_place(place_id, token = nil)
83
+ raise "Place #{place_id} not found" unless @places[place_id]
84
+
85
+ token_to_remove = token || @token_pools[place_id].first
86
+ @token_pools[place_id].delete(token_to_remove)
87
+ @places[place_id].remove_tokens(1)
88
+ token_to_remove
89
+ end
90
+
91
+ # Fire a colored transition
92
+ def fire_colored_transition(transition_id, token_bindings = {}, context = {})
93
+ transition = @transitions[transition_id]
94
+ raise "Transition #{transition_id} not found" unless transition
95
+
96
+ # Check guard with token data
97
+ guard_context = context.merge(tokens: token_bindings)
98
+ return false unless transition.enabled?(guard_context)
99
+
100
+ # Process input arcs (consume tokens with expressions)
101
+ input_tokens = {}
102
+ transition.input_arcs.each do |arc|
103
+ token = remove_token_from_place(arc.source.id, token_bindings[arc.source.id])
104
+ input_tokens[arc.source.id] = token
105
+ end
106
+
107
+ # Process output arcs (produce tokens with expressions)
108
+ transition.output_arcs.each do |arc|
109
+ # Get token data from corresponding input
110
+ input_token = input_tokens.values.first
111
+ output_data = if arc.expression
112
+ arc.expression.execute(input_token&.data || {}, context)
113
+ else
114
+ input_token&.data || {}
115
+ end
116
+
117
+ output_token = Core::Token.new(
118
+ color: @colored_places.dig(arc.target.id, :color) || :default,
119
+ data: output_data
120
+ )
121
+
122
+ add_token_to_place(arc.target.id, output_token)
123
+ end
124
+
125
+ true
126
+ end
127
+
128
+ # Get colored marking (with token data)
129
+ def colored_marking
130
+ marking = {}
131
+ @token_pools.each do |place_id, tokens|
132
+ marking[place_id] = tokens.map(&:to_h)
133
+ end
134
+ marking
135
+ end
136
+
137
+ def to_s
138
+ "ColoredNet(#{@name}, P=#{@places.size}, T=#{@transitions.size}, Colors=#{@colors.size})"
139
+ end
140
+
141
+ def inspect
142
+ "#<PetriFlow::Colored::ColoredNet name=#{@name} colors=#{@colors.keys} #{stats}>"
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Colored
5
+ # Represents a guard condition for colored Petri net transitions
6
+ # Guards determine if a transition can fire based on token data
7
+ class Guard
8
+ attr_reader :name, :condition
9
+
10
+ def initialize(name: nil, &condition)
11
+ @name = name
12
+ @condition = condition
13
+ end
14
+
15
+ # Evaluate the guard condition
16
+ # @param context [Hash] Context including token data, event data, etc.
17
+ # @return [Boolean] true if guard is satisfied
18
+ def satisfied?(context = {})
19
+ return true unless @condition
20
+
21
+ @condition.call(context)
22
+ end
23
+
24
+ # Combine guards with AND logic
25
+ def and(other_guard)
26
+ Guard.new(name: "#{@name} AND #{other_guard.name}") do |context|
27
+ satisfied?(context) && other_guard.satisfied?(context)
28
+ end
29
+ end
30
+
31
+ # Combine guards with OR logic
32
+ def or(other_guard)
33
+ Guard.new(name: "#{@name} OR #{other_guard.name}") do |context|
34
+ satisfied?(context) || other_guard.satisfied?(context)
35
+ end
36
+ end
37
+
38
+ # Negate guard
39
+ def not
40
+ Guard.new(name: "NOT #{@name}") do |context|
41
+ !satisfied?(context)
42
+ end
43
+ end
44
+
45
+ def to_s
46
+ "Guard(#{@name || 'anonymous'})"
47
+ end
48
+
49
+ def inspect
50
+ "#<PetriFlow::Colored::Guard name=#{@name}>"
51
+ end
52
+ end
53
+
54
+ # Common guard factories
55
+ module Guards
56
+ # Guard that checks if a field equals a value
57
+ def self.field_equals(field, value)
58
+ Guard.new(name: "#{field}==#{value}") do |context|
59
+ context.dig(:token, :data, field) == value
60
+ end
61
+ end
62
+
63
+ # Guard that checks if a field matches a pattern
64
+ def self.field_matches(field, pattern)
65
+ Guard.new(name: "#{field}=~/#{pattern}/") do |context|
66
+ context.dig(:token, :data, field)&.match?(pattern)
67
+ end
68
+ end
69
+
70
+ # Guard that checks if a condition is met
71
+ def self.condition(name, &block)
72
+ Guard.new(name: name, &block)
73
+ end
74
+
75
+ # Guard that always passes (true)
76
+ def self.always_true
77
+ Guard.new(name: "always_true") { |_| true }
78
+ end
79
+
80
+ # Guard that never passes (false)
81
+ def self.always_false
82
+ Guard.new(name: "always_false") { |_| false }
83
+ end
84
+
85
+ # Guard for privacy policy checks (PAM integration)
86
+ def self.has_consent(purpose)
87
+ Guard.new(name: "has_consent(#{purpose})") do |context|
88
+ user_id = context.dig(:token, :data, :user_id)
89
+ # In real implementation, check consent registry
90
+ context[:consent_granted]&.include?([user_id, purpose])
91
+ end
92
+ end
93
+
94
+ # Guard for retention policy checks
95
+ def self.within_retention(model_class)
96
+ Guard.new(name: "within_retention(#{model_class})") do |context|
97
+ timestamp = context.dig(:token, :timestamp)
98
+ retention_period = context.dig(:retention_policies, model_class) || 7.years
99
+ timestamp && (Time.current - timestamp) < retention_period
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Core
5
+ # Represents an arc in a Petri net
6
+ # Arcs connect places to transitions or transitions to places
7
+ class Arc
8
+ attr_reader :source, :target, :weight
9
+ attr_accessor :expression
10
+
11
+ # Create a new arc
12
+ # @param source [Place, Transition] The source node
13
+ # @param target [Transition, Place] The target node
14
+ # @param weight [Integer] Number of tokens consumed/produced (default: 1)
15
+ # @param expression [Proc] Arc expression for colored nets (optional)
16
+ def initialize(source:, target:, weight: 1, expression: nil)
17
+ validate_arc!(source, target)
18
+
19
+ @source = source
20
+ @target = target
21
+ @weight = weight
22
+ @expression = expression
23
+ end
24
+
25
+ # Check if this is an input arc (place -> transition)
26
+ def input_arc?
27
+ @source.is_a?(Place) && @target.is_a?(Transition)
28
+ end
29
+
30
+ # Check if this is an output arc (transition -> place)
31
+ def output_arc?
32
+ @source.is_a?(Transition) && @target.is_a?(Place)
33
+ end
34
+
35
+ # Execute arc expression (for colored Petri nets)
36
+ def execute_expression(token_data)
37
+ return token_data unless @expression
38
+
39
+ @expression.call(token_data)
40
+ end
41
+
42
+ def to_s
43
+ "Arc(#{@source.name} -> #{@target.name}, weight: #{@weight})"
44
+ end
45
+
46
+ def inspect
47
+ "#<PetriFlow::Core::Arc #{@source.class.name}(#{@source.name}) -> " \
48
+ "#{@target.class.name}(#{@target.name}) weight=#{@weight}>"
49
+ end
50
+
51
+ private
52
+
53
+ def validate_arc!(source, target)
54
+ valid = (source.is_a?(Place) && target.is_a?(Transition)) ||
55
+ (source.is_a?(Transition) && target.is_a?(Place))
56
+
57
+ raise ArcValidationError, "Arc must connect Place<->Transition or Transition<->Place" unless valid
58
+ end
59
+ end
60
+
61
+ class ArcValidationError < StandardError; end
62
+ end
63
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Core
5
+ # Represents a marking (state) of a Petri net
6
+ # A marking is a distribution of tokens across places
7
+ class Marking
8
+ attr_reader :tokens_by_place
9
+
10
+ def initialize(tokens_by_place = {})
11
+ @tokens_by_place = tokens_by_place.dup
12
+ end
13
+
14
+ # Get token count for a place
15
+ def tokens_at(place)
16
+ place_id = place.is_a?(Place) ? place.id : place
17
+ @tokens_by_place[place_id] || 0
18
+ end
19
+
20
+ # Set token count for a place
21
+ def set_tokens(place, count)
22
+ place_id = place.is_a?(Place) ? place.id : place
23
+ @tokens_by_place[place_id] = count
24
+ end
25
+
26
+ # Check if this marking is equal to another
27
+ def ==(other)
28
+ return false unless other.is_a?(Marking)
29
+
30
+ @tokens_by_place == other.tokens_by_place
31
+ end
32
+
33
+ alias_method :eql?, :==
34
+
35
+ def hash
36
+ @tokens_by_place.hash
37
+ end
38
+
39
+ # Create a copy of this marking
40
+ def dup
41
+ self.class.new(@tokens_by_place)
42
+ end
43
+
44
+ # Convert marking to array format for comparison
45
+ def to_a(places)
46
+ places.map { |place| tokens_at(place) }
47
+ end
48
+
49
+ # Convert to hash representation
50
+ def to_h
51
+ @tokens_by_place.dup
52
+ end
53
+
54
+ def to_s
55
+ place_strings = @tokens_by_place.map { |place_id, count| "#{place_id}:#{count}" }
56
+ "Marking(#{place_strings.join(', ')})"
57
+ end
58
+
59
+ def inspect
60
+ "#<PetriFlow::Core::Marking #{to_s}>"
61
+ end
62
+ end
63
+ end
64
+ end