lex-react 0.1.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 +10 -0
- data/README.md +29 -0
- data/lib/legion/extensions/react/actors/event_subscriber.rb +16 -0
- data/lib/legion/extensions/react/helpers/constants.rb +24 -0
- data/lib/legion/extensions/react/helpers/event_matcher.rb +46 -0
- data/lib/legion/extensions/react/helpers/loop_breaker.rb +66 -0
- data/lib/legion/extensions/react/reaction_dispatcher.rb +64 -0
- data/lib/legion/extensions/react/rule_engine.rb +50 -0
- data/lib/legion/extensions/react/runners/react.rb +80 -0
- data/lib/legion/extensions/react/version.rb +9 -0
- data/lib/legion/extensions/react.rb +44 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 77b98f390c59e0768be5c629e704e3d5920297695a78fa114481a8fe5b89e9ac
|
|
4
|
+
data.tar.gz: b48f8e5959e929359661b2e4fe206ca65fa0d1e189a1c3628a403bfa142cfcf3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8608c662d7654352d1cd4222307ef6749f2ad916ca0a92a20ead94ad8cc28650e3f8f4a8e13a3c3f4f105b4d387afb4e087ef88cad8eecb5150bb2097d8a4e3c
|
|
7
|
+
data.tar.gz: c9b7db15123922391729970bd08d38d1208514115369b5f4ebc170e2f78d4576b5c3d52cf9985836804055c4ab858ef79d483395fac23ef344ccc0b50d14216f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Initial release: rule engine, event matcher, reaction dispatcher, loop breaker
|
|
7
|
+
- Wildcard event subscription via Legion::Events
|
|
8
|
+
- YAML-based configurable reaction rules via Legion::Settings
|
|
9
|
+
- Synapse autonomy level gating (OBSERVE/FILTER/TRANSFORM/AUTONOMOUS)
|
|
10
|
+
- Loop prevention with configurable depth limit and cooldown window
|
data/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# lex-react
|
|
2
|
+
|
|
3
|
+
Reaction engine for LegionIO. Subscribes to `Legion::Events` and fires configurable reaction chains in response to events, with Synapse autonomy gating and loop prevention.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
react:
|
|
9
|
+
rules:
|
|
10
|
+
ci_failure:
|
|
11
|
+
enabled: true
|
|
12
|
+
source: "github.check_run.completed"
|
|
13
|
+
condition: "conclusion == 'failure'"
|
|
14
|
+
autonomy: FILTER
|
|
15
|
+
chain:
|
|
16
|
+
- lex-github.runners.fetch_check_logs
|
|
17
|
+
- lex-transformer.runners.analyze
|
|
18
|
+
max_depth: 3
|
|
19
|
+
cooldown_seconds: 60
|
|
20
|
+
max_reactions_per_hour: 100
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
lex-react auto-subscribes to `Legion::Events` on extension load. Rules are evaluated against every event. Matching rules dispatch task chains respecting Synapse autonomy levels.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module React
|
|
6
|
+
module Actor
|
|
7
|
+
class EventSubscriber < Legion::Extensions::Actors::Once
|
|
8
|
+
def runner_class = 'Legion::Extensions::React'
|
|
9
|
+
def runner_function = 'subscribe!'
|
|
10
|
+
def check_subtask? = false
|
|
11
|
+
def generate_task? = false
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module React
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
AUTONOMY_LEVELS = %i[observe filter transform autonomous].freeze
|
|
9
|
+
|
|
10
|
+
AUTONOMY_THRESHOLDS = {
|
|
11
|
+
observe: 0.0,
|
|
12
|
+
filter: 0.3,
|
|
13
|
+
transform: 0.6,
|
|
14
|
+
autonomous: 0.8
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
DEFAULT_MAX_DEPTH = 3
|
|
18
|
+
DEFAULT_COOLDOWN_SECONDS = 60
|
|
19
|
+
DEFAULT_MAX_REACTIONS_PER_HOUR = 100
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module React
|
|
6
|
+
module Helpers
|
|
7
|
+
module EventMatcher
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def match?(pattern, event_name)
|
|
11
|
+
# Use placeholder so '.**' double-star replacement isn't re-processed by single '*' gsub
|
|
12
|
+
regex_str = pattern.gsub('.**', '__DS__')
|
|
13
|
+
.gsub('*', '[^.]*')
|
|
14
|
+
.gsub('__DS__', '\..*')
|
|
15
|
+
regex_str = "\\A#{regex_str}\\z"
|
|
16
|
+
Regexp.new(regex_str).match?(event_name)
|
|
17
|
+
rescue RegexpError
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def evaluate_condition(condition, event)
|
|
22
|
+
return true if condition.nil? || condition.strip.empty?
|
|
23
|
+
|
|
24
|
+
# Parse simple conditions: "key == 'value'" or "key != 'value'"
|
|
25
|
+
if condition =~ /\A(\w+)\s*(==|!=)\s*'([^']*)'\z/
|
|
26
|
+
key = Regexp.last_match(1).to_sym
|
|
27
|
+
op = Regexp.last_match(2)
|
|
28
|
+
value = Regexp.last_match(3)
|
|
29
|
+
actual = event[key]&.to_s
|
|
30
|
+
|
|
31
|
+
case op
|
|
32
|
+
when '==' then actual == value
|
|
33
|
+
when '!=' then actual != value
|
|
34
|
+
else false
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
rescue StandardError
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module React
|
|
6
|
+
module Helpers
|
|
7
|
+
class LoopBreaker
|
|
8
|
+
def initialize(max_depth: Constants::DEFAULT_MAX_DEPTH,
|
|
9
|
+
cooldown_seconds: Constants::DEFAULT_COOLDOWN_SECONDS,
|
|
10
|
+
max_per_hour: Constants::DEFAULT_MAX_REACTIONS_PER_HOUR)
|
|
11
|
+
@max_depth = max_depth
|
|
12
|
+
@cooldown_seconds = cooldown_seconds
|
|
13
|
+
@max_per_hour = max_per_hour
|
|
14
|
+
@recent = [] # Array of { rule_id:, event_id:, at: }
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def allow?(rule_id:, depth:, event_id: nil)
|
|
19
|
+
return false if depth > @max_depth
|
|
20
|
+
|
|
21
|
+
@mutex.synchronize do
|
|
22
|
+
prune_old_entries
|
|
23
|
+
return false if @recent.size >= @max_per_hour
|
|
24
|
+
|
|
25
|
+
if event_id && @cooldown_seconds.positive?
|
|
26
|
+
duplicate = @recent.any? do |r|
|
|
27
|
+
r[:rule_id] == rule_id && r[:event_id] == event_id
|
|
28
|
+
end
|
|
29
|
+
return false if duplicate
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def record(rule_id:, event_id:)
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
@recent << { rule_id: rule_id, event_id: event_id, at: Time.now }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reactions_this_hour
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
prune_old_entries
|
|
45
|
+
@recent.size
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stats
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
prune_old_entries
|
|
52
|
+
{ reactions_this_hour: @recent.size, max_per_hour: @max_per_hour }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def prune_old_entries
|
|
59
|
+
cutoff = Time.now - 3600
|
|
60
|
+
@recent.reject! { |r| r[:at] < cutoff }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module React
|
|
6
|
+
class ReactionDispatcher
|
|
7
|
+
def initialize(loop_breaker:)
|
|
8
|
+
@loop_breaker = loop_breaker
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def dispatch(rule:, event:, depth: 0)
|
|
12
|
+
event_id = event[:event].to_s
|
|
13
|
+
|
|
14
|
+
unless @loop_breaker.allow?(rule_id: rule[:id].to_s, depth: depth, event_id: event_id)
|
|
15
|
+
log_blocked(rule, event)
|
|
16
|
+
return { success: false, rule_id: rule[:id], reason: :loop_blocked }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if rule[:autonomy] == :observe
|
|
20
|
+
log_observed(rule, event)
|
|
21
|
+
return { success: true, rule_id: rule[:id], action: :observed, chain: rule[:chain] }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
execute_chain(rule, event)
|
|
25
|
+
@loop_breaker.record(rule_id: rule[:id].to_s, event_id: event_id)
|
|
26
|
+
|
|
27
|
+
{ success: true, rule_id: rule[:id], action: :dispatched, chain: rule[:chain] }
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
{ success: false, rule_id: rule[:id], error: e.message }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def execute_chain(rule, event)
|
|
35
|
+
rule[:chain].each do |runner_ref|
|
|
36
|
+
dispatch_runner(runner_ref, event, rule)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def dispatch_runner(runner_ref, event, rule)
|
|
41
|
+
Legion::Events.emit('react.dispatched',
|
|
42
|
+
rule_id: rule[:id],
|
|
43
|
+
runner_ref: runner_ref,
|
|
44
|
+
source_event: event[:event],
|
|
45
|
+
autonomy: rule[:autonomy])
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
Legion::Logging.warn "[React] dispatch error: #{e.message}" if defined?(Legion::Logging)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def log_observed(rule, event)
|
|
51
|
+
return unless defined?(Legion::Logging)
|
|
52
|
+
|
|
53
|
+
Legion::Logging.info "[React] OBSERVE rule=#{rule[:id]} event=#{event[:event]} chain=#{rule[:chain]}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def log_blocked(rule, event)
|
|
57
|
+
return unless defined?(Legion::Logging)
|
|
58
|
+
|
|
59
|
+
Legion::Logging.warn "[React] BLOCKED rule=#{rule[:id]} event=#{event[:event]} (loop prevention)"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module React
|
|
6
|
+
class RuleEngine
|
|
7
|
+
attr_reader :rules
|
|
8
|
+
|
|
9
|
+
def initialize(rules_hash = {})
|
|
10
|
+
@rules = parse_rules(rules_hash)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.from_settings
|
|
14
|
+
rules_hash = if defined?(Legion::Settings) && !Legion::Settings[:react].nil?
|
|
15
|
+
Legion::Settings.dig(:react, :rules) || {}
|
|
16
|
+
else
|
|
17
|
+
{}
|
|
18
|
+
end
|
|
19
|
+
new(rules_hash)
|
|
20
|
+
rescue StandardError
|
|
21
|
+
new({})
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def match(event)
|
|
25
|
+
event_name = event[:event].to_s
|
|
26
|
+
@rules.select do |rule|
|
|
27
|
+
Helpers::EventMatcher.match?(rule[:source], event_name) &&
|
|
28
|
+
Helpers::EventMatcher.evaluate_condition(rule[:condition], event)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def parse_rules(rules_hash)
|
|
35
|
+
rules_hash.filter_map do |id, config|
|
|
36
|
+
next unless config.is_a?(Hash) && config[:enabled] != false
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
id: id.to_sym,
|
|
40
|
+
source: config[:source].to_s,
|
|
41
|
+
condition: config[:condition],
|
|
42
|
+
autonomy: (config[:autonomy] || 'observe').to_s.downcase.to_sym,
|
|
43
|
+
chain: Array(config[:chain])
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module React
|
|
6
|
+
module Runners
|
|
7
|
+
module React
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def handle_event(event:)
|
|
11
|
+
engine = rule_engine
|
|
12
|
+
matches = engine.match(event)
|
|
13
|
+
dispatcher = reaction_dispatcher
|
|
14
|
+
|
|
15
|
+
results = matches.map do |rule|
|
|
16
|
+
dispatcher.dispatch(rule: rule, event: event, depth: event[:react_depth] || 0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
success: true,
|
|
21
|
+
matched_rules: matches.size,
|
|
22
|
+
results: results
|
|
23
|
+
}
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
{ success: false, error: e.message }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def react_stats
|
|
29
|
+
engine = rule_engine
|
|
30
|
+
breaker_stats = loop_breaker.stats
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
success: true,
|
|
34
|
+
rule_count: engine.rules.size,
|
|
35
|
+
reactions_this_hour: breaker_stats[:reactions_this_hour],
|
|
36
|
+
max_per_hour: breaker_stats[:max_per_hour]
|
|
37
|
+
}
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
{ success: false, error: e.message }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset!
|
|
43
|
+
@rule_engine = nil
|
|
44
|
+
@loop_breaker = nil
|
|
45
|
+
@reaction_dispatcher = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def rule_engine
|
|
49
|
+
@rule_engine ||= RuleEngine.from_settings
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def loop_breaker
|
|
53
|
+
@loop_breaker ||= begin
|
|
54
|
+
settings = react_settings
|
|
55
|
+
Helpers::LoopBreaker.new(
|
|
56
|
+
max_depth: settings[:max_depth] || Helpers::Constants::DEFAULT_MAX_DEPTH,
|
|
57
|
+
cooldown_seconds: settings[:cooldown_seconds] || Helpers::Constants::DEFAULT_COOLDOWN_SECONDS,
|
|
58
|
+
max_per_hour: settings[:max_reactions_per_hour] || Helpers::Constants::DEFAULT_MAX_REACTIONS_PER_HOUR
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reaction_dispatcher
|
|
64
|
+
@reaction_dispatcher ||= ReactionDispatcher.new(loop_breaker: loop_breaker)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def react_settings
|
|
68
|
+
return {} unless defined?(Legion::Settings) && !Legion::Settings[:react].nil?
|
|
69
|
+
|
|
70
|
+
Legion::Settings[:react] || {}
|
|
71
|
+
rescue StandardError
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private_class_method :rule_engine, :loop_breaker, :reaction_dispatcher, :react_settings
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/react/version'
|
|
4
|
+
require_relative 'react/helpers/constants'
|
|
5
|
+
require_relative 'react/helpers/event_matcher'
|
|
6
|
+
require_relative 'react/helpers/loop_breaker'
|
|
7
|
+
require_relative 'react/rule_engine'
|
|
8
|
+
require_relative 'react/reaction_dispatcher'
|
|
9
|
+
require_relative 'react/runners/react'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module React
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined?(:Core)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def data_required?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def subscribe!
|
|
22
|
+
return unless defined?(Legion::Events)
|
|
23
|
+
|
|
24
|
+
@subscription ||= Legion::Events.on('*') do |event|
|
|
25
|
+
next if event[:event].to_s.start_with?('react.')
|
|
26
|
+
|
|
27
|
+
Runners::React.handle_event(event: event)
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
Legion::Logging.warn "[React] event handler error: #{e.message}" if defined?(Legion::Logging)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def unsubscribe!
|
|
34
|
+
return unless @subscription && defined?(Legion::Events)
|
|
35
|
+
|
|
36
|
+
Legion::Events.off('*', @subscription)
|
|
37
|
+
@subscription = nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
require_relative 'react/actors/event_subscriber' if defined?(Legion::Extensions::Actors::Once)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-react
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Event-driven automation rules that subscribe to Legion::Events and fire
|
|
13
|
+
configurable reaction chains
|
|
14
|
+
email:
|
|
15
|
+
- legionio@esity.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/legion/extensions/react.rb
|
|
23
|
+
- lib/legion/extensions/react/actors/event_subscriber.rb
|
|
24
|
+
- lib/legion/extensions/react/helpers/constants.rb
|
|
25
|
+
- lib/legion/extensions/react/helpers/event_matcher.rb
|
|
26
|
+
- lib/legion/extensions/react/helpers/loop_breaker.rb
|
|
27
|
+
- lib/legion/extensions/react/reaction_dispatcher.rb
|
|
28
|
+
- lib/legion/extensions/react/rule_engine.rb
|
|
29
|
+
- lib/legion/extensions/react/runners/react.rb
|
|
30
|
+
- lib/legion/extensions/react/version.rb
|
|
31
|
+
homepage: https://github.com/LegionIO/lex-react
|
|
32
|
+
licenses:
|
|
33
|
+
- MIT
|
|
34
|
+
metadata:
|
|
35
|
+
rubygems_mfa_required: 'true'
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '3.4'
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 3.6.9
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: Reaction engine for LegionIO
|
|
53
|
+
test_files: []
|