conservation-guardian 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 017bb06291f703c2bbb5d8d07d5efac74460f64c2eba4a4ff56b7312453d8f73
4
+ data.tar.gz: 59d2a23f363cfb005b8f8c6e785cbc1faf25d9d69a7fa938a8875de6428157de
5
+ SHA512:
6
+ metadata.gz: cbbdcec8e4a8b22a0d9b1468a9948ada0818a9974a533a64cb065369e710180d738d6b9b22b1bd550b9405e0f0557375b642887f6b7cef955b038542c9b1ad70
7
+ data.tar.gz: 74831bf3304f302668035688043adb4224d69b2f631484610bc81ea80005a4e46c9853aa874f7af783ebf9834e246c42c66a8fa07e7c02ab7e3918ada032a035
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Conservation Guardian
2
+
3
+ Universal **Budget → Profile → Detect → Report** pattern for resource conservation in any Ruby project.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ gem install conservation-guardian
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ require "conservation_guardian"
15
+
16
+ # Define a budget
17
+ budget = ConservationGuardian::Budget.new(
18
+ name: "server_resources",
19
+ limits: { memory_mb: 512, cpu_percent: 80 }
20
+ )
21
+
22
+ # Profile usage
23
+ profiler = ConservationGuardian::Profiler.new(budget)
24
+ profiler.ingest([
25
+ { category: :memory_mb, value: 200, label: "worker_1" },
26
+ { category: :memory_mb, value: 600, label: "worker_2" },
27
+ { category: :cpu_percent, value: 45, label: "worker_1" },
28
+ { category: :cpu_percent, value: 95, label: "worker_2" }
29
+ ])
30
+
31
+ # Detect waste
32
+ detector = ConservationGuardian::Detector.new(budget, profiler)
33
+ findings = detector.detect
34
+
35
+ # Generate report
36
+ report = ConservationGuardian::Report.new(budget, profiler, findings)
37
+ puts report
38
+
39
+ # Or use the convenience method
40
+ report = ConservationGuardian.analyze(budget: budget, samples: samples)
41
+ ```
42
+
43
+ ## API
44
+
45
+ ### Budget
46
+
47
+ Defines resource limits by category.
48
+
49
+ ```ruby
50
+ budget = ConservationGuardian::Budget.new(name: "limits", limits: { memory: 512 })
51
+ budget.exceeded?(:memory, 600) # => true
52
+ budget.limit_for(:memory) # => 512
53
+ ```
54
+
55
+ ### Profiler
56
+
57
+ Tracks usage samples per category.
58
+
59
+ ```ruby
60
+ profiler.ingest({ category: :memory, value: 200, label: "process" })
61
+ profiler.stats_for(:memory) # => { count:, min:, max:, sum:, avg: }
62
+ ```
63
+
64
+ ### Detector
65
+
66
+ Finds over-budget and anomalous usage.
67
+
68
+ ```ruby
69
+ detector.detect # => [{ type: :over_budget, severity: :high, ... }]
70
+ ```
71
+
72
+ ### Report
73
+
74
+ Human-readable and machine-parseable output.
75
+
76
+ ```ruby
77
+ report.to_h # => Hash with budget, stats, findings, summary
78
+ report.to_s # => formatted text
79
+ report.summary # => { total_samples:, total_findings:, ... }
80
+ ```
81
+
82
+ ## License
83
+
84
+ MIT
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConservationGuardian
4
+ class Budget
5
+ attr_reader :limits, :name
6
+
7
+ # limits: Hash of { category_name => max_value }
8
+ def initialize(name: "default", limits: {})
9
+ @name = name
10
+ @limits = limits.transform_keys(&:to_sym)
11
+ freeze
12
+ end
13
+
14
+ def limit_for(category)
15
+ @limits[category.to_sym]
16
+ end
17
+
18
+ def exceeded?(category, value)
19
+ max = limit_for(category)
20
+ return false if max.nil?
21
+
22
+ value > max
23
+ end
24
+
25
+ def categories
26
+ @limits.keys
27
+ end
28
+
29
+ def to_h
30
+ { name: @name, limits: @limits.dup }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConservationGuardian
4
+ class Detector
5
+ attr_reader :budget, :profiler
6
+
7
+ def initialize(budget, profiler)
8
+ @budget = budget
9
+ @profiler = profiler
10
+ end
11
+
12
+ def detect
13
+ findings = []
14
+ findings.concat(detect_over_budget)
15
+ findings.concat(detect_anomalies)
16
+ findings
17
+ end
18
+
19
+ private
20
+
21
+ def detect_over_budget
22
+ profiler.over_budget_samples.map do |sample|
23
+ {
24
+ type: :over_budget,
25
+ severity: :high,
26
+ category: sample[:category],
27
+ value: sample[:value],
28
+ limit: budget.limit_for(sample[:category]),
29
+ label: sample[:label],
30
+ message: "#{sample[:label]} (#{sample[:value]}) exceeds limit #{budget.limit_for(sample[:category])} for #{sample[:category]}"
31
+ }
32
+ end
33
+ end
34
+
35
+ def detect_anomalies
36
+ findings = []
37
+ profiler.samples.map { |s| s[:category] }.uniq.each do |cat|
38
+ stats = profiler.stats_for(cat)
39
+ next unless stats && stats[:count] >= 2
40
+
41
+ threshold = stats[:avg] * 2
42
+ profiler.samples.select { |s| s[:category] == cat && s[:value] > threshold }.each do |sample|
43
+ findings << {
44
+ type: :anomaly,
45
+ severity: :medium,
46
+ category: cat,
47
+ value: sample[:value],
48
+ avg: stats[:avg],
49
+ label: sample[:label],
50
+ message: "#{sample[:label]} (#{sample[:value]}) is >2x the average (#{stats[:avg].round(2)}) for #{cat}"
51
+ }
52
+ end
53
+ end
54
+ findings
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConservationGuardian
4
+ class Profiler
5
+ attr_reader :samples, :budget
6
+
7
+ def initialize(budget)
8
+ @budget = budget
9
+ @samples = []
10
+ end
11
+
12
+ # samples: Array of { category:, value:, label: }
13
+ # or pass a single sample at a time
14
+ def ingest(samples)
15
+ samples = [samples] if samples.is_a?(Hash)
16
+ samples.each do |s|
17
+ @samples << {
18
+ category: s[:category].to_sym,
19
+ value: s[:value].to_f,
20
+ label: s[:label].to_s,
21
+ timestamp: Time.now
22
+ }
23
+ end
24
+ end
25
+
26
+ def stats_for(category)
27
+ cat = category.to_sym
28
+ vals = @samples.select { |s| s[:category] == cat }.map { |s| s[:value] }
29
+ return nil if vals.empty?
30
+
31
+ {
32
+ count: vals.size,
33
+ min: vals.min,
34
+ max: vals.max,
35
+ sum: vals.sum,
36
+ avg: vals.sum / vals.size
37
+ }
38
+ end
39
+
40
+ def all_stats
41
+ @samples.map { |s| s[:category] }.uniq.each_with_object({}) do |cat, h|
42
+ h[cat] = stats_for(cat)
43
+ end
44
+ end
45
+
46
+ def over_budget_samples
47
+ @samples.select { |s| @budget.exceeded?(s[:category], s[:value]) }
48
+ end
49
+
50
+ def clear!
51
+ @samples.clear
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConservationGuardian
4
+ class Report
5
+ attr_reader :budget, :profiler, :findings
6
+
7
+ def initialize(budget, profiler, findings)
8
+ @budget = budget
9
+ @profiler = profiler
10
+ @findings = findings
11
+ end
12
+
13
+ def to_h
14
+ {
15
+ budget: budget.to_h,
16
+ stats: profiler.all_stats,
17
+ findings: findings,
18
+ summary: summary
19
+ }
20
+ end
21
+
22
+ def summary
23
+ {
24
+ total_samples: profiler.samples.size,
25
+ total_findings: findings.size,
26
+ over_budget: findings.count { |f| f[:type] == :over_budget },
27
+ anomalies: findings.count { |f| f[:type] == :anomaly },
28
+ categories_checked: budget.categories.size
29
+ }
30
+ end
31
+
32
+ def to_s
33
+ lines = []
34
+ lines << "=== Conservation Guardian Report ==="
35
+ lines << "Budget: #{budget.name}"
36
+ lines << "Samples analyzed: #{summary[:total_samples]}"
37
+ lines << "Findings: #{summary[:total_findings]} (#{summary[:over_budget]} over budget, #{summary[:anomalies]} anomalies)"
38
+ lines << ""
39
+
40
+ if findings.any?
41
+ lines << "--- Findings ---"
42
+ findings.each do |f|
43
+ lines << "[#{f[:severity].upcase}] #{f[:message]}"
44
+ end
45
+ else
46
+ lines << "✅ No waste detected. All clear!"
47
+ end
48
+
49
+ lines.join("\n")
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "conservation_guardian/budget"
4
+ require_relative "conservation_guardian/profiler"
5
+ require_relative "conservation_guardian/detector"
6
+ require_relative "conservation_guardian/report"
7
+
8
+ module ConservationGuardian
9
+ class Error < StandardError; end
10
+
11
+ # Convenience: run the full pipeline and return a Report
12
+ def self.analyze(budget:, samples:)
13
+ profiler = Profiler.new(budget)
14
+ profiler.ingest(samples)
15
+ detector = Detector.new(budget, profiler)
16
+ findings = detector.detect
17
+ Report.new(budget, profiler, findings)
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: conservation-guardian
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - SuperInstance
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A guardian pattern library that provides budget tracking, usage profiling,
14
+ waste detection, and conservation reporting for any Ruby project.
15
+ email:
16
+ - team@superinstance.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - README.md
22
+ - lib/conservation_guardian.rb
23
+ - lib/conservation_guardian/budget.rb
24
+ - lib/conservation_guardian/detector.rb
25
+ - lib/conservation_guardian/profiler.rb
26
+ - lib/conservation_guardian/report.rb
27
+ homepage: https://github.com/SuperInstance/gem-conservation-guardian
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.7'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubygems_version: 3.3.5
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: Universal Budget → Profile → Detect → Report pattern for resource conservation
50
+ test_files: []