wongi-engine 0.3.9 → 0.4.0.pre.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -2
  3. data/.gitignore +2 -0
  4. data/README.md +12 -12
  5. data/lib/wongi-engine/alpha_index.rb +58 -0
  6. data/lib/wongi-engine/alpha_memory.rb +2 -24
  7. data/lib/wongi-engine/beta/aggregate_node.rb +16 -15
  8. data/lib/wongi-engine/beta/assignment_node.rb +7 -2
  9. data/lib/wongi-engine/beta/beta_node.rb +27 -23
  10. data/lib/wongi-engine/beta/filter_node.rb +8 -13
  11. data/lib/wongi-engine/beta/join_node.rb +15 -28
  12. data/lib/wongi-engine/beta/ncc_node.rb +14 -26
  13. data/lib/wongi-engine/beta/ncc_partner.rb +18 -18
  14. data/lib/wongi-engine/beta/neg_node.rb +23 -55
  15. data/lib/wongi-engine/beta/optional_node.rb +24 -48
  16. data/lib/wongi-engine/beta/or_node.rb +24 -1
  17. data/lib/wongi-engine/beta/production_node.rb +9 -3
  18. data/lib/wongi-engine/beta/root_node.rb +47 -0
  19. data/lib/wongi-engine/beta.rb +1 -1
  20. data/lib/wongi-engine/compiler.rb +6 -34
  21. data/lib/wongi-engine/dsl/action/{base.rb → base_action.rb} +5 -1
  22. data/lib/wongi-engine/dsl/action/error_generator.rb +1 -1
  23. data/lib/wongi-engine/dsl/action/simple_action.rb +1 -1
  24. data/lib/wongi-engine/dsl/action/simple_collector.rb +1 -1
  25. data/lib/wongi-engine/dsl/action/statement_generator.rb +21 -22
  26. data/lib/wongi-engine/dsl/action/trace_action.rb +1 -1
  27. data/lib/wongi-engine/dsl/clause/fact.rb +2 -6
  28. data/lib/wongi-engine/dsl.rb +1 -25
  29. data/lib/wongi-engine/graph.rb +1 -1
  30. data/lib/wongi-engine/network/debug.rb +2 -10
  31. data/lib/wongi-engine/network.rb +44 -105
  32. data/lib/wongi-engine/overlay.rb +589 -0
  33. data/lib/wongi-engine/template.rb +22 -2
  34. data/lib/wongi-engine/token.rb +10 -26
  35. data/lib/wongi-engine/token_assignment.rb +15 -0
  36. data/lib/wongi-engine/version.rb +1 -1
  37. data/lib/wongi-engine/wme.rb +10 -39
  38. data/lib/wongi-engine.rb +3 -1
  39. data/spec/alpha_index_spec.rb +78 -0
  40. data/spec/bug_specs/issue_4_spec.rb +11 -11
  41. data/spec/high_level_spec.rb +8 -101
  42. data/spec/network_spec.rb +8 -6
  43. data/spec/overlay_spec.rb +161 -3
  44. data/spec/rule_specs/any_rule_spec.rb +39 -0
  45. data/spec/rule_specs/assign_spec.rb +1 -1
  46. data/spec/rule_specs/maybe_rule_spec.rb +58 -1
  47. data/spec/rule_specs/ncc_spec.rb +78 -19
  48. data/spec/rule_specs/negative_rule_spec.rb +12 -14
  49. data/spec/spec_helper.rb +4 -0
  50. data/spec/wme_spec.rb +0 -32
  51. metadata +11 -9
  52. data/lib/wongi-engine/beta/beta_memory.rb +0 -60
  53. data/lib/wongi-engine/data_overlay.rb +0 -149
  54. data/spec/rule_specs/or_rule_spec.rb +0 -40
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a8fbae534e8adfb43c9611f637b0cef5d0554205cf7285fbf2b95a3add8450b
4
- data.tar.gz: 51ebd685dee47ab62dcd35ce51a76c68d05945ff8ea1ee2ea35094f724e33747
3
+ metadata.gz: cf15effd33cba1faca7c6b2a6ce8b8a4730981a9b778fb5d41e11b4dce7803f8
4
+ data.tar.gz: 8d3cc0c99061e7097747b4339c55a83c11e79bb0edfbba4a0c95e4c850301595
5
5
  SHA512:
