orfeas_petri_flow 0.6.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6cf2817416167b49fdf8e5da2b764bb93b689ba5d5e3809a467e12c55ff3e3e6
4
- data.tar.gz: 438a3cd472f47a486a09500279c7a02124b4c851554a3caef76c612f5587efdf
3
+ metadata.gz: 72c3710503d8edda7e6e26f9272e57f3a8446f59df0f4abefa63e7153bce2c4d
4
+ data.tar.gz: dc3293f07ee62eeed5dfba86037f7eaa04be50e00a101b5aa0d88853e371eaff
5
5
  SHA512:
6
- metadata.gz: 6e92c2fb54ea6ff9916c7a4c7352a6cf3730cd86827c0b7bc7ed987549842354c4738cbdd1758c4218a7ac0eb0bf7e1939b91daf9d70b988ab2cca5000cb492f
7
- data.tar.gz: 69c8ae6183a931b338de55085caf6dbaaaeca62528ddfe467f8a7901ec2eb06658fad9f0fe868b9a2ad90942c3eecfbf845390586af2cc83a8baf573c5196af5
6
+ metadata.gz: 93f9fde29280050100fc099baa016a39ff29a8765eab84269503eaf7d7cd14c20953421cd6efd0555504d4ae126b57bfc815bd427a7963ff6af21f8072350822
7
+ data.tar.gz: 4fd31d7d6ee40d4c8065cc0638b19b528cc03ac0bd922e7b597c44579e2ea154c8c61c28eae3587104e677571f75ecf46f1ddcb3429fb4c34e4f109adec2ce57
data/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ All notable changes to the PetriFlow gem will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.0] - 2026-10-10
8
+
9
+ ### Added
10
+ - **Terminal-aware deadlock-freedom** — `PetriFlow.verify` and `Verification::LivenessChecker`
11
+ take `terminal_places:`. The liveness report adds `terminal_places`, `terminates_properly`
12
+ (no reachable dead marking outside the terminal places) and `improper_dead_markings` (the
13
+ count of those that remain). The raw `deadlock_free`, which counts the intended end of a net
14
+ as a deadlock, is unchanged. `Workflow#verify!` now passes the workflow's `terminal_places`.
15
+ First liveness tests for the gem.
16
+
17
+ ### Changed
18
+ - **Ruby 4.0 or later is required** (`required_ruby_version >= 4.0`; none was declared before).
19
+
20
+ ### Fixed
21
+ - **`PetriFlow.verify` left the net in the marking its checks ended in** — the liveness and
22
+ reachability checks fire transitions on the net they are given; `verify` now restores the
23
+ marking the net had when it was called.
24
+ - **Undeclared `rexml` dependency** — the PNML and CPN Tools exporters `require "rexml/document"`,
25
+ but the gemspec did not declare `rexml`, which has not been a default gem since Ruby 3.4.
26
+ Requiring `petri_flow` then failed with a `LoadError` part-way through, which Lyra rescued
27
+ as "PetriFlow not installed", leaving `PetriFlow` half-defined (no `PetriFlow::Workflow`);
28
+ any app that eager-loads Lyra's `app/workflows` then failed to boot. `rexml` is now a
29
+ runtime dependency.
30
+
31
+ ## [0.8.0] - 2026-08-09
32
+
33
+ No changes since 0.6.0 apart from the gem's name (`orfeas_petri_flow`); released with Lyra and
34
+ PAM DSL 0.8.0.
35
+
7
36
  ## [0.6.0] - 2026-01-05
8
37
 
9
38
  ### Added
data/README.md CHANGED
@@ -17,7 +17,6 @@ PetriFlow is a Ruby gem that provides a complete toolkit for modeling, analyzing
17
17
  - **Token Colors**: Typed tokens carrying structured data
18
18
  - **Guards**: Conditional transition firing (e.g., privacy policies)
19
19
  - **Arc Expressions**: Data transformations during transitions
