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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f221abbf1751de26290f2d11137ec31338bb77dfb64b033ce09ebffbcd73d9d
4
- data.tar.gz: fac9bfec6e9275b5b48d8d1e253edf98e1fa504b48f90300a44604fe79bd5cc9
3
+ metadata.gz: 119e238ddb0711af3bb4d23c5c65bb85c910420aa4ce79c1fe28c81950a38d0c
4
+ data.tar.gz: da4f50f6e3b1653c18d70617ab81630f3f9db6aa01cf6271205882f34df312f6
5
5
  SHA512:
6
- metadata.gz: '0259374ea1f54906333dba2cbb9de76c6c1081a1e24082956a86b055cfdaa942a442c8d48eb3f49f373276c91bf2901e21720b669205ddc90a3fe97001976d41'
7
- data.tar.gz: b6a5d1afa9da7d4f7a6feecd24e49ae2bced0123e79df54d368fa6f1e610189a72e8991866768ff0f4881bd056404b6208318764fb38e66bf57ab2f30835277e
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 (Level 2)**: `/Users/miverso2/rubymine/legion/extensions/CLAUDE.md`
5
- - **Parent (Level 1)**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
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.0
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
- This is NOT a typical LEX no runners, no actors, no AMQP transport, no Faraday client. It is a local-only utility gem with zero runtime dependencies.
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/plans/2026-03-18-lex-detect-design.md`
53
- - Implementation: `docs/plans/2026-03-18-lex-detect-implementation.md`
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
- 37 specs across 4 spec files.
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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Detect
6
- VERSION = '0.1.1'
6
+ VERSION = '0.1.3'
7
7
  end
8
8
  end
9
9
  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.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