6
- metadata.gz: d0bb45dbd2297b67baeb9582606879fd376aaf48275d1f52759b3dfd561ce7053dea666515c5a5d0fb1cddc7394c832680e027790a43f11e3cac379d48435ece
7
- data.tar.gz: 7bd9f97dec2d971781c549a722ed110541b3dfa7859434570101fa122f9c56476cbd811029283b4aa958cf1cda25f3afcc4d01d4294dba2cc2fd33ac462cd10a
6
+ metadata.gz: 7140c583e23389df8f5c959b7ed31220782d72afcbd10f0883ba7314f24534656f1ca3fed33ed9d49a7962da8cbe7535f48bf160be88eef86d3798efe7ccece5
7
+ data.tar.gz: 3f0e9957b8497666a25c3ac933bcfe93495ed2c4cdac2f4eb27468c95401a107139f6a23840816241f94c0811edf69ab373818dd33def938d3ae9450505c68b5
@@ -11,10 +11,10 @@ jobs:
11
11
 
12
12
  strategy:
13
13
  matrix:
14
- ruby-version: ["3.1", "3.0", "2.7", "2.6", "jruby-head"]
14
+ ruby-version: ["3.1", "3.0", "2.7", "jruby-head"]
15
15
 
16
16
  steps:
17
- - uses: actions/checkout@v2
17
+ - uses: actions/checkout@v3
18
18
  - name: Set up Ruby ${{ matrix.ruby-version }}
19
19
  uses: ruby/setup-ruby@v1
20
20
  with:
data/.gitignore CHANGED
@@ -19,3 +19,5 @@ test/version_tmp
19
19
  tmp
20
20
  .ruby-version
21
21
  .idea
