lex-habit 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/lib/legion/extensions/habit/client.rb +22 -0
- data/lib/legion/extensions/habit/helpers/action_sequence.rb +116 -0
- data/lib/legion/extensions/habit/helpers/constants.rb +40 -0
- data/lib/legion/extensions/habit/helpers/habit_store.rb +144 -0
- data/lib/legion/extensions/habit/runners/habit.rb +82 -0
- data/lib/legion/extensions/habit/version.rb +9 -0
- data/lib/legion/extensions/habit.rb +16 -0
- metadata +68 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d0a865cd888c03af9bc77d51dc7ea7721fa49413288f568d8dd5e765a8dc0d2e
|
|
4
|
+
data.tar.gz: 24761157f9e83cfec300b8b0dccba1a1d92e50476793be7802ced4710d71e950
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f8f2f8170d0c98b3ae47937ddc8630c12ed373bd43bf15e2e3e5754c01ce23bb4b42c3ceaec8394fcf6beb58c8608d0ad14e65a5ac01a7097668c342d4b2e6b1
|
|
7
|
+
data.tar.gz: 6e49a7a3686f54b7b24d36d501fb4455b53f6d3c2efdacf5b7244f371c701967bc072b83f296b9ac96de895e929244d227c37662232542bb8c9f116709899ca6
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/habit/helpers/constants'
|
|
4
|
+
require 'legion/extensions/habit/helpers/action_sequence'
|
|
5
|
+
require 'legion/extensions/habit/helpers/habit_store'
|
|
6
|
+
require 'legion/extensions/habit/runners/habit'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module Habit
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::Habit
|
|
13
|
+
|
|
14
|
+
attr_reader :habit_store
|
|
15
|
+
|
|
16
|
+
def initialize(habit_store: nil, **)
|
|
17
|
+
@habit_store = habit_store || Helpers::HabitStore.new
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module Habit
|
|
8
|
+
module Helpers
|
|
9
|
+
class ActionSequence
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :actions, :context, :execution_count, :success_count,
|
|
13
|
+
:strength, :maturity, :last_executed, :created_at
|
|
14
|
+
|
|
15
|
+
def initialize(actions:, context: {})
|
|
16
|
+
@id = SecureRandom.uuid
|
|
17
|
+
@actions = actions.map(&:to_sym)
|
|
18
|
+
@context = context
|
|
19
|
+
@execution_count = 0
|
|
20
|
+
@success_count = 0
|
|
21
|
+
@strength = 0.3
|
|
22
|
+
@maturity = :novel
|
|
23
|
+
@last_executed = nil
|
|
24
|
+
@created_at = Time.now.utc
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def record_execution(success:)
|
|
28
|
+
@execution_count += 1
|
|
29
|
+
@success_count += 1 if success
|
|
30
|
+
@last_executed = Time.now.utc
|
|
31
|
+
|
|
32
|
+
@strength = if success
|
|
33
|
+
[@strength + Constants::REINFORCEMENT_RATE, 1.0].min
|
|
34
|
+
else
|
|
35
|
+
[@strength - Constants::REINFORCEMENT_RATE, 0.0].max
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
update_maturity
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cognitive_cost
|
|
42
|
+
Constants::COGNITIVE_COST[@maturity]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def success_rate
|
|
46
|
+
return 0.0 if @execution_count.zero?
|
|
47
|
+
|
|
48
|
+
@success_count.to_f / @execution_count
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def matches_context?(ctx)
|
|
52
|
+
return true if ctx.empty?
|
|
53
|
+
|
|
54
|
+
relevant = Constants::CONTEXT_DIMENSIONS.select { |d| @context.key?(d) || ctx.key?(d) }
|
|
55
|
+
return true if relevant.empty?
|
|
56
|
+
|
|
57
|
+
matches = relevant.count { |d| @context[d] == ctx[d] }
|
|
58
|
+
matches.to_f / relevant.size >= 0.5
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def decay
|
|
62
|
+
@strength -= Constants::DECAY_RATE
|
|
63
|
+
@strength >= Constants::HABIT_STRENGTH_FLOOR
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def mature?
|
|
67
|
+
@maturity == :habitual || @maturity == :automatic
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def stale?(threshold = 3600)
|
|
71
|
+
return true if @last_executed.nil?
|
|
72
|
+
|
|
73
|
+
(Time.now.utc - @last_executed) > threshold
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def similarity(other)
|
|
77
|
+
set_a = @actions.uniq
|
|
78
|
+
set_b = other.actions.uniq
|
|
79
|
+
return 0.0 if set_a.empty? && set_b.empty?
|
|
80
|
+
|
|
81
|
+
intersection = (set_a & set_b).size
|
|
82
|
+
union = (set_a | set_b).size
|
|
83
|
+
return 0.0 if union.zero?
|
|
84
|
+
|
|
85
|
+
intersection.to_f / union
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_h
|
|
89
|
+
{
|
|
90
|
+
id: @id,
|
|
91
|
+
actions: @actions,
|
|
92
|
+
context: @context,
|
|
93
|
+
execution_count: @execution_count,
|
|
94
|
+
success_count: @success_count,
|
|
95
|
+
strength: @strength,
|
|
96
|
+
maturity: @maturity,
|
|
97
|
+
cognitive_cost: cognitive_cost,
|
|
98
|
+
success_rate: success_rate,
|
|
99
|
+
last_executed: @last_executed,
|
|
100
|
+
created_at: @created_at
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def update_maturity
|
|
107
|
+
new_stage = Constants::MATURITY_STAGES.reverse.find do |stage|
|
|
108
|
+
@execution_count >= Constants::MATURITY_THRESHOLDS[stage]
|
|
109
|
+
end
|
|
110
|
+
@maturity = new_stage || :novel
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Habit
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MATURITY_STAGES = %i[novel learning practiced habitual automatic].freeze
|
|
9
|
+
|
|
10
|
+
MATURITY_THRESHOLDS = {
|
|
11
|
+
novel: 0,
|
|
12
|
+
learning: 3,
|
|
13
|
+
practiced: 10,
|
|
14
|
+
habitual: 25,
|
|
15
|
+
automatic: 50
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
COGNITIVE_COST = {
|
|
19
|
+
novel: 1.0,
|
|
20
|
+
learning: 0.8,
|
|
21
|
+
practiced: 0.5,
|
|
22
|
+
habitual: 0.2,
|
|
23
|
+
automatic: 0.05
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
REINFORCEMENT_RATE = 0.1
|
|
27
|
+
DECAY_RATE = 0.02
|
|
28
|
+
MIN_SEQUENCE_LENGTH = 2
|
|
29
|
+
MAX_SEQUENCE_LENGTH = 10
|
|
30
|
+
MAX_HABITS = 200
|
|
31
|
+
SIMILARITY_THRESHOLD = 0.7
|
|
32
|
+
CHUNKING_THRESHOLD = 5
|
|
33
|
+
HABIT_STRENGTH_FLOOR = 0.1
|
|
34
|
+
|
|
35
|
+
CONTEXT_DIMENSIONS = %i[domain mood time_of_day trigger].freeze
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Habit
|
|
6
|
+
module Helpers
|
|
7
|
+
class HabitStore # rubocop:disable Metrics/ClassLength
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
MAX_BUFFER_SIZE = 50
|
|
11
|
+
|
|
12
|
+
attr_reader :habits, :action_buffer
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@habits = {}
|
|
16
|
+
@action_buffer = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def record_action(action, context: {})
|
|
20
|
+
@action_buffer << { action: action.to_sym, context: context }
|
|
21
|
+
@action_buffer.shift if @action_buffer.size > MAX_BUFFER_SIZE
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def detect_patterns
|
|
25
|
+
new_habits = []
|
|
26
|
+
actions = @action_buffer.map { |e| e[:action] }
|
|
27
|
+
ctx = @action_buffer.last&.dig(:context) || {}
|
|
28
|
+
|
|
29
|
+
(Constants::MIN_SEQUENCE_LENGTH..Constants::MAX_SEQUENCE_LENGTH).each do |len|
|
|
30
|
+
next if actions.size < len
|
|
31
|
+
|
|
32
|
+
process_length(len, actions, ctx, new_habits)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
new_habits
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def find_matching(context: {})
|
|
39
|
+
@habits.values
|
|
40
|
+
.select { |h| h.matches_context?(context) }
|
|
41
|
+
.sort_by { |h| -h.strength }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get(id)
|
|
45
|
+
@habits[id]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reinforce(id, success:)
|
|
49
|
+
habit = @habits[id]
|
|
50
|
+
return unless habit
|
|
51
|
+
|
|
52
|
+
habit.record_execution(success: success)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def decay_all
|
|
56
|
+
removed = 0
|
|
57
|
+
@habits.each do |id, habit|
|
|
58
|
+
unless habit.decay
|
|
59
|
+
@habits.delete(id)
|
|
60
|
+
removed += 1
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
removed
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def merge_similar
|
|
67
|
+
merged = 0
|
|
68
|
+
@habits.keys.combination(2).each do |id_a, id_b|
|
|
69
|
+
merged += 1 if merge_pair(id_a, id_b)
|
|
70
|
+
end
|
|
71
|
+
merged
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def by_maturity(stage)
|
|
75
|
+
@habits.values.select { |h| h.maturity == stage }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def stats
|
|
79
|
+
habits_list = @habits.values
|
|
80
|
+
per_maturity = Constants::MATURITY_STAGES.to_h { |s| [s, 0] }
|
|
81
|
+
habits_list.each { |h| per_maturity[h.maturity] += 1 }
|
|
82
|
+
avg_strength = habits_list.empty? ? 0.0 : habits_list.sum(&:strength) / habits_list.size
|
|
83
|
+
oldest = habits_list.min_by(&:created_at)
|
|
84
|
+
{ total: habits_list.size, per_maturity: per_maturity, avg_strength: avg_strength, oldest: oldest&.to_h }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def evict_if_needed
|
|
88
|
+
return unless @habits.size > Constants::MAX_HABITS
|
|
89
|
+
|
|
90
|
+
weakest_id = @habits.min_by { |_id, h| h.strength }.first
|
|
91
|
+
@habits.delete(weakest_id)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def process_length(len, actions, ctx, new_habits)
|
|
97
|
+
count_subsequences(actions, len).each do |subseq, count|
|
|
98
|
+
next if count < Constants::CHUNKING_THRESHOLD
|
|
99
|
+
|
|
100
|
+
record_or_reinforce(subseq, count, ctx, new_habits)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def count_subsequences(actions, len)
|
|
105
|
+
counts = Hash.new(0)
|
|
106
|
+
(0..(actions.size - len)).each { |i| counts[actions[i, len]] += 1 }
|
|
107
|
+
counts
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def record_or_reinforce(subseq, count, ctx, new_habits)
|
|
111
|
+
existing = find_existing_sequence(subseq)
|
|
112
|
+
if existing
|
|
113
|
+
existing.record_execution(success: true)
|
|
114
|
+
else
|
|
115
|
+
create_habit(subseq, count, ctx, new_habits)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def create_habit(subseq, count, ctx, new_habits)
|
|
120
|
+
habit = ActionSequence.new(actions: subseq, context: ctx)
|
|
121
|
+
(count - 1).times { habit.record_execution(success: true) }
|
|
122
|
+
@habits[habit.id] = habit
|
|
123
|
+
evict_if_needed
|
|
124
|
+
new_habits << habit
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def merge_pair(id_a, id_b)
|
|
128
|
+
habit_a = @habits[id_a]
|
|
129
|
+
habit_b = @habits[id_b]
|
|
130
|
+
return false unless habit_a && habit_b
|
|
131
|
+
return false if habit_a.similarity(habit_b) < Constants::SIMILARITY_THRESHOLD
|
|
132
|
+
|
|
133
|
+
@habits.delete(habit_a.strength >= habit_b.strength ? id_b : id_a)
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def find_existing_sequence(actions)
|
|
138
|
+
@habits.values.find { |h| h.actions == actions.map(&:to_sym) }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Habit
|
|
6
|
+
module Runners
|
|
7
|
+
module Habit
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def observe_action(action:, context: {}, **)
|
|
12
|
+
Legion::Logging.debug "[habit] observe_action: action=#{action} context=#{context}"
|
|
13
|
+
habit_store.record_action(action, context: context)
|
|
14
|
+
detected = habit_store.detect_patterns
|
|
15
|
+
Legion::Logging.info "[habit] patterns detected: #{detected.size}" unless detected.empty?
|
|
16
|
+
{
|
|
17
|
+
recorded: true,
|
|
18
|
+
action: action,
|
|
19
|
+
new_habits_detected: detected.size,
|
|
20
|
+
habits: detected.map(&:to_h)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def suggest_habit(context: {}, **)
|
|
25
|
+
matches = habit_store.find_matching(context: context)
|
|
26
|
+
Legion::Logging.debug "[habit] suggest_habit: context=#{context} matches=#{matches.size}"
|
|
27
|
+
if matches.empty?
|
|
28
|
+
{ suggestion: nil, reason: :no_matching_habits }
|
|
29
|
+
else
|
|
30
|
+
best = matches.first
|
|
31
|
+
{
|
|
32
|
+
suggestion: best.to_h,
|
|
33
|
+
cognitive_savings: 1.0 - best.cognitive_cost,
|
|
34
|
+
alternatives: matches[1..2]&.map(&:to_h) || []
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def execute_habit(id:, success: true, **)
|
|
40
|
+
habit = habit_store.get(id)
|
|
41
|
+
return { error: :not_found } unless habit
|
|
42
|
+
|
|
43
|
+
habit.record_execution(success: success)
|
|
44
|
+
Legion::Logging.debug "[habit] execute_habit: id=#{id} success=#{success} maturity=#{habit.maturity}"
|
|
45
|
+
{ executed: true, habit: habit.to_h, cognitive_cost: habit.cognitive_cost }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def decay_habits(**)
|
|
49
|
+
removed = habit_store.decay_all
|
|
50
|
+
Legion::Logging.debug "[habit] decay_habits: removed=#{removed}"
|
|
51
|
+
{ decayed: true, removed_count: removed }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def merge_habits(**)
|
|
55
|
+
merged = habit_store.merge_similar
|
|
56
|
+
Legion::Logging.debug "[habit] merge_habits: merged=#{merged}"
|
|
57
|
+
{ merged_count: merged }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def habit_stats(**)
|
|
61
|
+
habit_store.stats
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def habit_repertoire(maturity: nil, limit: 20, **)
|
|
65
|
+
habits = maturity ? habit_store.by_maturity(maturity.to_sym) : habit_store.habits.values
|
|
66
|
+
Legion::Logging.debug "[habit] habit_repertoire: maturity=#{maturity} total=#{habits.size}"
|
|
67
|
+
{
|
|
68
|
+
habits: habits.sort_by { |h| -h.strength }.first(limit).map(&:to_h),
|
|
69
|
+
total: habits.size
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def habit_store
|
|
76
|
+
@habit_store ||= Helpers::HabitStore.new
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/habit/version'
|
|
4
|
+
require 'legion/extensions/habit/helpers/constants'
|
|
5
|
+
require 'legion/extensions/habit/helpers/action_sequence'
|
|
6
|
+
require 'legion/extensions/habit/helpers/habit_store'
|
|
7
|
+
require 'legion/extensions/habit/runners/habit'
|
|
8
|
+
require 'legion/extensions/habit/client'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module Habit
|
|
13
|
+
extend Legion::Extensions::Core if defined?(Legion::Extensions::Core)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-habit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matthew Iverson
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: legion-gaia
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Procedural learning and skill acquisition for brain-modeled agentic AI
|
|
27
|
+
— repeated action sequences become chunked habits with decreasing cognitive overhead
|
|
28
|
+
email:
|
|
29
|
+
- matt@legionIO.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- lib/legion/extensions/habit.rb
|
|
35
|
+
- lib/legion/extensions/habit/client.rb
|
|
36
|
+
- lib/legion/extensions/habit/helpers/action_sequence.rb
|
|
37
|
+
- lib/legion/extensions/habit/helpers/constants.rb
|
|
38
|
+
- lib/legion/extensions/habit/helpers/habit_store.rb
|
|
39
|
+
- lib/legion/extensions/habit/runners/habit.rb
|
|
40
|
+
- lib/legion/extensions/habit/version.rb
|
|
41
|
+
homepage: https://github.com/LegionIO/lex-habit
|
|
42
|
+
licenses:
|
|
43
|
+
- MIT
|
|
44
|
+
metadata:
|
|
45
|
+
homepage_uri: https://github.com/LegionIO/lex-habit
|
|
46
|
+
source_code_uri: https://github.com/LegionIO/lex-habit
|
|
47
|
+
documentation_uri: https://github.com/LegionIO/lex-habit
|
|
48
|
+
changelog_uri: https://github.com/LegionIO/lex-habit
|
|
49
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-habit/issues
|
|
50
|
+
rubygems_mfa_required: 'true'
|
|
51
|
+
rdoc_options: []
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '3.4'
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
requirements: []
|
|
65
|
+
rubygems_version: 3.6.9
|
|
66
|
+
specification_version: 4
|
|
67
|
+
summary: LEX Habit
|
|
68
|
+
test_files: []
|