lex-detect 0.1.1 → 0.1.3
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 +4 -4
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +10 -7
- data/lib/legion/extensions/detect/actors/delta_scan.rb +99 -0
- data/lib/legion/extensions/detect/actors/full_scan.rb +70 -0
- data/lib/legion/extensions/detect/local_migrations/20260319000001_create_detect_results.rb +16 -0
- data/lib/legion/extensions/detect/runners/task_observer.rb +88 -0
- data/lib/legion/extensions/detect/version.rb +1 -1
- data/lib/legion/extensions/detect.rb +11 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 119e238ddb0711af3bb4d23c5c65bb85c910420aa4ce79c1fe28c81950a38d0c
|
|
4
|
+
data.tar.gz: da4f50f6e3b1653c18d70617ab81630f3f9db6aa01cf6271205882f34df312f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f38552069181a4198433a821b64744f6bdf4cd42be0285c22eccfb4ef219f09f5d5d8a5825ac53c95ded7adb45278d5ca9ef20018c96f2dcac1ad61389da296
|
|
7
|
+
data.tar.gz: ecb32f15376b6079a1810c161a726f447e1aa6b07a3a187d894fe6393b31d608cc4c0a09227d52aefe88110e6f51ff3f3d9ba1e9e61772463587557600142896
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.3] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Runners::TaskObserver` for monitoring task failure patterns
|
|
7
|
+
- `check_and_publish_failure_patterns` publishes failure pattern events to AMQP for self-healing pipeline integration
|
|
8
|
+
- `extract_gem_name` helper derives gem name from runner class string
|
|
9
|
+
- `build_failure_pattern` creates structured failure pattern hash
|
|
10
|
+
|
|
11
|
+
## [0.1.2] - 2026-03-19
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `FullScan` Once actor: runs full environment scan at boot with 2s delay, persists results and writes traces
|
|
15
|
+
- `DeltaScan` Every actor: runs delta detection every 300s, computes additions/removals via Set comparison
|
|
16
|
+
- Data::Local migration for detect_results table (optional persistence)
|
|
17
|
+
|
|
3
18
|
## [0.1.1] - 2026-03-18
|
|
4
19
|
|
|
5
20
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# lex-detect: Environment-Aware Extension Discovery
|
|
2
2
|
|
|
3
3
|
**Repository Level 3 Documentation**
|
|
4
|
-
- **Parent
|
|
5
|
-
- **
|
|
4
|
+
- **Parent**: `/Users/miverso2/rubymine/legion/extensions-core/CLAUDE.md`
|
|
5
|
+
- **Grandparent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
|
|
6
6
|
|
|
7
7
|
## Purpose
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ Legion Extension that scans the local environment and recommends which `lex-*` e
|
|
|
10
10
|
|
|
11
11
|
**GitHub**: https://github.com/LegionIO/lex-detect
|
|
12
12
|
**License**: MIT
|
|
13
|
-
**Version**: 0.1.
|
|
13
|
+
**Version**: 0.1.2
|
|
14
14
|
|
|
15
15
|
## Architecture
|
|
16
16
|
|
|
@@ -19,10 +19,13 @@ Legion::Extensions::Detect
|
|
|
19
19
|
├── CATALOG (constant) # Frozen array of 20 detection rules
|
|
20
20
|
├── Scanner # Probes environment, matches against catalog
|
|
21
21
|
├── Installer # Gem.install wrapper with dry_run support
|
|
22
|
+
├── Actors/
|
|
23
|
+
│ ├── FullScan (Once) # Full environment scan at startup
|
|
24
|
+
│ └── DeltaScan (Every) # Periodic delta scan every 6 hours
|
|
22
25
|
└── Entry Point # Public API: scan, missing, install_missing!, catalog
|
|
23
26
|
```
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
Local-only utility gem. Actors run scans but do not create AMQP queues.
|
|
26
29
|
|
|
27
30
|
- `data_required? false` — no database needed
|
|
28
31
|
- `remote_invocable? false` — no AMQP queue creation
|
|
@@ -49,12 +52,12 @@ Legion::Extensions::Detect.catalog # raw CATALOG constant
|
|
|
49
52
|
|
|
50
53
|
## Design Docs
|
|
51
54
|
|
|
52
|
-
- Design: `docs/
|
|
53
|
-
- Implementation: `docs/
|
|
55
|
+
- Design: `docs/work/completed/2026-03-18-lex-detect-design.md`
|
|
56
|
+
- Implementation: `docs/work/completed/2026-03-18-lex-detect-implementation.md`
|
|
54
57
|
|
|
55
58
|
## Testing
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
40 specs across 6 spec files.
|
|
58
61
|
|
|
59
62
|
```bash
|
|
60
63
|
bundle install
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Detect
|
|
6
|
+
module Actors
|
|
7
|
+
class DeltaScan < Legion::Extensions::Actors::Every
|
|
8
|
+
def time
|
|
9
|
+
settings_interval || 300
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run_now?
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def action(**_opts)
|
|
17
|
+
current = Scanner.scan
|
|
18
|
+
previous = last_scan_results
|
|
19
|
+
deltas = compute_deltas(current, previous)
|
|
20
|
+
|
|
21
|
+
persist_results(current) unless deltas[:added].empty? && deltas[:removed].empty?
|
|
22
|
+
write_delta_traces(deltas) unless deltas[:added].empty? && deltas[:removed].empty?
|
|
23
|
+
deltas
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def settings_interval
|
|
29
|
+
return nil unless defined?(Legion::Settings)
|
|
30
|
+
|
|
31
|
+
Legion::Settings.dig(:extensions, :'lex-detect', :delta_interval)
|
|
32
|
+
rescue StandardError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def last_scan_results
|
|
37
|
+
return [] unless defined?(Legion::Data::Local) &&
|
|
38
|
+
Legion::Data::Local.respond_to?(:connected?) &&
|
|
39
|
+
Legion::Data::Local.connected?
|
|
40
|
+
|
|
41
|
+
Legion::Data::Local.model(:detect_results).all.map do |row|
|
|
42
|
+
{
|
|
43
|
+
name: row[:name],
|
|
44
|
+
extensions: Legion::JSON.load(row[:extensions]),
|
|
45
|
+
matched_signals: Legion::JSON.load(row[:matched_signals])
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError
|
|
49
|
+
[]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def compute_deltas(current, previous)
|
|
53
|
+
prev_names = previous.to_set { |r| r[:name] }
|
|
54
|
+
curr_names = current.to_set { |r| r[:name] }
|
|
55
|
+
|
|
56
|
+
added = current.reject { |r| prev_names.include?(r[:name]) }
|
|
57
|
+
removed = previous.reject { |r| curr_names.include?(r[:name]) }
|
|
58
|
+
{ added: added, removed: removed }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def persist_results(results)
|
|
62
|
+
return unless defined?(Legion::Data::Local) &&
|
|
63
|
+
Legion::Data::Local.respond_to?(:connected?) &&
|
|
64
|
+
Legion::Data::Local.connected?
|
|
65
|
+
|
|
66
|
+
model = Legion::Data::Local.model(:detect_results)
|
|
67
|
+
model.where.delete
|
|
68
|
+
results.each do |result|
|
|
69
|
+
model.insert(
|
|
70
|
+
name: result[:name],
|
|
71
|
+
extensions: Legion::JSON.dump(result[:extensions]),
|
|
72
|
+
matched_signals: Legion::JSON.dump(result[:matched_signals]),
|
|
73
|
+
installed: Legion::JSON.dump(result[:installed]),
|
|
74
|
+
scanned_at: Time.now,
|
|
75
|
+
created_at: Time.now,
|
|
76
|
+
updated_at: Time.now
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def write_delta_traces(deltas)
|
|
84
|
+
return unless defined?(Legion::Extensions::Agentic::Memory)
|
|
85
|
+
|
|
86
|
+
deltas[:added]&.each do |r|
|
|
87
|
+
Legion::Extensions::Agentic::Memory::Trace.write(
|
|
88
|
+
source: 'lex-detect', category: 'environment',
|
|
89
|
+
content: "New detection: #{r[:name]}", confidence: 0.8
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
rescue StandardError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Detect
|
|
6
|
+
module Actors
|
|
7
|
+
class FullScan < Legion::Extensions::Actors::Once
|
|
8
|
+
def delay
|
|
9
|
+
2.0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def action(**_opts)
|
|
13
|
+
results = Scanner.scan
|
|
14
|
+
persist_results(results)
|
|
15
|
+
write_traces(results)
|
|
16
|
+
transition_catalog
|
|
17
|
+
results
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def persist_results(results)
|
|
23
|
+
return unless defined?(Legion::Data::Local) &&
|
|
24
|
+
Legion::Data::Local.respond_to?(:connected?) &&
|
|
25
|
+
Legion::Data::Local.connected?
|
|
26
|
+
|
|
27
|
+
model = Legion::Data::Local.model(:detect_results)
|
|
28
|
+
model.where.delete
|
|
29
|
+
results.each do |result|
|
|
30
|
+
model.insert(
|
|
31
|
+
name: result[:name],
|
|
32
|
+
extensions: Legion::JSON.dump(result[:extensions]),
|
|
33
|
+
matched_signals: Legion::JSON.dump(result[:matched_signals]),
|
|
34
|
+
installed: Legion::JSON.dump(result[:installed]),
|
|
35
|
+
scanned_at: Time.now,
|
|
36
|
+
created_at: Time.now,
|
|
37
|
+
updated_at: Time.now
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
Legion::Logging.debug { "FullScan persist failed: #{e.message}" } if defined?(Legion::Logging)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def write_traces(results)
|
|
45
|
+
return unless defined?(Legion::Extensions::Agentic::Memory)
|
|
46
|
+
|
|
47
|
+
results.each do |result|
|
|
48
|
+
Legion::Extensions::Agentic::Memory::Trace.write(
|
|
49
|
+
source: 'lex-detect',
|
|
50
|
+
category: 'environment',
|
|
51
|
+
content: "Detected #{result[:name]}: #{result[:matched_signals].join(', ')}",
|
|
52
|
+
confidence: 0.9
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
Legion::Logging.debug { "FullScan trace write failed: #{e.message}" } if defined?(Legion::Logging)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def transition_catalog
|
|
60
|
+
return unless defined?(Legion::Extensions::Catalog)
|
|
61
|
+
|
|
62
|
+
Legion::Extensions::Catalog.transition('lex-detect', :running)
|
|
63
|
+
rescue StandardError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
change do
|
|
5
|
+
create_table(:detect_results) do
|
|
6
|
+
primary_key :id
|
|
7
|
+
String :name, null: false
|
|
8
|
+
String :extensions, text: true
|
|
9
|
+
String :matched_signals, text: true
|
|
10
|
+
String :installed, text: true
|
|
11
|
+
Time :scanned_at
|
|
12
|
+
Time :created_at
|
|
13
|
+
Time :updated_at
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Detect
|
|
6
|
+
module Runners
|
|
7
|
+
module TaskObserver
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def observe(tasks:, **)
|
|
11
|
+
return { success: true, alerts: [] } unless tasks.is_a?(Array)
|
|
12
|
+
|
|
13
|
+
alerts = generate_alerts(tasks)
|
|
14
|
+
check_and_publish_failure_patterns(tasks)
|
|
15
|
+
{ success: true, alerts: alerts, task_count: tasks.size }
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
{ success: false, reason: :error, message: e.message }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def generate_alerts(tasks)
|
|
23
|
+
failed = tasks.select { |t| t[:status] == 'failed' }
|
|
24
|
+
return [] if failed.empty?
|
|
25
|
+
|
|
26
|
+
failed.group_by { |t| t[:runner_class] }.filter_map do |runner_class, failures|
|
|
27
|
+
next unless failures.size >= 3
|
|
28
|
+
|
|
29
|
+
{ runner_class: runner_class, failure_count: failures.size, level: :warning }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check_and_publish_failure_patterns(tasks)
|
|
34
|
+
return unless defined?(Legion::Data) && defined?(Legion::Transport)
|
|
35
|
+
|
|
36
|
+
runner_failures = tasks.select { |t| t[:status] == 'failed' }
|
|
37
|
+
.group_by { |t| t[:runner_class] }
|
|
38
|
+
|
|
39
|
+
runner_failures.each do |runner_class, failures|
|
|
40
|
+
next unless failures.size >= 3
|
|
41
|
+
|
|
42
|
+
gem_name = extract_gem_name(runner_class)
|
|
43
|
+
backtraces = failures.filter_map { |f| f[:error_backtrace] }.first(5)
|
|
44
|
+
error_class = failures.first[:error_class] || 'StandardError'
|
|
45
|
+
|
|
46
|
+
pattern = build_failure_pattern(gem_name, runner_class, error_class, backtraces, failures.size)
|
|
47
|
+
publish_failure_pattern(pattern)
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_failure_pattern(gem_name, runner_class, error_class, backtraces, count)
|
|
54
|
+
{
|
|
55
|
+
gem_name: gem_name,
|
|
56
|
+
runner_class: runner_class,
|
|
57
|
+
error_class: error_class,
|
|
58
|
+
backtraces: backtraces,
|
|
59
|
+
failure_count: count,
|
|
60
|
+
window_minutes: 60
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def publish_failure_pattern(pattern)
|
|
65
|
+
return unless defined?(Legion::Transport::Messages::Dynamic)
|
|
66
|
+
|
|
67
|
+
Legion::Transport::Messages::Dynamic.new(
|
|
68
|
+
function: 'auto_fix',
|
|
69
|
+
payload: pattern
|
|
70
|
+
).publish
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extract_gem_name(runner_class)
|
|
76
|
+
return nil unless runner_class
|
|
77
|
+
|
|
78
|
+
parts = runner_class.to_s.split('::')
|
|
79
|
+
ext_idx = parts.index('Extensions')
|
|
80
|
+
return nil unless ext_idx && parts[ext_idx + 1]
|
|
81
|
+
|
|
82
|
+
"lex-#{parts[ext_idx + 1].gsub(/([a-z])([A-Z])/, '\1_\2').downcase}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -4,6 +4,7 @@ require 'legion/extensions/detect/version'
|
|
|
4
4
|
require 'legion/extensions/detect/catalog'
|
|
5
5
|
require 'legion/extensions/detect/scanner'
|
|
6
6
|
require 'legion/extensions/detect/installer'
|
|
7
|
+
require_relative 'detect/runners/task_observer'
|
|
7
8
|
|
|
8
9
|
module Legion
|
|
9
10
|
module Extensions
|
|
@@ -40,6 +41,16 @@ module Legion
|
|
|
40
41
|
CATALOG
|
|
41
42
|
end
|
|
42
43
|
end
|
|
44
|
+
|
|
45
|
+
require_relative 'detect/actors/full_scan' if defined?(Legion::Extensions::Actors::Once)
|
|
46
|
+
require_relative 'detect/actors/delta_scan' if defined?(Legion::Extensions::Actors::Every)
|
|
47
|
+
|
|
48
|
+
if defined?(Legion::Data::Local)
|
|
49
|
+
Legion::Data::Local.register_migrations(
|
|
50
|
+
name: :detect,
|
|
51
|
+
path: File.join(__dir__, 'detect', 'local_migrations')
|
|
52
|
+
)
|
|
53
|
+
end
|
|
43
54
|
end
|
|
44
55
|
end
|
|
45
56
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-detect
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -27,8 +27,12 @@ files:
|
|
|
27
27
|
- README.md
|
|
28
28
|
- lex-detect.gemspec
|
|
29
29
|
- lib/legion/extensions/detect.rb
|
|
30
|
+
- lib/legion/extensions/detect/actors/delta_scan.rb
|
|
31
|
+
- lib/legion/extensions/detect/actors/full_scan.rb
|
|
30
32
|
- lib/legion/extensions/detect/catalog.rb
|
|
31
33
|
- lib/legion/extensions/detect/installer.rb
|
|
34
|
+
- lib/legion/extensions/detect/local_migrations/20260319000001_create_detect_results.rb
|
|
35
|
+
- lib/legion/extensions/detect/runners/task_observer.rb
|
|
32
36
|
- lib/legion/extensions/detect/scanner.rb
|
|
33
37
|
- lib/legion/extensions/detect/version.rb
|
|
34
38
|
homepage: https://github.com/LegionIO/lex-detect
|