self_agency 0.0.1

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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +40 -0
  4. data/.irbrc +22 -0
  5. data/CHANGELOG.md +5 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +177 -0
  9. data/Rakefile +8 -0
  10. data/docs/api/configuration.md +85 -0
  11. data/docs/api/errors.md +166 -0
  12. data/docs/api/index.md +37 -0
  13. data/docs/api/self-agency-module.md +198 -0
  14. data/docs/architecture/overview.md +181 -0
  15. data/docs/architecture/security.md +101 -0
  16. data/docs/assets/images/self_agency.gif +0 -0
  17. data/docs/assets/images/self_agency.mp4 +0 -0
  18. data/docs/development/contributing.md +45 -0
  19. data/docs/development/setup.md +81 -0
  20. data/docs/development/testing.md +70 -0
  21. data/docs/examples/autonomous-robots.md +109 -0
  22. data/docs/examples/basic-examples.md +237 -0
  23. data/docs/examples/collaborative-robots.md +98 -0
  24. data/docs/examples/full-workflow.md +100 -0
  25. data/docs/examples/index.md +36 -0
  26. data/docs/getting-started/installation.md +71 -0
  27. data/docs/getting-started/quick-start.md +94 -0
  28. data/docs/guide/configuration.md +113 -0
  29. data/docs/guide/generating-methods.md +146 -0
  30. data/docs/guide/how-to-use.md +144 -0
  31. data/docs/guide/lifecycle-hooks.md +86 -0
  32. data/docs/guide/prompt-templates.md +189 -0
  33. data/docs/guide/saving-methods.md +84 -0
  34. data/docs/guide/scopes.md +74 -0
  35. data/docs/guide/source-inspection.md +96 -0
  36. data/docs/index.md +77 -0
  37. data/examples/01_basic_usage.rb +27 -0
  38. data/examples/02_multiple_methods.rb +43 -0
  39. data/examples/03_scopes.rb +40 -0
  40. data/examples/04_source_inspection.rb +46 -0
  41. data/examples/05_lifecycle_hook.rb +55 -0
  42. data/examples/06_configuration.rb +97 -0
  43. data/examples/07_error_handling.rb +103 -0
  44. data/examples/08_class_context.rb +64 -0
  45. data/examples/09_method_override.rb +52 -0
  46. data/examples/10_full_workflow.rb +118 -0
  47. data/examples/11_collaborative_robots/atlas.rb +31 -0
  48. data/examples/11_collaborative_robots/echo.rb +30 -0
  49. data/examples/11_collaborative_robots/main.rb +190 -0
  50. data/examples/11_collaborative_robots/nova.rb +71 -0
  51. data/examples/11_collaborative_robots/robot.rb +119 -0
  52. data/examples/12_autonomous_robots/analyst.rb +193 -0
  53. data/examples/12_autonomous_robots/collector.rb +78 -0
  54. data/examples/12_autonomous_robots/main.rb +166 -0
  55. data/examples/12_autonomous_robots/planner.rb +125 -0
  56. data/examples/12_autonomous_robots/robot.rb +284 -0
  57. data/examples/generated/from_range_class.rb +3 -0
  58. data/examples/generated/mean_instance.rb +4 -0
  59. data/examples/generated/median_instance.rb +15 -0
  60. data/examples/generated/report_singleton.rb +3 -0
  61. data/examples/generated/standard_deviation_instance.rb +8 -0
  62. data/examples/lib/message_bus.rb +57 -0
  63. data/examples/lib/setup.rb +8 -0
  64. data/lib/self_agency/configuration.rb +76 -0
  65. data/lib/self_agency/errors.rb +35 -0
  66. data/lib/self_agency/generator.rb +47 -0
  67. data/lib/self_agency/prompts/generate/system.txt.erb +15 -0
  68. data/lib/self_agency/prompts/generate/user.txt.erb +13 -0
  69. data/lib/self_agency/prompts/shape/system.txt.erb +26 -0
  70. data/lib/self_agency/prompts/shape/user.txt.erb +10 -0
  71. data/lib/self_agency/sandbox.rb +17 -0
  72. data/lib/self_agency/saver.rb +62 -0
  73. data/lib/self_agency/validator.rb +64 -0
  74. data/lib/self_agency/version.rb +5 -0
  75. data/lib/self_agency.rb +315 -0
  76. data/mkdocs.yml +156 -0
  77. data/sig/self_agency.rbs +4 -0
  78. metadata +163 -0
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # 03_scopes.rb — Instance, singleton, and class scopes
5
+ #
6
+ # Demonstrates:
7
+ # - scope: :instance — available to all instances
8
+ # - scope: :singleton — available to only one instance
9
+ # - scope: :class — available on the class itself
10
+ # - Singleton methods are NOT available on other instances
11
+ #
12
+ # Requires a running Ollama instance with the configured model.
13
+
14
+ require_relative "lib/setup"
15
+
16
+ class Greeter
17
+ include SelfAgency
18
+ end
19
+
20
+ alice = Greeter.new
21
+ bob = Greeter.new
22
+
23
+ # --- Instance scope (default) ---
24
+ puts "=== Instance scope ==="
25
+ alice._("an instance method named 'hello' that returns the string 'Hello, world!'")
26
+ puts "alice.hello = #{alice.hello}"
27
+ puts "bob.hello = #{bob.hello} (available to all instances)"
28
+ puts
29
+
30
+ # --- Singleton scope ---
31
+ puts "=== Singleton scope ==="
32
+ alice._("an instance method named 'secret' that returns the string 'Alice only'", scope: :singleton)
33
+ puts "alice.secret = #{alice.secret}"
34
+ puts "bob.respond_to?(:secret) = #{bob.respond_to?(:secret)} (NOT available on other instances)"
35
+ puts
36
+
37
+ # --- Class scope ---
38
+ puts "=== Class scope ==="
39
+ alice._("a class method named 'self.class_greeting' that returns 'Greetings from Greeter'", scope: :class)
40
+ puts "Greeter.class_greeting = #{Greeter.class_greeting}"
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # 04_source_inspection.rb — Viewing generated source code
5
+ #
6
+ # Demonstrates:
7
+ # - _source_for on an instance (instance-level lookup)
8
+ # - _source_for on the class (class-level lookup via ClassMethods)
9
+ # - Original description appears as a comment header
10
+ # - Fallback to method_source for file-defined methods
11
+ #
12
+ # Requires a running Ollama instance with the configured model.
13
+
14
+ require_relative "lib/setup"
15
+
16
+ class MathHelper
17
+ include SelfAgency
18
+
19
+ # Multiplies a number by two.
20
+ def double(n)
21
+ n * 2
22
+ end
23
+ end
24
+
25
+ helper = MathHelper.new
26
+
27
+ puts "=== Generate a method ==="
28
+ description = "an instance method named 'square' that accepts an integer n and returns n * n"
29
+ helper._(description)
30
+ puts "helper.square(5) = #{helper.square(5)}"
31
+ puts
32
+
33
+ puts "=== Instance-level _source_for ==="
34
+ puts helper._source_for(:square)
35
+ puts
36
+
37
+ puts "=== Class-level _source_for ==="
38
+ puts MathHelper._source_for(:square)
39
+ puts
40
+
41
+ puts "=== Fallback to method_source for file-defined methods ==="
42
+ puts helper._source_for(:double)
43
+ puts
44
+
45
+ puts "=== _source_for returns nil for unknown methods ==="
46
+ puts helper._source_for(:nonexistent).inspect
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # 05_lifecycle_hook.rb — Persisting generated methods
5
+ #
6
+ # Demonstrates:
7
+ # - Overriding on_method_generated(method_name, scope, code)
8
+ # - Hook fires for each method generated
9
+ # - Persistence pattern: saving generated code to files
10
+ #
11
+ # Requires a running Ollama instance with the configured model.
12
+
13
+ require_relative "lib/setup"
14
+
15
+ class PersistentCalculator
16
+ include SelfAgency
17
+
18
+ attr_reader :generation_log
19
+
20
+ def initialize
21
+ @generation_log = []
22
+ end
23
+
24
+ def on_method_generated(method_name, scope, code)
25
+ @generation_log << { method_name: method_name, scope: scope, code: code }
26
+ puts " [hook] Generated :#{method_name} (scope: #{scope})"
27
+ puts " [hook] Code preview: #{code.lines.first.chomp}..."
28
+ end
29
+ end
30
+
31
+ calc = PersistentCalculator.new
32
+
33
+ puts "=== Generating methods (hook will fire for each) ==="
34
+ puts
35
+
36
+ calc._("an instance method named 'increment' that accepts an integer n and returns n + 1")
37
+ puts
38
+
39
+ calc._(
40
+ "two instance methods: " \
41
+ "'min_of(a, b)' returns the smaller of a and b, " \
42
+ "'max_of(a, b)' returns the larger of a and b"
43
+ )
44
+ puts
45
+
46
+ puts "=== Generation log ==="
47
+ calc.generation_log.each_with_index do |entry, i|
48
+ puts "#{i + 1}. :#{entry[:method_name]} (#{entry[:scope]})"
49
+ end
50
+
51
+ puts
52
+ puts "=== Using the generated methods ==="
53
+ puts "calc.increment(41) = #{calc.increment(41)}"
54
+ puts "calc.min_of(3, 7) = #{calc.min_of(3, 7)}"
55
+ puts "calc.max_of(3, 7) = #{calc.max_of(3, 7)}"
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # 06_configuration.rb — Configuration options
5
+ #
6
+ # Demonstrates:
7
+ # - All configurable options with their defaults
8
+ # - Changing provider, model, timeouts, retries
9
+ # - SelfAgency.reset! and SelfAgency.ensure_configured!
10
+ # - Custom template_directory configuration
11
+ #
12
+ # This example does NOT require a running Ollama instance for
13
+ # the configuration inspection parts. Only the final generation
14
+ # call requires a live LLM.
15
+
16
+ require_relative "../lib/self_agency"
17
+
18
+ puts "=== Default configuration (before configure) ==="
19
+ SelfAgency.reset!
20
+ cfg = SelfAgency.configuration
21
+ puts <<~DEFAULTS
22
+ provider: #{cfg.provider.inspect}
23
+ model: #{cfg.model.inspect}
24
+ api_base: #{cfg.api_base.inspect}
25
+ request_timeout: #{cfg.request_timeout}
26
+ max_retries: #{cfg.max_retries}
27
+ retry_interval: #{cfg.retry_interval}
28
+ template_directory: #{cfg.template_directory}
29
+ DEFAULTS
30
+
31
+ puts "=== ensure_configured! before configure ==="
32
+ begin
33
+ SelfAgency.ensure_configured!
34
+ rescue SelfAgency::Error => e
35
+ puts "Caught expected error: #{e.message}"
36
+ end
37
+ puts
38
+
39
+ puts "=== Configuring with custom values ==="
40
+ SelfAgency.configure do |config|
41
+ config.provider = :ollama
42
+ config.model = "qwen3-coder:30b"
43
+ config.api_base = "http://localhost:11434/v1"
44
+ config.request_timeout = 60
45
+ config.max_retries = 3
46
+ config.retry_interval = 1.0
47
+ end
48
+
49
+ cfg = SelfAgency.configuration
50
+ puts <<~CUSTOM
51
+ provider: #{cfg.provider.inspect}
52
+ model: #{cfg.model.inspect}
53
+ api_base: #{cfg.api_base.inspect}
54
+ request_timeout: #{cfg.request_timeout}
55
+ max_retries: #{cfg.max_retries}
56
+ retry_interval: #{cfg.retry_interval}
57
+ template_directory: #{cfg.template_directory}
58
+ CUSTOM
59
+
60
+ puts "=== ensure_configured! after configure ==="
61
+ SelfAgency.ensure_configured!
62
+ puts "No error — configuration is active."
63
+ puts
64
+
65
+ puts "=== reset! restores defaults ==="
66
+ SelfAgency.reset!
67
+ cfg = SelfAgency.configuration
68
+ puts "provider after reset: #{cfg.provider.inspect}"
69
+ puts "model after reset: #{cfg.model.inspect}"
70
+ puts
71
+
72
+ begin
73
+ SelfAgency.ensure_configured!
74
+ rescue SelfAgency::Error => e
75
+ puts "ensure_configured! after reset: #{e.message}"
76
+ end
77
+ puts
78
+
79
+ puts "=== Custom template directory ==="
80
+ SelfAgency.configure do |config|
81
+ config.provider = :ollama
82
+ config.model = "qwen3-coder:30b"
83
+ config.api_base = "http://localhost:11434/v1"
84
+ config.template_directory = "/tmp/my_custom_prompts"
85
+ end
86
+ puts "template_directory: #{SelfAgency.configuration.template_directory}"
87
+ puts
88
+
89
+ # Restore standard config for any follow-up usage
90
+ SelfAgency.reset!
91
+ SelfAgency.configure do |config|
92
+ config.provider = :ollama
93
+ config.model = "qwen3-coder:30b"
94
+ config.api_base = "http://localhost:11434/v1"
95
+ end
96
+ puts "=== Configuration restored for live usage ==="
97
+ puts "Ready to generate methods (requires running Ollama)."
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # 07_error_handling.rb — Error classes and rescue patterns
5
+ #
6
+ # Demonstrates:
7
+ # - The SelfAgency error hierarchy
8
+ # - Rescuing GenerationError, ValidationError, SecurityError
9
+ # - Calling _() before configure raises SelfAgency::Error
10
+ # - Defensive usage patterns
11
+ #
12
+ # This example does NOT require a running Ollama instance.
13
+
14
+ require_relative "../lib/self_agency"
15
+
16
+ puts "=== Error hierarchy ==="
17
+ puts <<~HIERARCHY
18
+ SelfAgency::Error < StandardError
19
+ SelfAgency::GenerationError < SelfAgency::Error
20
+ SelfAgency::ValidationError < SelfAgency::Error
21
+ SelfAgency::SecurityError < SelfAgency::Error
22
+ HIERARCHY
23
+
24
+ puts "Verification:"
25
+ puts " GenerationError < Error: #{SelfAgency::GenerationError < SelfAgency::Error}"
26
+ puts " ValidationError < Error: #{SelfAgency::ValidationError < SelfAgency::Error}"
27
+ puts " SecurityError < Error: #{SelfAgency::SecurityError < SelfAgency::Error}"
28
+ puts " Error < StandardError: #{SelfAgency::Error < StandardError}"
29
+ puts
30
+
31
+ # --- Calling _() before configure ---
32
+ puts "=== Calling _() before configure ==="
33
+ SelfAgency.reset!
34
+
35
+ class Widget
36
+ include SelfAgency
37
+ end
38
+
39
+ begin
40
+ Widget.new._("a method")
41
+ rescue SelfAgency::Error => e
42
+ puts "Caught SelfAgency::Error: #{e.message}"
43
+ end
44
+ puts
45
+
46
+ # --- Validation errors via private helpers ---
47
+ puts "=== ValidationError examples ==="
48
+ SelfAgency.configure do |config|
49
+ config.provider = :ollama
50
+ config.model = "qwen3-coder:30b"
51
+ config.api_base = "http://localhost:11434/v1"
52
+ end
53
+
54
+ widget = Widget.new
55
+
56
+ # Empty code
57
+ begin
58
+ widget.send(:self_agency_validate!, "")
59
+ rescue SelfAgency::ValidationError => e
60
+ puts "Empty code: #{e.class} — #{e.message}"
61
+ end
62
+
63
+ # Missing def...end
64
+ begin
65
+ widget.send(:self_agency_validate!, "puts 'hello'")
66
+ rescue SelfAgency::ValidationError => e
67
+ puts "No def...end: #{e.class} — #{e.message}"
68
+ end
69
+
70
+ # Syntax error
71
+ begin
72
+ widget.send(:self_agency_validate!, "def broken\n if true\nend")
73
+ rescue SelfAgency::ValidationError => e
74
+ puts "Syntax error: #{e.class} — #{e.message}"
75
+ end
76
+ puts
77
+
78
+ # --- Security errors ---
79
+ puts "=== SecurityError examples ==="
80
+
81
+ dangerous_samples = {
82
+ "system call" => "def hack\n system('ls')\nend",
83
+ "File access" => "def hack\n File.read('/etc/passwd')\nend",
84
+ "eval usage" => "def hack\n eval('1+1')\nend",
85
+ "require" => "def hack\n require 'socket'\nend",
86
+ }
87
+
88
+ dangerous_samples.each do |label, code|
89
+ begin
90
+ widget.send(:self_agency_validate!, code)
91
+ rescue SelfAgency::SecurityError => e
92
+ puts "#{label.ljust(14)}: #{e.class} — #{e.message}"
93
+ end
94
+ end
95
+ puts
96
+
97
+ # --- Catching all SelfAgency errors with a single rescue ---
98
+ puts "=== Catching all errors with rescue SelfAgency::Error ==="
99
+ begin
100
+ raise SelfAgency::SecurityError, "example security violation"
101
+ rescue SelfAgency::Error => e
102
+ puts "Caught via base class: #{e.class} — #{e.message}"
103
+ end
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # 08_class_context.rb — Instance variable and method awareness
5
+ #
6
+ # Demonstrates:
7
+ # - The LLM receives class context (class name, ivars, public methods)
8
+ # - Generated methods can reference existing instance variables
9
+ # - Class introspection drives smarter code generation
10
+ #
11
+ # Requires a running Ollama instance with the configured model.
12
+
13
+ require_relative "lib/setup"
14
+
15
+ class BankAccount
16
+ include SelfAgency
17
+
18
+ attr_reader :owner, :balance
19
+
20
+ def initialize(owner, balance)
21
+ @owner = owner
22
+ @balance = balance
23
+ end
24
+
25
+ def deposit(amount)
26
+ @balance += amount
27
+ end
28
+ end
29
+
30
+ account = BankAccount.new("Alice", 1000)
31
+
32
+ # Show what the LLM will see as context
33
+ vars = account.send(:self_agency_generation_vars)
34
+ puts "=== Class context sent to LLM ==="
35
+ puts " class_name: #{vars[:class_name]}"
36
+ puts " ivars: #{vars[:ivars]}"
37
+ puts " methods: #{vars[:methods]}"
38
+ puts
39
+
40
+ puts "=== Generating a method that uses existing instance variables ==="
41
+ account._(
42
+ "an instance method named 'summary' that returns a string like " \
43
+ "'Account for <owner>: $<balance>' using the @owner and @balance instance variables"
44
+ )
45
+
46
+ puts account.summary
47
+ puts
48
+
49
+ puts "=== Generating a method that complements existing methods ==="
50
+ account._(
51
+ "an instance method named 'withdraw' that accepts an amount, " \
52
+ "raises a RuntimeError with 'Insufficient funds' if amount > @balance, " \
53
+ "otherwise decreases @balance by amount and returns the new balance"
54
+ )
55
+
56
+ puts "Balance before withdraw: $#{account.balance}"
57
+ puts "Withdrawing $200..."
58
+ puts "Balance after withdraw: $#{account.withdraw(200)}"
59
+
60
+ begin
61
+ account.withdraw(999_999)
62
+ rescue RuntimeError => e
63
+ puts "Caught expected error: #{e.message}"
64
+ end
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # 09_method_override.rb — Overriding existing methods
5
+ #
6
+ # Demonstrates:
7
+ # - Defining a class with an existing method
8
+ # - Generating a method with the same name overrides the original
9
+ # - Prepend-based MRO means the generated method takes priority
10
+ # - _source_for returns the new (generated) source
11
+ #
12
+ # Requires a running Ollama instance with the configured model.
13
+
14
+ require_relative "lib/setup"
15
+
16
+ class Formatter
17
+ include SelfAgency
18
+
19
+ def greet(name)
20
+ "Hello, #{name}"
21
+ end
22
+ end
23
+
24
+ fmt = Formatter.new
25
+
26
+ puts "=== Before override ==="
27
+ puts "fmt.greet('World') = #{fmt.greet('World')}"
28
+ puts
29
+ puts "Source (from file):"
30
+ puts fmt._source_for(:greet)
31
+ puts
32
+
33
+ puts "=== Generating a replacement method with the same name ==="
34
+ fmt._(
35
+ "an instance method named 'greet' that accepts a name parameter " \
36
+ "and returns the string 'Greetings, <name>! Welcome aboard.' " \
37
+ "(this should override the existing greet method)"
38
+ )
39
+
40
+ puts
41
+ puts "=== After override ==="
42
+ puts "fmt.greet('World') = #{fmt.greet('World')}"
43
+ puts
44
+ puts "Source (now LLM-generated):"
45
+ puts fmt._source_for(:greet)
46
+ puts
47
+
48
+ puts "=== MRO shows prepended module ==="
49
+ puts "Formatter ancestors:"
50
+ Formatter.ancestors.first(5).each_with_index do |ancestor, i|
51
+ puts " #{i}. #{ancestor}"
52
+ end
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # 10_full_workflow.rb — Complete real-world workflow
5
+ #
6
+ # Demonstrates:
7
+ # - Building a domain class with multiple generated methods
8
+ # - Using different scopes (instance, singleton, class)
9
+ # - Lifecycle hook to save generated code to files
10
+ # - Source inspection for all generated methods
11
+ # - End-to-end usage combining all features
12
+ #
13
+ # Requires a running Ollama instance with the configured model.
14
+
15
+ require "fileutils"
16
+ require_relative "lib/setup"
17
+
18
+ # Output directory for persisted generated code
19
+ GENERATED_DIR = File.join(__dir__, "generated")
20
+ FileUtils.mkdir_p(GENERATED_DIR)
21
+
22
+ class StatisticsCalculator
23
+ include SelfAgency
24
+
25
+ attr_reader :data
26
+
27
+ def initialize(data = [])
28
+ @data = data.dup
29
+ end
30
+
31
+ def on_method_generated(method_name, scope, code)
32
+ filename = "#{method_name}_#{scope}.rb"
33
+ filepath = File.join(GENERATED_DIR, filename)
34
+ File.write(filepath, code)
35
+ puts " [saved] #{filepath}"
36
+ end
37
+ end
38
+
39
+ puts "=== Building a StatisticsCalculator ==="
40
+ calc = StatisticsCalculator.new([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
41
+ puts "Data: #{calc.data.inspect}"
42
+ puts
43
+
44
+ # --- Instance methods ---
45
+ puts "--- Generating instance methods ---"
46
+
47
+ calc._(
48
+ "an instance method named 'mean' that calculates and returns the " \
49
+ "arithmetic mean of the @data array as a Float"
50
+ )
51
+
52
+ calc._(
53
+ "an instance method named 'median' that returns the median value " \
54
+ "of the @data array (sort the array, return the middle element for " \
55
+ "odd length, or average of two middle elements for even length) as a Float"
56
+ )
57
+
58
+ calc._(
59
+ "an instance method named 'standard_deviation' that calculates and " \
60
+ "returns the population standard deviation of @data as a Float " \
61
+ "(square root of the mean of squared differences from the mean)"
62
+ )
63
+ puts
64
+
65
+ # --- Class method ---
66
+ puts "--- Generating a class method ---"
67
+ calc._(
68
+ "a class method named 'self.from_range' that accepts two integers " \
69
+ "(low, high) and returns a new StatisticsCalculator instance " \
70
+ "initialized with (low..high).to_a",
71
+ scope: :class
72
+ )
73
+ puts
74
+
75
+ # --- Singleton method ---
76
+ puts "--- Generating a singleton method ---"
77
+ calc._(
78
+ "an instance method named 'report' that returns a multi-line string " \
79
+ "with the data count (data.length), mean (call self.mean), " \
80
+ "median (call self.median), and standard_deviation (call self.standard_deviation)",
81
+ scope: :singleton
82
+ )
83
+ puts
84
+
85
+ # --- Use the generated methods ---
86
+ puts "=== Using generated methods ==="
87
+ puts "Mean: #{calc.mean}"
88
+ puts "Median: #{calc.median}"
89
+ puts "Standard deviation: #{calc.standard_deviation}"
90
+ puts
91
+
92
+ puts "=== Class method ==="
93
+ range_calc = StatisticsCalculator.from_range(1, 5)
94
+ puts "StatisticsCalculator.from_range(1, 5).data = #{range_calc.data.inspect}"
95
+ puts "Mean of 1..5: #{range_calc.mean}"
96
+ puts
97
+
98
+ puts "=== Singleton report (only on this instance) ==="
99
+ puts calc.report
100
+ puts
101
+
102
+ other = StatisticsCalculator.new([1, 2, 3])
103
+ puts "other.respond_to?(:report) = #{other.respond_to?(:report)} (singleton — not available)"
104
+ puts
105
+
106
+ # --- Inspect source ---
107
+ puts "=== Source inspection ==="
108
+ [:mean, :median, :standard_deviation].each do |name|
109
+ puts "--- #{name} ---"
110
+ puts calc._source_for(name)
111
+ puts
112
+ end
113
+
114
+ # --- List saved files ---
115
+ puts "=== Saved generated code ==="
116
+ Dir.glob(File.join(GENERATED_DIR, "*.rb")).sort.each do |path|
117
+ puts " #{path}"
118
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "robot"
4
+
5
+ class Atlas < Robot
6
+ # Method named 'generate_weather_data' that takes no parameters. It must return an Array of 24 Hashes (one per hour, index 0..23). Each Hash has these keys (all Symbols): :hour => the integer hour (0..23), :temperature => a Float computed as 20.0 + 8.0 * Math.sin((hour - 6) * Math::PI / 12.0), :humidity => a Float computed as 60.0 + 20.0 * Math.cos((hour - 14) * Math::PI / 12.0), :wind_speed => a Float computed as 10.0 + 5.0 * Math.sin((hour * 7) * Math::PI / 24.0). Do NOT use random numbers. Use only the deterministic formulas above.
7
+ def generate_weather_data
8
+ weather_data = []
9
+ (0..23).each do |hour|
10
+ temperature = 20.0 + 8.0 * Math.sin(((hour - 6) * Math::PI) / 12.0)
11
+ humidity = 60.0 + 20.0 * Math.cos(((hour - 14) * Math::PI) / 12.0)
12
+ wind_speed = 10.0 + 5.0 * Math.sin(((hour * 7) * Math::PI) / 24.0)
13
+ weather_data << {
14
+ hour: hour,
15
+ temperature: temperature,
16
+ humidity: humidity,
17
+ wind_speed: wind_speed
18
+ }
19
+ end
20
+ weather_data
21
+ end
22
+
23
+ # Method named 'summarize_raw_data' that takes one parameter (data), an Array of Hashes as described above. It returns a Hash with: :readings_count => data.size, :raw_data => data, :source => "Atlas"
24
+ def summarize_raw_data(data)
25
+ {
26
+ readings_count: data.size,
27
+ raw_data: data,
28
+ source: "Atlas"
29
+ }
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "robot"
4
+
5
+ class Echo < Robot
6
+ # Return a formatted multi-line String report like: "=== Weather Report ===\n" + "Source: #{data[:source]} | Readings: #{data[:readings]}\n" + "Temperature: #{data[:avg_temp]}° (min: #{data[:min_temp]}°, max: #{data[:max_temp]}°) [#{data[:temperature_class]}]\n" + "Humidity: #{data[:avg_humidity]}% [#{data[:humidity_class]}]\n" + "Wind: #{data[:avg_wind]} km/h [#{data[:wind_class]}]\n" + "======================". Use string interpolation. Return the String, do not print it.
7
+ def weather_report
8
+ data = {
9
+ source: "Weather Station 42",
10
+ readings: "24 hr",
11
+ avg_temp: 22.5,
12
+ min_temp: 18.2,
13
+ max_temp: 28.7,
14
+ temperature_class: "Mild",
15
+ avg_humidity: 65,
16
+ humidity_class: "Moderate",
17
+ avg_wind: 12.3,
18
+ wind_class: "Light Breeze"
19
+ }
20
+
21
+ report = "=== Weather Report ===\n"
22
+ report += "Source: #{data[:source]} | Readings: #{data[:readings]}\n"
23
+ report += "Temperature: #{data[:avg_temp]}° (min: #{data[:min_temp]}°, max: #{data[:max_temp]}°) [#{data[:temperature_class]}]\n"
24
+ report += "Humidity: #{data[:avg_humidity]}% [#{data[:humidity_class]}]\n"
25
+ report += "Wind: #{data[:avg_wind]} km/h [#{data[:wind_class]}]\n"
26
+ report += "======================"
27
+
28
+ report
29
+ end
30
+ end