20
- - **Hierarchical Nets**: Multi-level system modeling
21
20
 
22
21
  ### 📊 Matrix Analysis
23
22
  - **CRUD-Event Mapping Matrix**: Track which CRUD operations generate which events
@@ -29,7 +28,7 @@ PetriFlow is a Ruby gem that provides a complete toolkit for modeling, analyzing
29
28
  ### 🔍 Formal Verification
30
29
  - **Reachability Analysis**: What states are reachable?
31
30
  - **Boundedness Checking**: Are token counts bounded?
32
- - **Liveness Checking**: Can transitions fire? Deadlock detection
31
+ - **Liveness Checking**: Can transitions fire? Deadlock detection, optionally relative to designated terminal places
33
32
  - **Invariant Checking**: Custom property verification
34
33
 
35
34
  ### 🎬 Simulation
@@ -48,9 +47,12 @@ PetriFlow is a Ruby gem that provides a complete toolkit for modeling, analyzing
48
47
  Add to your Gemfile:
49
48
 
50
49
  ```ruby
51
- gem 'petri_flow', path: 'gems/petri_flow'
50
+ gem "orfeas_petri_flow", require: "petri_flow"
52
51
  ```
53
52
 
53
+ The gem is published as `orfeas_petri_flow`; the library is required as
54
+ `petri_flow`. It needs Ruby 4.0 or later.
55
+
54
56
  Then:
55
57
  ```bash
56
58
  bundle install
@@ -138,8 +140,8 @@ analyzer.analyze_events(events)
138
140
  # Get CRUD mapping
139
141
  puts analyzer.crud_mapping.to_table
140
142
 
141
- # Get data lineage
142
- lineage = analyzer.field_history("email")
143
+ # Get data lineage (fields are keyed as given in :changes, here symbols)
144
+ lineage = analyzer.field_history(:email)
143
145
  lineage.each do |mod|
144
146
  puts "#{mod[:timestamp]}: #{mod[:old_value]} → #{mod[:new_value]}"
145
147
  end
@@ -160,6 +162,13 @@ puts "Bounded: #{results[:boundedness][:is_bounded]}"
160
162
  puts "Safe: #{results[:boundedness][:is_safe]}"
161
163
  puts "Deadlock-free: #{results[:liveness][:deadlock_free]}"
162
164
 
165
+ # deadlock_free counts every reachable dead marking as a deadlock, including
166
+ # the intended end of the net. Pass terminal_places: to check deadlock-freedom
167
+ # except at markings that mark one of those places.
168
+ results = PetriFlow.verify(net, terminal_places: [:completed, :rejected])
169
+ puts "Terminates properly: #{results[:liveness][:terminates_properly]}"
170
+ puts "Improper dead markings: #{results[:liveness][:improper_dead_markings]}"
171
+
163
172
  # Custom invariants
164
173
  checker = PetriFlow::Verification::InvariantChecker.new(net)
165
174
 
@@ -222,6 +231,24 @@ ascii = PetriFlow.visualize(net, format: :ascii)
222
231
  puts ascii
223
232
  ```
224
233
 
234
+ ### Export
235
+
236
+ Nets (plain and colored) can be exported to PNML, CPN Tools XML, JSON and YAML:
237
+
238
+ ```ruby
239
+ pnml = PetriFlow.export(net, format: :pnml)
240
+ cpn = PetriFlow.export(net, format: :cpn) # CPN Tools XML
241
+ json = PetriFlow.export(net, format: :json) # pretty: true by default
242
+ yaml = PetriFlow.export(net, format: :yaml)
243
+
244
+ # Write to a file; the format is detected from the extension
245
+ # (.pnml, .cpn, .json, .yaml/.yml; .xml is PNML unless the name contains "cpn")
246
+ PetriFlow.save(net, "order.pnml")
247
+ PetriFlow.save(net, "order.xml", format: :cpn)
248
+ ```
249
+
250
+ See [docs/PETRIFLOW_EXPORT.md](docs/PETRIFLOW_EXPORT.md) and `examples/export_example.rb`.
251
+
225
252
  ## Rails Integration: Workflow DSL