22
+ .hugo_build.lock
23
+ themes
data/README.md CHANGED
@@ -3,26 +3,26 @@
3
3
  [![Gem](https://img.shields.io/gem/v/wongi-engine.svg)](https://rubygems.org/gems/wongi-engine/)
4
4
  [![Build Status](https://github.com/ulfurinn/wongi-engine/actions/workflows/test.yml/badge.svg)](https://github.com/ulfurinn/wongi-engine/actions/workflows/test.yml)
5
5
 
6
+ This is a pure-Ruby forward-chaining rule engine based on the classic [Rete algorithm](http://en.wikipedia.org/wiki/Rete_algorithm).
7
+
6
8
  Ruby >= 2.7 and JRuby are supported. Rubinius should work but isn't actively supported.
7
9
 
8
- ## [Documentation](http://ulfurinn.github.io/wongi-engine/)
10
+ ## Documentation
11
+
12
+ There is no API documentation, as most of the library's interfaces are for internal use only and would not be safe to use directly.
13
+
14
+ Instead, follow the [tutorial](http://ulfurinn.github.io/wongi-engine/) and stick to the constructs described in it.
9
15
 
10
- This library contains a rule engine written in Ruby. It's based on the [Rete algorithm](http://en.wikipedia.org/wiki/Rete_algorithm) and uses a DSL to express rules in a readable way.
16
+ ## Upgrading
11
17
 
12
- **Word of caution**: this is complex and fragile machinery, and there may be subtle bugs that are only revealed with nontrivial usage. Be conservative with upgrades, test your rules extensively, and please report any behaviour that is not consistent with your expectations.
18
+ Until there is a 1.0 release, all minor versions should be treated as potentially breaking.
13
19
 
14
- [Feature annoucements](https://github.com/ulfurinn/wongi-engine/issues?q=is%3Aopen+is%3Aissue+label%3Aannoucement)
20
+ Always test your rules extensively. There's always a chance of you finding a bug in the engine that is only triggered by a very specific rule configuration.
21
+
22
+ [Feature annoucements](https://github.com/ulfurinn/wongi-engine/issues?q=is%3Aissue+label%3Aannoucement)
15
23
 
16
24
  [Open discussions](https://github.com/ulfurinn/wongi-engine/issues?q=is%3Aopen+is%3Aissue+label%3Adiscussion)
17
25
 
18
26
  ## Acknowledgements
19
27
 
20
28
  The Rete implementation in this library largely follows the outline presented in [\[Doorenbos, 1995\]](http://reports-archive.adm.cs.cmu.edu/anon/1995/CMU-CS-95-113.pdf).
21
-
22
- ## Contributing
23
-
24
- 1. Fork it
25
- 2. Create your feature branch (`git checkout -b my-new-feature`)
26
- 3. Commit your changes (`git commit -am 'Added some feature'`)
27
- 4. Push to the branch (`git push origin my-new-feature`)
28
- 5. Create new Pull Request
@@ -0,0 +1,58 @@
1
+ module Wongi::Engine
2
+ class AlphaIndex
3
+ attr_reader :pattern, :index
4
+ private :pattern
5
+ private :index
6
+
7
+ def initialize(pattern)
8
+ @pattern = pattern
9
+ @index = Hash.new { |h, k| h[k] = [] }
10
+ end
11
+
12
+ def add(wme)
13
+ collection_for_wme(wme).push(wme)
14
+ end
15
+
16
+ def remove(wme)
17
+ collection = collection_for_wme(wme)
18
+ collection.delete(wme)
19
+ if collection.empty?
20
+ # release some memory
21
+ index.delete(hashed_key(wme))
22
+ end
23
+ end
24
+
25
+ def collection_for_wme(wme)
26
+ index[hashed_key(wme)]
27
+ end
28
+
29
+ def collections_for_template(template)
30
+ return nil unless template_matches_pattern?(template)
31
+
32
+ # here we know that all fields on which we're indexing are concrete in the template
33
+ collection_for_wme(template)
34
+ end
35
+
36
+ private def template_matches_pattern?(template)
37
+ template_element_matches_pattern?(:subject, template.subject) &&
38
+ template_element_matches_pattern?(:predicate, template.predicate) &&
39
+ template_element_matches_pattern?(:object, template.object)
40
+ end
41
+
42
+ private def template_element_matches_pattern?(member, template_element)
43
+ if Template.concrete?(template_element)
44
+ pattern.include?(member)
45
+ else
46
+ !pattern.include?(member)
47
+ end
48
+ end
49
+
50
+ private def key(wme)
51
+ pattern.map { wme.public_send(_1) }
52
+ end
53
+
54
+ private def hashed_key(wme)
55
+ key(wme).map(&:hash)
56
+ end
57
+ end
58
+ end
@@ -2,16 +2,14 @@ module Wongi::Engine
2
2
  class AlphaMemory
3
3
  attr_reader :betas, :template, :rete
4
4
 
5
- def initialize(template, rete = nil)
5
+ def initialize(template, rete)
6
6
  @template = template
7
7
  @rete = rete
8
8
  @betas = []
9
- @wmes = []
10
9
  @frozen = false
11
10
  end
12
11
 
13
12
  def activate(wme)
14
- wme.overlay.add_wme(wme, self)
15
13
  # TODO: it used to activate before adding to the list. mandated by the original thesis. investigate. it appears to create duplicate tokens - needs a remedy in collecting nodes
16
14
  betas.each do |beta|
17
15
  beta.alpha_activate wme
@@ -19,18 +17,12 @@ module Wongi::Engine
19
17
  end
20
18
 
21
19
  def deactivate(wme)
22
- wme.overlay.remove_wme(wme, self)
20
+ # p deactivate: {wme:}
23
21
  betas.each do |beta|
24
22
  beta.alpha_deactivate wme
25
23
  end
26
24
  end
27
25
 
28
- def snapshot!(alpha)
29
- alpha.wmes.map(&:dup).each do |wme|
30
- activate wme
31
- end
32
- end
33
-
34
26
  def inspect
35
27
  "<Alpha #{__id__} template=#{template}>"
36
28
  end
@@ -38,19 +30,5 @@ module Wongi::Engine
38
30
  def to_s
39
31
  inspect
40
32
  end
41
-
42
- def size
43
- wmes.count
44
- end
45
-
46
- def wmes
47
- Enumerator.new do |y|
48
- rete.overlays.each do |overlay|
49
- overlay.raw_wmes(self).dup.each do |wme|
50
- y << wme
51
- end
52
- end
53
- end
54
- end
55
33
  end
56
34
  end
@@ -27,41 +27,42 @@ module Wongi::Engine
27
27
 
28
28
  def alpha_activate(wme)
29
29
  # we need to re-run all WMEs through the aggregator, so the new incoming one doesn't matter
30
- parent.tokens.each do |token|
31
- evaluate(wme, token)
30
+ tokens.each do |token|
31
+ evaluate(wme: wme, token: token)
32
32
  end
33
33
  end
34
34
 
35
35
  def alpha_deactivate(wme)
36
36
  # we need to re-run all WMEs through the aggregator, so the new incoming one doesn't matter
37
- parent.tokens.each do |token|
38
- evaluate(wme, token)
37
+ tokens.each do |token|
38
+ evaluate(wme: wme, token: token)
39
39
  end
40
40
  end
41
41
 
42
42
  def beta_activate(token)
43
- evaluate(nil, token)
43
+ return if tokens.find { |t| t.duplicate? token }
44
+
45
+ overlay.add_token(token)
46
+ evaluate(wme: nil, token: token)
44
47
  end
45
48
 
46
49
  def beta_deactivate(token)
47
- children.each do |child|
48
- child.tokens.each do |t|
49
- child.beta_deactivate t if t.parent == token
50
- end
51
- end
50
+ overlay.remove_token(token)
51
+ beta_deactivate_children(token: token)
52
52
  end
53
53
 
54
54
  def refresh_child(child)
55
- parent.tokens.each do |token|
56
- evaluate(nil, token, child)
55
+ tokens.each do |token|
56
+ evaluate(wme: nil, token: token, child: child)
57
57
  end
58
58
  end
59
59
 
60
- def evaluate(wme, token, child = nil)
60
+ def evaluate(wme:, token:, child: nil)
61
61
  # clean up previous decisions
62
- beta_deactivate(token)
62
+ # # TODO: optimise: only clean up if the value changed
63
+ beta_deactivate_children(token: token)
63
64
 
64
- candidates = alpha.wmes.select { |asserted_wme| matches?(token, asserted_wme) }
65
+ candidates = select_wmes(alpha.template) { |asserted_wme| matches?(token, asserted_wme) }
65
66
 
66
67
  return if candidates.empty?
67
68
 
@@ -7,12 +7,17 @@ module Wongi::Engine
7
7
  end
8
8
 
9
9
  def beta_activate(token, _wme = nil, _assignments = {})
10
+ return if tokens.find { |t| t.duplicate? token }
11
+
12
+ overlay.add_token(token)
10
13
  children.each do |child|
11
- child.beta_activate Token.new(child, token, nil, { @variable => @body.respond_to?(:call) ? @body.call(token) : @body })
14
+ value = @body.respond_to?(:call) ? @body.call(token) : @body
15
+ child.beta_activate Token.new(child, token, nil, { @variable => value })
12
16
  end
13
17
  end
14
18
 
15
19
  def beta_deactivate(token)
20
+ overlay.remove_token(token)
16
21
  children.each do |child|
17
22
  child.tokens.each do |t|
18
23
  if t.parent == token
@@ -24,7 +29,7 @@ module Wongi::Engine
24
29
  end
25
30
 
26
31
  def refresh_child(child)
27
- parent.tokens.each do |token|
32
+ tokens.each do |token|
28
33
  child.beta_activate Token.new(child, token, nil, { @variable => @body.respond_to?(:call) ? @body.call(token) : @body })
29
34
  end
30
35
  end
@@ -1,26 +1,5 @@
1
1
  module Wongi::Engine
2
2
  class BetaNode
3
- module TokenContainer
4
- def tokens
5
- Enumerator.new do |y|
6
- rete.overlays.each do |overlay|
7
- overlay.raw_tokens(self).dup.each do |token|
8
- y << token unless token.deleted?
9
- end
10
- overlay.raw_tokens(self).reject!(&:deleted?)
11
- end
12
- end
13
- end
14
-
15
- def empty?
16
- tokens.first.nil?
17
- end
18
-
19
- def size
20
- tokens.count
21
- end
22
- end
23
-
24
3
  include CoreExt
25
4
 
26
5
  attr_writer :rete
@@ -58,6 +37,7 @@ module Wongi::Engine
58
37
  abstract :beta_activate
59
38
  abstract :beta_deactivate
60
39
  abstract :beta_reactivate
40
+ abstract :refresh_child
61
41
 
62
42
  def assignment_node(variable, body)
63
43
  node = AssignmentNode.new self, variable, body
@@ -69,8 +49,32 @@ module Wongi::Engine
69
49
  parent.refresh_child self
70
50
  end
71
51
 
72
- def refresh_child(_node)
73
- raise "#{self.class} must implement refresh_child"
52
+ def beta_deactivate_children(token: nil, wme: nil, children: self.children)
53
+ children.each do |child|
54
+ child.tokens.select { (token.nil? || _1.parent == token) && (wme.nil? || _1.wme == wme) }.each do |child_token|
55
+ child.beta_deactivate(child_token)
56
+ end
57
+ end
58
+ end
59
+
60
+ private def select_wmes(template)
61
+ rete.current_overlay.select(template)
62
+ end
63
+
64
+ def tokens
65
+ overlay.node_tokens(self)
66
+ end
67
+
68
+ def overlay
69
+ rete.current_overlay
70
+ end
71
+
72
+ def empty?
73
+ tokens.first.nil?
74
+ end
75
+
76
+ def size
77
+ tokens.count
74
78
  end
75
79
 
76
80
  private
@@ -10,22 +10,20 @@ module Wongi
10
10
  end
11
11
 
12
12
  def beta_activate(token)
13
+ return if tokens.find { |t| t.duplicate? token }
14
+
13
15
  return unless test.passes?(token)
14
16
 
17
+ overlay.add_token(token)
18
+
15
19
  children.each do |child|
16
20
  child.beta_activate Token.new(child, token, nil, {})
17
21
  end
18
22
  end
19
23
 
20
24
  def beta_deactivate(token)
21
- children.each do |child|
22
- child.tokens.each do |t|
23
- if t.parent == token
24
- child.beta_deactivate t
25
- # token.destroy
26
- end
27
- end
28
- end
25
+ overlay.remove_token(token)
26
+ beta_deactivate_children(token: token)
29
27
  end
30
28
 
31
29
  def equivalent?(test)
@@ -33,12 +31,9 @@ module Wongi
33
31
  end
34
32
 
35
33
  def refresh_child(child)
36
- tmp = children
37
- self.children = [child]
38
- parent.tokens.each do |token|
39
- beta_activate token
34
+ tokens.select { test.passes?(_1) }.each do |token|
35
+ child.beta_activate Token.new(child, token, nil, {})
40
36
  end
41
- self.children = tmp
42
37
  end
43
38
  end
44
39
  end
@@ -1,19 +1,5 @@
1
1
  module Wongi
2
2
  module Engine
3
- TokenAssignment = Struct.new(:wme, :field) do
4
- def call(_token = nil)
5
- wme.send field
6
- end
7
-
8
- def inspect
9
- "#{field} of #{wme}"
10
- end
11
-
12
- def to_s
13
- inspect
14
- end
15
- end
16
-
17
3
  class BetaTest
18
4
  attr_reader :field, :variable
19
5
 
@@ -57,8 +43,10 @@ module Wongi
57
43
  end
58
44
 
59
45
  def alpha_activate(wme)
46
+ # p alpha_activate: {class: self.class, object_id:, wme:}
60
47
  assignments = collect_assignments(wme)
61
- parent.tokens.each do |token|
48
+
49
+ tokens.each do |token|
62
50
  next unless matches?(token, wme)
63
51
 
64
52
  children.each do |beta|
@@ -68,15 +56,16 @@ module Wongi
68
56
  end
69
57
 
70
58
  def alpha_deactivate(wme)
71
- children.each do |child|
72
- child.tokens.each do |token|
73
- child.beta_deactivate token if token.wme == wme
74
- end
75
- end
59
+ beta_deactivate_children(wme: wme)
76
60
  end
77
61
 
78
62
  def beta_activate(token)
79
- alpha.wmes.each do |wme|
63
+ # p beta_activate: {class: self.class, object_id:, token:}
64
+ return if tokens.find { |t| t.duplicate? token }
65
+
66
+ overlay.add_token(token)
67
+
68
+ select_wmes(alpha.template).each do |wme|
80
69
  next unless matches?(token, wme)
81
70
 
82
71
  assignments = collect_assignments(wme)
@@ -87,17 +76,15 @@ module Wongi
87
76
  end
88
77
 
89
78
  def beta_deactivate(token)
90
- children.each do |child|
91
- child.tokens.each do |t|
92
- child.beta_deactivate t if t.parent == token
93
- end
94
- end
79
+ # p beta_deactivate: {class: self.class, object_id:, token:}
80
+ overlay.remove_token(token)
81
+ beta_deactivate_children(token: token)
95
82
  end
96
83
 
97
84
  def refresh_child(child)
98
- alpha.wmes.each do |wme|
85
+ select_wmes(alpha.template).each do |wme|
99
86
  assignments = collect_assignments(wme)
100
- parent.tokens.each do |token|
87
+ tokens.each do |token|
101
88
  child.beta_activate Token.new(child, token, wme, assignments) if matches?(token, wme)
102
89
  end
103
90
  end
@@ -1,52 +1,40 @@
1
1
  module Wongi
2
2
  module Engine
3
3
  class NccNode < BetaNode
4
- include TokenContainer
5
-
6
4
  attr_accessor :partner
7
5
 
8
6
  def beta_activate(token)
9
- return if tokens.find { |t| t.parent == token }
7
+ # p beta_activate: {class: self.class, object_id:, token:}
8
+ return if tokens.find { |t| t.duplicate? token }
10
9
 
11
- t = Token.new self, token, nil, {}
12
- t.overlay.add_token(t, self)
10
+ overlay.add_token(token)
13
11
  partner.tokens.each do |ncc_token|
14
- next unless ncc_token.ancestors.find { |a| a.equal? token }
15
-
16
- t.ncc_results << ncc_token
17
- ncc_token.owner = t
12
+ if partner.owner_for(ncc_token) == token
13
+ overlay.add_ncc_token(token, ncc_token)
14
+ end
18
15
  end
19
- return unless t.ncc_results.empty?
16
+ return if overlay.ncc_tokens_for(token).any?
20
17
 
21
18
  children.each do |child|
22
- child.beta_activate Token.new(child, t, nil, {})
19
+ child.beta_activate Token.new(child, token, nil, {})
23
20
  end
24
21
  end
25
22
 
26
23
  def beta_deactivate(token)
27
- t = tokens.find { |tok| tok.parent == token }
28
- return unless t
29
-
30
- t.overlay.remove_token(t, self)
31
- t.deleted!
32
- partner.tokens.select { |ncc| ncc.owner == t }.each do |ncc_token|
33
- ncc_token.owner = nil
34
- t.ncc_results.delete(ncc_token)
35
- end
36
- children.each do |beta|
37
- beta.tokens.select { |child_token| child_token.parent == t }.each do |child_token|
38
- beta.beta_deactivate(child_token)
39
- end
40
- end
24
+ # p beta_deactivate: {class: self.class, object_id:, token:}
25
+ overlay.remove_token(token)
26
+ beta_deactivate_children(token: token)
41
27
  end
42
28
 
43
29
  def ncc_activate(token)
30
+ # p ncc_activate: {class: self.class, object_id:, token:}
44
31
  children.each do |child|
45
32
  child.beta_activate Token.new(child, token, nil, {})
46
33
  end
47
34
  end
48
35
 
49
36
  def ncc_deactivate(token)
37
+ # p ncc_deactivate: {class: self.class, object_id:, token:}
50
38
  children.each do |beta|
51
39
  beta.tokens.select { |t| t.parent == token }.each do |t|
52
40
  beta.beta_deactivate t
@@ -56,7 +44,7 @@ module Wongi
56
44
 
57
45
  def refresh_child(child)
58
46
  tokens.each do |token|
59
- child.beta_activate Token.new(child, token, nil, {}) if token.ncc_results.empty?
47
+ child.beta_activate Token.new(child, token, nil, {}) if overlay.ncc_tokens_for(token).empty?
60
48
  end
61
49
  end
62
50
  end
@@ -1,39 +1,39 @@
1
1
  module Wongi
2
2
  module Engine
3
3
  class NccPartner < BetaNode
4
- include TokenContainer
5
-
6
4
  attr_accessor :ncc, :divergent
7
5
 
8
6
  def beta_activate(token)
9
- t = Token.new self, token, nil, {}
10
- owner = owner_for(t)
11
- t.overlay.add_token(t, self)
7
+ # p beta_activate: {class: self.class, object_id:, token:}
8
+ return if tokens.find { |t| t.duplicate? token }
9
+
10
+ overlay.add_token(token)
11
+
12
+ owner = owner_for(token)
12
13
  return unless owner
13
14
 
14
- owner.ncc_results << t
15
- t.owner = owner
15
+ overlay.add_ncc_token(owner, token)
16
16
  owner.node.ncc_deactivate owner
17
17
  end
18
18
 
19
- def beta_deactivate(t)
20
- token = tokens.find { |tok| tok.parent == t }
21
- return unless token
19
+ def beta_deactivate(token)
20
+ # p beta_deactivate: {class: self.class, object_id:, token:}
22
21
 
23
- token.overlay.remove_token(token, self)
22
+ # fetch the owner before deleting the token
23
+ owner = overlay.ncc_owner(token)
24
24
 
25
- owner = token.owner
25
+ overlay.remove_token(token)
26
26
  return unless owner
27
27
 
28
- owner.ncc_results.delete token
29
- ncc.ncc_activate owner if owner.ncc_results.empty?
28
+ ncc.ncc_activate owner if overlay.ncc_tokens_for(owner).empty?
30
29
  end
31
30
 
32
- private
33
-
34
31
  def owner_for(token)
35
- divergent_token = token.ancestors.find { |t| t.node == divergent }
36
- ncc.tokens.find { |t| t.ancestors.include? divergent_token }
32
+ # find a token in the NCC node that has the same lineage as this token:
33
+ # - the NCC token will be a direct descendant of the divergent, therefore
34
+ # - one of this token's ancestors will be a duplicate of that token
35
+ # TODO: this should be more resilient, but child token creation does not allow for much else at the moment
36
+ ncc.tokens.find { |t| token.ancestors.any? { |ancestor| ancestor.duplicate?(t) } }
37
37
  end
38
38
  end
39
39
  end