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