226
253
 
227
254
  PetriFlow includes a declarative DSL for defining workflows in Rails applications,
@@ -276,15 +303,38 @@ rake workflows:verify
276
303
  rake workflows:list
277
304
  ```
278
305
 
306
+ ### Generating Workflows from State Machines
307
+
308
+ Models that use `aasm` or `state_machines-activerecord` can be turned into
309
+ workflow classes:
310
+
311
+ ```bash
312
+ # List models with a supported state machine
313
+ rake petri_flow:generate:scan
314
+
315
+ # Write app/workflows/order_workflow.rb from Order's state machine
316
+ # (state attribute defaults to :state), then verify it
317
+ rake petri_flow:generate:from_state_machine[Order,state]
318
+
319
+ # Aliases
320
+ rake workflows:scan
321
+ rake workflows:from_state_machine[Order,state]
322
+ ```
323
+
324
+ `Workflow#verify!` passes the workflow's `terminal_places` to `PetriFlow.verify`,
325
+ so its results include `liveness[:terminates_properly]`.
326
+
279
327
  ### Generated Reports
280
328
 
281
329
  Reports are saved to `reports/petri_flow_<timestamp>/`:
282
330
 
283
331
  - `verification_report.md` - Summary of all workflows
284
- - `<workflow_name>.md` - Individual workflow report
285
- - `<workflow_name>_petri.png` - Petri net diagram
286
- - `<workflow_name>_petri.dot` - GraphViz source
287
- - `<workflow_name>_petri.mmd` - Mermaid diagram
332
+ - `<workflow_id>.md` - Individual workflow report
333
+ - `<workflow_id>_petri.dot` - GraphViz source
334
+ - `<workflow_id>_petri.mmd` - Mermaid diagram
335
+ - `<workflow_id>_petri.png`, `.pdf`, `.svg` - Petri net diagram (only when GraphViz's `dot` is installed)
336
+
337
+ `<workflow_id>` is the underscored workflow name (`refund_request_workflow` for the example above).
288
338
 
289
339
  ### Configuration
290
340
 
@@ -337,15 +387,20 @@ class FlawedApprovalWorkflow < PetriFlow::Workflow
337
387
  end
338
388
  ```
339
389
 
340
- Running verification will detect this:
390
+ Running verification (`rake petri_flow:verify`) will detect this:
341
391
 
342
392
  ```
343
- Terminal State Reachability:
344
- completed ✓ REACHABLE
345
- rejected ✓ REACHABLE
346
- escalated ✗ UNREACHABLE
393
+ Terminal State Reachability:
394
+ completed ✓ REACHABLE
395
+ rejected ✓ REACHABLE
396
+ escalated ✗ UNREACHABLE
397
+ ```
398
+
399
+ and the workflow's report is marked `✗ UNREACHABLE STATES DETECTED`, with:
347
400
 
348
- > WARNING: Some terminal states are unreachable from the initial state.
401
+ ```
402
+ > **WARNING:** Some terminal states are unreachable from the initial state.
403
+ > This indicates a workflow design issue that should be investigated.
349
404
  ```
350
405
 
351
406
  This is valuable for GDPR compliance - proving that the `anonymized` state IS reachable
@@ -418,20 +473,25 @@ end
418
473
  ### 3. Event Flow Analysis
419
474
 
420
475
  ```ruby
421
- # Build causation matrix from event store
476
+ # Build causation matrix from events. Each event is a hash; causation is
477
+ # recorded between event ids, from :event_id and :caused_by_event_id.
422
478
  analyzer = PetriFlow.create_analyzer
423
479
 
424
- events = EventStore.read_all_events
480
+ events = [
481
+ { event_id: "order-created-1" },
482
+ { event_id: "invoice-issued-1", caused_by_event_id: "order-created-1" },
483
+ { event_id: "email-sent-1", caused_by_event_id: "invoice-issued-1" }
484
+ ]
425
485
  analyzer.analyze_events(events)
426
486
 
427
487
  # Find causation chain
428
- chain = analyzer.find_causation_chain("OrderCreated", "EmailSent")
488
+ chain = analyzer.find_causation_chain("order-created-1", "email-sent-1")
429
489
  puts "Causation chain: #{chain.join(' → ')}"
430
490
 
431
491
  # Get transitive causation
432
492
  closure = analyzer.causation.transitive_closure
433
- puts "Events caused by OrderCreated (transitively):"
434
- puts closure.effects_of("OrderCreated")
493
+ puts "Events caused by order-created-1 (transitively):"
494
+ puts closure.effects_of("order-created-1")
435
495
 
436
496
  # Identify most influential events
437
497
  centrality = analyzer.causation.centrality_scores
@@ -473,28 +533,30 @@ puts "Total modifications: #{impact[:total_modifications]}"
473
533
 
474
534
  ## Architecture Integration with Lyra
475
535
 
536
+ Lyra uses PetriFlow at verification time; PetriFlow enforces nothing while the
537
+ application runs. Lyra builds Petri nets of its own write paths and hands them
538
+ to PetriFlow for analysis:
539
+
476
540
  ```
477
- ┌─────────────────────────────────────────────────┐
478
- │ Lyra Framework │
479
- │ │
480
- │ ┌─────────────┐ ┌─────────────┐ │
481
- │ │ CRUD │────────▶│ Events │ │
482
- │ │ Operations │ │ Store │ │
483
- │ └─────────────┘ └──────┬──────┘ │
484
- │ │ │
485
- │ ▼ │
486
- │ ┌──────────────┐ │
487
- │ │ PetriFlow │ │
488
- │ │ Analysis │ │
489
- │ └──────────────┘ │
490
- │ │ │
491
- │ ┌─────────────────────┼──────────┐ │
492
- │ ▼ ▼ ▼ │
493
- │ ┌──────────┐ ┌──────────┐ ┌──────────┐
494
- │ │ CPN │ │ Matrix │ │ Verify │
495
- │ │ Model │ │ Analysis │ │ Props │
496
- │ └──────────┘ └──────────┘ └──────────┘
497
- └─────────────────────────────────────────────────┘
541
+ ┌──────────────────────────── Lyra ─────────────────────────────┐
542
+ │ │
543
+ │ CRUD lifecycle and Create/Update/Destroy mode nets │
544
+ │ (Lyra::Verification::CrudLifecycleWorkflow and friends) │
545
+ │ Bypass-write net (Lyra::Verification::BypassWorkflow) │
546
+ │ Per-mode nets (Lyra::Verification::WorkflowGenerator, │
547
+ │ app/workflows/*_workflow.rb) │
548
+ │ │
549
+ └───────────────────────────────┬───────────────────────────────┘
550
+ │ PetriFlow::Workflow nets
551
+ ▼
552
+ ┌────────────────────────────┐
553
+ │ PetriFlow │
554
+ │ reachability, boundedness, │
555
+ │ liveness, invariants, │
556
+ │ simulation, export │
557
+ └──────────────┬─────────────┘
558
+ ▼
559
+ Lyra.verify_mapping!, rake lyra:workflows:verify, rake petri_flow:verify
498
560
  ```
499
561
 
500
562
  ## Research Foundation
@@ -552,14 +614,24 @@ It provides the formal verification foundation for the CRUD-to-Event mapping fra
552
614
  - `PetriFlow::Visualization::Graphviz` - DOT/GraphViz output
553
615
  - `PetriFlow::Visualization::Mermaid` - Mermaid diagrams
554
616
 
617
+ ### Export
618
+
619
+ - `PetriFlow::Export` - `export` / `save` dispatch by format
620
+ - `PetriFlow::Export::PnmlExporter`, `CpnToolsExporter`, `JsonExporter`, `YamlExporter`
621
+
622
+ ### Workflows
623
+
624
+ - `PetriFlow::Workflow` - Declarative workflow DSL
625
+ - `PetriFlow::Generators::WorkflowGenerator` - Workflow classes from AASM / state_machines
626
+
555
627
  ## Development
556
628
 
557
629
  ```bash
558
630
  # Install dependencies
559
631
  bundle install
560
632
 
561
- # Run tests
562
- bundle exec rspec
633
+ # Run tests (Minitest)
634
+ bundle exec rake test
563
635
 
564
636
  # Run examples
565
637
  ruby examples/crud_mapping_example.rb
@@ -587,6 +659,6 @@ MIT License. See MIT-LICENSE file.
587
659
  author={Pantelelis, Michail},
588
660
  year={2026},
589
661
  note={Part of ORFEAS PhD Thesis},
590
- url={https://github.com/mpantel/lyra-engine/gems/petri-flow}
662
+ url={https://github.com/mpantel/lyra-engine/tree/main/gems/petri_flow}
591
663
  }
592
664
  ```
@@ -14,10 +14,13 @@ module PetriFlow
14
14
  # L3 (Potentially firable infinitely): Can fire infinitely
15
15
  # L4 (Live): Can always eventually fire
16
16
 
17
- def initialize(net, reachability_analyzer = nil)
17
+ # terminal_places: places whose marking means the net finished rather
18
+ # than got stuck. See #terminates_properly?.
19
+ def initialize(net, reachability_analyzer = nil, terminal_places: [])
18
20
  @net = net
19
21
  @reachability_analyzer = reachability_analyzer ||
20
22
  ReachabilityAnalyzer.new(net)
23
+ @terminal_places = Array(terminal_places).map(&:to_sym)
21
24
  end
22
25
 
23
26
  # Check if transition is dead (L0)
@@ -51,6 +54,23 @@ module PetriFlow
51
54
  end
52
55
  end
53
56
 
57
+ # Deadlock-freedom relative to designated terminal places: a reachable
58
+ # marking in which no transition is enabled is a deadlock unless it
59
+ # marks at least one terminal place. deadlock_free? counts every dead
60
+ # marking, so it is false for any net that is meant to finish; with no
61
+ # terminal places the two agree.
62
+ def terminates_properly?
63
+ improper_dead_markings.empty?
64
+ end
65
+
66
+ # Reachable dead markings that mark no terminal place.
67
+ def improper_dead_markings
68
+ @reachability_analyzer.reachable_markings.select do |marking|
69
+ @net.set_marking(marking)
70
+ @net.deadlocked? && @terminal_places.none? { |place| marking.tokens_at(place).positive? }
71
+ end
72
+ end
73
+
54
74
  # Find dead transitions
55
75
  def dead_transitions
56
76
  @net.transitions.keys.select { |tid| dead?(tid) }
@@ -86,6 +106,9 @@ module PetriFlow
86
106
 
87
107
  {
88
108
  deadlock_free: deadlock_free?,
109
+ terminal_places: @terminal_places,
110
+ terminates_properly: terminates_properly?,
111
+ improper_dead_markings: improper_dead_markings.size,
89
112
  dead_transitions: classification.select { |_, v| v == :dead }.keys,
90
113
  live_transitions: classification.select { |_, v| v == :live }.keys,
91
114
  potentially_firable: classification.select { |_, v| v == :potentially_firable }.keys,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PetriFlow
4
- VERSION = "0.6.0"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -143,7 +143,7 @@ module PetriFlow
143
143
  # @return [Hash] Verification results
144
144
  def verify!
145
145
  reset_to_initial!
146
- @verification_results = PetriFlow.verify(@net)
146
+ @verification_results = PetriFlow.verify(@net, terminal_places: self.class.defined_terminal_places || [])
147
147
  verify_terminal_reachability
148
148
  @verification_results
149
149
  end
data/lib/petri_flow.rb CHANGED
@@ -106,20 +106,28 @@ module PetriFlow
106
106
  # Use true for full state-space exploration of colored nets.
107
107
  # Guards are treated as non-deterministic choice, which is sound
108
108
  # for structural properties (boundedness, liveness, reachability).
109
- def verify(net, initial_marking: nil, pt_abstraction: false)
110
- initial_marking ||= net.current_marking
109
+ # @param terminal_places [Array<Symbol>] Places whose marking means the
110
+ # net has finished; liveness[:terminates_properly] is deadlock-freedom
111
+ # except at markings that mark one of them.
112
+ # The analysis fires transitions to explore the state space; the net's
113
+ # marking is restored afterwards, so verifying a net does not change it.
114
+ def verify(net, initial_marking: nil, pt_abstraction: false, terminal_places: [])
115
+ marking_before = net.current_marking
116
+ initial_marking ||= marking_before
111
117
 
112
118
  reachability = Verification::ReachabilityAnalyzer.new(net, initial_marking, pt_abstraction: pt_abstraction)
113
119
  reachability.analyze
114
120
 
115
121
  boundedness = Verification::BoundednessChecker.new(net, reachability)
116
- liveness = Verification::LivenessChecker.new(net, reachability)
122
+ liveness = Verification::LivenessChecker.new(net, reachability, terminal_places: terminal_places)
117
123
 
118
124
  {
119
125
  reachability: reachability.report,
120
126
  boundedness: boundedness.report,
121
127
  liveness: liveness.report
122
128
  }
129
+ ensure
130
+ net.set_marking(marking_before) if marking_before
123
131
  end
124
132
 
125
133
  # Quick simulation
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: orfeas_petri_flow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michail Pantelelis
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.4'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rexml
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.2'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: minitest
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -147,13 +161,13 @@ files:
147
161
  - lib/petri_flow/visualization/graphviz.rb
148
162
  - lib/petri_flow/visualization/mermaid.rb
149
163
  - lib/petri_flow/workflow.rb
150
- homepage: https://github.com/mpantel/lyra-engine/gems/petri-flow
164
+ homepage: https://github.com/mpantel/lyra-engine/tree/main/gems/petri_flow
151
165
  licenses:
152
166
  - MIT
153
167
  metadata:
154
- homepage_uri: https://github.com/mpantel/lyra-engine/gems/petri-flow
155
- source_code_uri: https://github.com/mpantel/lyra-engine/gems/petri-flow/tree/main/gems/petri_flow
156
- changelog_uri: https://github.com/mpantel/lyra-engine/gems/petri-flow/blob/main/gems/petri_flow/CHANGELOG.md
168
+ homepage_uri: https://github.com/mpantel/lyra-engine/tree/main/gems/petri_flow
169
+ source_code_uri: https://github.com/mpantel/lyra-engine/tree/main/gems/petri_flow
170
+ changelog_uri: https://github.com/mpantel/lyra-engine/blob/main/gems/petri_flow/CHANGELOG.md
157
171
  rdoc_options: []
158
172
  require_paths:
159
173
  - lib
@@ -161,14 +175,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
161
175
  requirements:
162
176
  - - ">="
163
177
  - !ruby/object:Gem::Version
164
- version: '0'
178
+ version: '4.0'
165
179
  required_rubygems_version: !ruby/object:Gem::Requirement
166
180
  requirements:
167
181
  - - ">="
168
182
  - !ruby/object:Gem::Version
169
183
  version: '0'
170
184
  requirements: []
171
- rubygems_version: 4.0.3
185
+ rubygems_version: 4.0.20
172
186
  specification_version: 4
173
187
  summary: Petri Net and Matrix Analysis for ORFEAS Framework
174
188
  test_files: []