assistant 0.1.0 → 1.0.0.rc1

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +39 -0
  3. data/.github/workflows/ci.yml +99 -0
  4. data/.github/workflows/docs.yml +64 -0
  5. data/.github/workflows/release.yml +1 -1
  6. data/.gitignore +5 -1
  7. data/.opencode/.gitignore +4 -0
  8. data/.opencode/opencode.json +13 -0
  9. data/.opencode/skills/create-pr/SKILL.md +138 -0
  10. data/.opencode/skills/ruby-services/SKILL.md +81 -0
  11. data/.rubocop.yml +14 -4
  12. data/.yardopts +17 -0
  13. data/CHANGELOG.md +378 -0
  14. data/CONTRIBUTING.md +131 -0
  15. data/Gemfile +10 -0
  16. data/Gemfile.lock +196 -29
  17. data/README.md +125 -16
  18. data/Rakefile +45 -0
  19. data/SECURITY.md +50 -0
  20. data/Steepfile +49 -0
  21. data/_config.yml +87 -0
  22. data/assistant.gemspec +24 -7
  23. data/docs/api-reference.md +264 -0
  24. data/docs/changelog.md +26 -0
  25. data/docs/deprecations.md +86 -0
  26. data/docs/examples/cli-handler.md +17 -0
  27. data/docs/examples/composing-services.md +17 -0
  28. data/docs/examples/execute-callbacks.md +17 -0
  29. data/docs/examples/index.md +29 -0
  30. data/docs/examples/instrumentation-notifier.md +17 -0
  31. data/docs/examples/rails-service.md +17 -0
  32. data/docs/examples/rbs-generator.md +17 -0
  33. data/docs/examples/sidekiq-worker.md +17 -0
  34. data/docs/getting-started.md +136 -0
  35. data/docs/guides/composing-services.md +222 -0
  36. data/docs/guides/index.md +25 -0
  37. data/docs/guides/inputs.md +333 -0
  38. data/docs/guides/logging-and-results.md +202 -0
  39. data/docs/guides/rbs-and-types.md +16 -0
  40. data/docs/guides/validation.md +180 -0
  41. data/docs/index.md +69 -0
  42. data/docs/roadmap.md +33 -0
  43. data/exe/assistant-rbs +7 -0
  44. data/lib/assistant/execute_callbacks.rb +103 -0
  45. data/lib/assistant/execute_callbacks.rbs +30 -0
  46. data/lib/assistant/input_builder/accessors.rb +36 -0
  47. data/lib/assistant/input_builder/accessors.rbs +10 -0
  48. data/lib/assistant/input_builder/default_option.rb +41 -0
  49. data/lib/assistant/input_builder/default_option.rbs +11 -0
  50. data/lib/assistant/input_builder/dsl.rb +37 -0
  51. data/lib/assistant/input_builder/dsl.rbs +12 -0
  52. data/lib/assistant/input_builder/optional_option.rb +45 -0
  53. data/lib/assistant/input_builder/optional_option.rbs +10 -0
  54. data/lib/assistant/input_builder/registry.rb +27 -0
  55. data/lib/assistant/input_builder/registry.rbs +13 -0
  56. data/lib/assistant/input_builder/require_validator.rb +104 -0
  57. data/lib/assistant/input_builder/require_validator.rbs +24 -0
  58. data/lib/assistant/input_builder/type_validator.rb +47 -0
  59. data/lib/assistant/input_builder/type_validator.rbs +18 -0
  60. data/lib/assistant/input_builder.rb +25 -81
  61. data/lib/assistant/input_builder.rbs +15 -0
  62. data/lib/assistant/log_item.rb +74 -16
  63. data/lib/assistant/log_item.rbs +40 -0
  64. data/lib/assistant/log_list.rb +43 -17
  65. data/lib/assistant/log_list.rbs +48 -0
  66. data/lib/assistant/rbs_generator/cli.rb +109 -0
  67. data/lib/assistant/rbs_generator/cli.rbs +24 -0
  68. data/lib/assistant/rbs_generator/renderer.rb +67 -0
  69. data/lib/assistant/rbs_generator/renderer.rbs +11 -0
  70. data/lib/assistant/rbs_generator/writer.rb +65 -0
  71. data/lib/assistant/rbs_generator/writer.rbs +24 -0
  72. data/lib/assistant/rbs_generator.rb +38 -0
  73. data/lib/assistant/rbs_generator.rbs +5 -0
  74. data/lib/assistant/refinements/string_blankness.rb +9 -13
  75. data/lib/assistant/refinements/string_blankness.rbs +6 -0
  76. data/lib/assistant/service.rb +300 -11
  77. data/lib/assistant/service.rbs +82 -1
  78. data/lib/assistant/version.rb +5 -1
  79. data/lib/assistant/version.rbs +5 -0
  80. data/lib/assistant.rb +54 -4
  81. data/lib/assistant.rbs +25 -0
  82. data/mise.toml +2 -0
  83. data/sig/examples/greeter.rbs +14 -0
  84. metadata +142 -38
  85. data/.fasterer.yml +0 -19
  86. data/.rubocop_todo.yml +0 -7
@@ -0,0 +1,13 @@
1
+ # Type signatures for `lib/assistant/input_builder/registry.rb`.
2
+
3
+ module Assistant::InputBuilder::Registry
4
+ @input_definitions: Hash[Symbol, Hash[Symbol, untyped]]
5
+
6
+ def input_definitions: () -> Hash[Symbol, Hash[Symbol, untyped]]
7
+
8
+ def register_input_definition: (
9
+ name: Symbol,
10
+ type: untyped,
11
+ options: Hash[Symbol, untyped]
12
+ ) -> Hash[Symbol, untyped]
13
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generators for the per-input requirement validators. Canonical names
4
+ # (M9):
5
+ #
6
+ # #valid_required_<name>? # for `required: true`
7
+ # #valid_required_conditional_<name>? # for `required: true, if: ...`
8
+ #
9
+ # Pre-M9 names (`valid_require_*?`, `valid_require_conditional_*?`) are
10
+ # kept as deprecated aliases that delegate to the canonical method and
11
+ # emit a one-shot `Kernel.warn` per call site. They are scheduled for
12
+ # removal in `assistant 2.0`.
13
+ module Assistant::InputBuilder::RequireValidator
14
+ # Guard so each deprecated alias warns at most once per textual call
15
+ # site (file + lineno), regardless of how many times it is invoked or
16
+ # how many input names are involved. Internal; tests reset it via
17
+ # `.send(:__reset_deprecation_warnings__)`.
18
+ DEPRECATION_WARNED = Set.new
19
+ private_constant :DEPRECATION_WARNED
20
+
21
+ # Emit the M9 one-shot deprecation warning for a `valid_require_*?`
22
+ # call. Deduped per `[canonical, caller path, caller lineno]` so the
23
+ # same call site never warns twice in one process.
24
+ #
25
+ # @param deprecated_name [Symbol] e.g. `:valid_require_name?`
26
+ # @param canonical_name [Symbol] e.g. `:valid_required_name?`
27
+ # @param caller_location [Thread::Backtrace::Location]
28
+ # @return [void]
29
+ def self.warn_deprecated(deprecated_name, canonical_name, caller_location)
30
+ key = [canonical_name, caller_location.path, caller_location.lineno]
31
+ return if DEPRECATION_WARNED.include?(key)
32
+
33
+ DEPRECATION_WARNED << key
34
+ Kernel.warn("assistant: `##{deprecated_name}` is deprecated; use `##{canonical_name}` (removed in assistant 2.0)")
35
+ end
36
+
37
+ # Test-only hook: clears the per-call-site dedupe set so a single
38
+ # test process can exercise multiple "first warn" scenarios.
39
+ #
40
+ # @return [void]
41
+ def self.__reset_deprecation_warnings__
42
+ DEPRECATION_WARNED.clear
43
+ end
44
+
45
+ # Define `#valid_required_<name>?` on the host class plus the
46
+ # deprecated `#valid_require_<name>?` alias.
47
+ #
48
+ # @param name [Symbol] input name
49
+ # @return [void]
50
+ def input_require_validator_meth(name:, **)
51
+ canonical = :"valid_required_#{name}?"
52
+ define_required_validator(canonical:, name:, **)
53
+ define_deprecated_alias(:"valid_require_#{name}?", canonical)
54
+ end
55
+
56
+ # Define `#valid_required_conditional_<name>?` on the host class
57
+ # plus the deprecated `#valid_require_conditional_<name>?` alias.
58
+ #
59
+ # @param name [Symbol] input name
60
+ # @return [void]
61
+ def input_require_conditional_meth(name:, **)
62
+ canonical = :"valid_required_conditional_#{name}?"
63
+ define_required_conditional_validator(canonical:, name:, **)
64
+ define_deprecated_alias(:"valid_require_conditional_#{name}?", canonical)
65
+ end
66
+
67
+ private
68
+
69
+ def define_required_validator(canonical:, name:, **options)
70
+ allow_nil = options.fetch(:allow_nil, false) == true
71
+
72
+ define_method(canonical) do |log = true|
73
+ # M2: explicit nil counts as "supplied" when allow_nil: true is set.
74
+ return true if allow_nil && @inputs.key?(name)
75
+ return true if options[:required] == true && send("#{name}?") == true
76
+
77
+ log && send(
78
+ :log_item_error_initialize, attr_name: name, message: "Service is missing argument with name #{name}"
79
+ )
80
+ false
81
+ end
82
+ end
83
+
84
+ def define_required_conditional_validator(canonical:, name:, **options)
85
+ define_method(canonical) do
86
+ return false if send(:"valid_required_#{name}?", false) == false
87
+ return true if options[:if].call(send(name))
88
+
89
+ send(
90
+ :log_item_error_initialize,
91
+ attr_name: name,
92
+ message: "Service argument conditional requirement not met properly for #{name}"
93
+ )
94
+ false
95
+ end
96
+ end
97
+
98
+ def define_deprecated_alias(deprecated, canonical)
99
+ define_method(deprecated) do |*args, **kwargs|
100
+ ::Assistant::InputBuilder::RequireValidator.warn_deprecated(deprecated, canonical, caller_locations(1, 1).first)
101
+ send(canonical, *args, **kwargs)
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,24 @@
1
+ # Type signatures for `lib/assistant/input_builder/require_validator.rb`
2
+ # (M9). Generates the canonical `valid_required_*?` and
3
+ # `valid_required_conditional_*?` instance methods plus deprecated
4
+ # `valid_require_*?` aliases.
5
+
6
+ module Assistant::InputBuilder::RequireValidator
7
+ DEPRECATION_WARNED: Set[Array[untyped]]
8
+
9
+ def self.warn_deprecated: (Symbol deprecated_name, Symbol canonical_name, Thread::Backtrace::Location caller_location) -> void
10
+
11
+ def self.__reset_deprecation_warnings__: () -> void
12
+
13
+ def input_require_validator_meth: (name: Symbol, **untyped) -> Symbol
14
+
15
+ def input_require_conditional_meth: (name: Symbol, **untyped) -> Symbol
16
+
17
+ private
18
+
19
+ def define_required_validator: (canonical: Symbol, name: Symbol, **untyped) -> Symbol
20
+
21
+ def define_required_conditional_validator: (canonical: Symbol, name: Symbol, **untyped) -> Symbol
22
+
23
+ def define_deprecated_alias: (Symbol deprecated, Symbol canonical) -> Symbol
24
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generators for the per-input `valid_type_<name>?` validator (M3:
4
+ # multi-type accepted; M2: `allow_nil:` short-circuits the check).
5
+ module Assistant::InputBuilder::TypeValidator
6
+ # Define `#valid_type_<name>?` on the host class.
7
+ #
8
+ # @param name [Symbol] input name
9
+ # @param type [Class, Array<Class>] declared type(s); `Array(type)` is unioned
10
+ # @param options [Hash] remaining `#input` keyword options (reads `:allow_nil`)
11
+ # @return [void]
12
+ def input_type_validator_meth(name:, type:, **options)
13
+ allow_nil = options.fetch(:allow_nil, false) == true
14
+ types = Array(type)
15
+ message_builder = type_mismatch_message_builder(name:, types:)
16
+ body = type_validator_body(name:, types:, allow_nil:, message_builder:)
17
+
18
+ define_method("valid_type_#{name}?", &body)
19
+ end
20
+
21
+ # Builds the Proc body for the per-input valid_type_<name>? method.
22
+ # Extracted to keep input_type_validator_meth under metric limits.
23
+ def type_validator_body(name:, types:, allow_nil:, message_builder:)
24
+ lambda do
25
+ value = @inputs[name]
26
+ # M2: when allow_nil: true is set, any supplied key short-circuits
27
+ # the type check (mirrors the require validator's behaviour).
28
+ next true if allow_nil && @inputs.key?(name)
29
+ next true if types.any? { |klass| value.is_a?(klass) }
30
+
31
+ send("#{name}?") &&
32
+ send(:log_item_error_initialize, attr_name: name, message: message_builder.call(send(name).class))
33
+ false
34
+ end
35
+ end
36
+
37
+ # Returns a Proc that, given the actual class of a failing input,
38
+ # produces the error message. Single-type keeps the original 0.1.0
39
+ # wording for back-compat; multi-type uses "is not one of […]". (M3)
40
+ def type_mismatch_message_builder(name:, types:)
41
+ if types.size == 1
42
+ ->(actual) { "Service argument with name #{name} is not a #{types.first} but #{actual}" }
43
+ else
44
+ ->(actual) { "Service argument with name #{name} is not one of [#{types.join(', ')}] but #{actual}" }
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,18 @@
1
+ # Type signatures for `lib/assistant/input_builder/type_validator.rb`
2
+ # (M3 multi-type; M2 `allow_nil:`).
3
+
4
+ module Assistant::InputBuilder::TypeValidator
5
+ def input_type_validator_meth: (name: Symbol, type: untyped, **untyped) -> Symbol
6
+
7
+ def type_validator_body: (
8
+ name: Symbol,
9
+ types: Array[untyped],
10
+ allow_nil: bool,
11
+ message_builder: ^(Class) -> String
12
+ ) -> ^() -> bool
13
+
14
+ def type_mismatch_message_builder: (
15
+ name: Symbol,
16
+ types: Array[untyped]
17
+ ) -> ^(Class) -> String
18
+ end
@@ -1,84 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'assistant/refinements/string_blankness'
4
-
5
- module Assistant
6
- # This module has the building blocks for the input validation.
7
- # The building blocks of listing inputs with the #input and #inputs methods
8
- # and the building blocks of validating inputs with the methods that are called within those methods.
9
- module InputBuilder
10
- using Assistant::Refinements::StringBlankness
11
-
12
- # Lists all inputs that have the same type and options.
13
- def inputs(attr_names, type:, **)
14
- attr_names.each do |attr_name|
15
- input(attr_name, type:, **)
16
- end
17
- end
18
-
19
- # Individual input with a specific type or options
20
- def input(attr_name, type:, **options)
21
- # Base Methods
22
- input_getter_meth(attr_name)
23
- input_checker_meth(attr_name)
24
-
25
- # Input type validation method, simple and conditional requirement validation methods
26
- input_type_validator_meth(attr_name, type)
27
- input_require_validator_meth(attr_name, **options) if options[:required] == true
28
- input_require_conditional_meth(attr_name, **options) if options[:required] == true && options[:if]
29
- end
30
-
31
- def input_getter_meth(attr_name)
32
- define_method(attr_name) do
33
- @inputs[attr_name]
34
- end
35
- end
36
-
37
- def input_checker_meth(attr_name)
38
- define_method("#{attr_name}?") do
39
- val = @inputs[attr_name]
40
- return false if val.nil? || val == false
41
- return !val.whitespace? if val.is_a?(String)
42
-
43
- val.respond_to?(:empty?) ? !val.empty? : true
44
- end
45
- end
46
-
47
- def input_require_validator_meth(attr_name, **options)
48
- define_method("valid_require_#{attr_name}?") do |log = true|
49
- return true if options[:required] == true && send("#{attr_name}?") == true
50
-
51
- log && send(
52
- :log_item_error_initialize, attr_name:, message: "Service is missing argument with name #{attr_name}"
53
- )
54
- false
55
- end
56
- end
57
-
58
- def input_require_conditional_meth(attr_name, **options)
59
- define_method("valid_require_conditional_#{attr_name}?") do
60
- return false if send("valid_require_#{attr_name}?", false) == false
61
- return true if options[:if].call(send(attr_name))
62
-
63
- send(
64
- :log_item_error_initialize,
65
- attr_name:, message: "Service argument conditional requirement not met properly for #{attr_name}"
66
- )
67
- false
68
- end
69
- end
70
-
71
- def input_type_validator_meth(attr_name, type)
72
- define_method("valid_type_#{attr_name}?") do
73
- return true if @inputs[attr_name].is_a?(type)
74
-
75
- send("#{attr_name}?") &&
76
- send(
77
- :log_item_error_initialize,
78
- attr_name:, message: "Service argument with name #{attr_name} is not a #{type} but #{send(attr_name).class}"
79
- )
80
- false
81
- end
82
- end
83
- end
3
+ # Umbrella for the input DSL. Each submodule owns one cohesive
4
+ # responsibility; this file wires them into a single `Assistant::InputBuilder`
5
+ # module that `Service` extends. (M13, v1 plan)
6
+ require 'assistant/input_builder/accessors'
7
+ require 'assistant/input_builder/default_option'
8
+ require 'assistant/input_builder/dsl'
9
+ require 'assistant/input_builder/optional_option'
10
+ require 'assistant/input_builder/registry'
11
+ require 'assistant/input_builder/require_validator'
12
+ require 'assistant/input_builder/type_validator'
13
+
14
+ # Declarative input DSL for `Assistant::Service` subclasses. `#input`
15
+ # registers a definition and generates the per-input reader, `?`-checker,
16
+ # type validator, and (when `required:` is set) requirement validator(s).
17
+ # Behaviour is unchanged from pre-M13; the umbrella only re-exports the
18
+ # submodule methods. See the per-submodule files for the specific concern
19
+ # each owns.
20
+ module Assistant::InputBuilder
21
+ include Registry
22
+ include DefaultOption
23
+ include OptionalOption
24
+ include Accessors
25
+ include RequireValidator
26
+ include TypeValidator
27
+ include Dsl
84
28
  end
@@ -0,0 +1,15 @@
1
+ # Type signatures for `lib/assistant/input_builder.rb` — the umbrella
2
+ # module that includes every per-concern submodule. The submodule
3
+ # signatures live alongside the corresponding `.rb` files under
4
+ # `lib/assistant/input_builder/`. See docs/v1/01-api-surface.md and
5
+ # docs/v1/02-features.md (M13).
6
+
7
+ module Assistant::InputBuilder
8
+ include Registry
9
+ include DefaultOption
10
+ include OptionalOption
11
+ include Accessors
12
+ include RequireValidator
13
+ include TypeValidator
14
+ include Dsl
15
+ end
@@ -1,24 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Assistant
4
- # Log base class
4
+ # A single structured log entry produced by `Service` (directly via
5
+ # `LogList#add_log` or through one of the `log_item_*` shorthands).
6
+ # Construction is **strict** since 1.0 (M10): invalid attributes raise
7
+ # `ArgumentError` rather than producing an instance whose `#valid?`
8
+ # returns `false`. See `docs/v1/06-migration-0x-to-1.md` for the
9
+ # rationale.
10
+ #
11
+ # @example Build an error log entry
12
+ # Assistant::LogItem.new(
13
+ # level: :error,
14
+ # source: :create_user,
15
+ # detail: :email,
16
+ # message: 'must not be blank'
17
+ # )
5
18
  class LogItem
19
+ # The exhaustive set of accepted `level:` values, in display order.
20
+ # @return [Array<Symbol>]
6
21
  VALID_LEVELS = %i[info warning error].freeze
7
22
 
23
+ # Validation rules applied in `#initialize`. Each entry pairs a
24
+ # human message with the predicate that must hold. Internal.
25
+ # @api private
26
+ ERRORS = [
27
+ ["level must be one of [#{VALID_LEVELS.join(', ')}]", :valid_level?],
28
+ ['source must be present and different from detail', :valid_source?],
29
+ ['detail must be present and different from source', :valid_detail?],
30
+ ['message must be present', :valid_message?]
31
+ ].freeze
32
+
33
+ # @return [Symbol] severity (`:info`, `:warning`, or `:error`)
34
+ # @!attribute [r] level
35
+ # @return [Symbol] high-level subsystem the entry came from (e.g. `:initialize`, `:hook`)
36
+ # @!attribute [r] source
37
+ # @return [Symbol] finer-grained detail under `source` (often an attribute name)
38
+ # @!attribute [r] detail
39
+ # @return [String] human-readable message
40
+ # @!attribute [r] message
41
+ # @return [Array<String>, nil] optional backtrace captured at construction
42
+ # @!attribute [r] trace
8
43
  attr_reader :level, :source, :detail, :message, :trace
9
44
 
45
+ # @param level [Symbol, #to_sym] one of {VALID_LEVELS}
46
+ # @param source [Symbol, #to_sym] subsystem identifier; must differ from `detail`
47
+ # @param detail [Symbol, #to_sym] sub-identifier; must differ from `source`
48
+ # @param message [#to_s] human-readable message; must not be blank
49
+ # @param trace [Array<String>, nil] optional backtrace
50
+ # @raise [ArgumentError] when any of the constructor checks in {ERRORS} fail
10
51
  def initialize(level:, source:, detail:, message:, trace: nil)
11
- @level = level.to_sym
12
- @source = source.to_sym
13
- @detail = detail.to_sym
52
+ @level = normalize_symbol(level)
53
+ @source = normalize_symbol(source)
54
+ @detail = normalize_symbol(detail)
14
55
  @message = message.to_s
15
56
  @trace = trace
57
+ validate!
16
58
  end
17
59
 
18
- def valid?
19
- [valid_level?, valid_source?, valid_detail?, valid_message?].all?
20
- end
60
+ # @return [Boolean] always `true` for instances constructed via {#initialize} (which raises on invalid input).
61
+ # Retained for introspection and downstream tooling.
62
+ def valid? = [valid_level?, valid_source?, valid_detail?, valid_message?].all?
21
63
 
64
+ # @return [Hash{Symbol => Object}] hash view with keys `:level`, `:source`, `:detail`, `:message`, `:trace`
22
65
  def item
23
66
  { level:, source:, detail:, message:, trace: }
24
67
  end
@@ -30,20 +73,35 @@ module Assistant
30
73
  end
31
74
  end
32
75
 
33
- def valid_level?
34
- VALID_LEVELS.include?(level)
35
- end
76
+ # @return [Boolean] true when `level` is one of {VALID_LEVELS}
77
+ def valid_level? = VALID_LEVELS.include?(level)
78
+
79
+ # @return [Boolean] true when `source` is non-empty and not equal to `detail`
80
+ def valid_source? = present_log_attribute?(source) && detail != source
81
+
82
+ # @return [Boolean] true when `detail` is non-empty and not equal to `source`
83
+ def valid_detail? = present_log_attribute?(detail) && source != detail
36
84
 
37
- def valid_source?
38
- source.size.positive? && detail != source
85
+ # @return [Boolean] true when `message` contains at least one non-whitespace character
86
+ def valid_message? = !message.strip.empty?
87
+
88
+ private
89
+
90
+ def validate!
91
+ errors = validation_errors
92
+ return if errors.empty?
93
+
94
+ raise ArgumentError, "invalid LogItem: #{errors.join('; ')}"
39
95
  end
40
96
 
41
- def valid_detail?
42
- detail.size.positive? && source != detail
97
+ def validation_errors = ERRORS.reject { |_, validation_method| send(validation_method) }.map(&:first)
98
+
99
+ def normalize_symbol(value)
100
+ value.respond_to?(:to_sym) ? value.to_sym : value
43
101
  end
44
102
 
45
- def valid_message?
46
- message.size.positive?
103
+ def present_log_attribute?(value)
104
+ value.respond_to?(:size) && value.size.positive?
47
105
  end
48
106
  end
49
107
  end
@@ -0,0 +1,40 @@
1
+ # Type signatures for `lib/assistant/log_item.rb`. See
2
+ # docs/v1/01-api-surface.md for the frozen surface.
3
+
4
+ class Assistant::LogItem
5
+ VALID_LEVELS: Array[Symbol]
6
+ ERRORS: Array[[String, Symbol]]
7
+
8
+ attr_reader level: Symbol
9
+ attr_reader source: Symbol
10
+ attr_reader detail: Symbol
11
+ attr_reader message: String
12
+ attr_reader trace: untyped
13
+
14
+ def initialize: (
15
+ detail: Symbol | String,
16
+ message: String | _ToS,
17
+ level: Symbol | String,
18
+ source: Symbol | String,
19
+ ?trace: untyped
20
+ ) -> void
21
+
22
+ def valid?: () -> bool
23
+ def valid_level?: () -> bool
24
+ def valid_source?: () -> bool
25
+ def valid_detail?: () -> bool
26
+ def valid_message?: () -> bool
27
+
28
+ def info?: () -> bool
29
+ def warning?: () -> bool
30
+ def error?: () -> bool
31
+
32
+ def item: () -> Hash[Symbol, untyped]
33
+
34
+ private
35
+
36
+ def validate!: () -> void
37
+ def validation_errors: () -> Array[String]
38
+ def normalize_symbol: (untyped value) -> untyped
39
+ def present_log_attribute?: (untyped value) -> bool
40
+ end
@@ -1,26 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Assistant
4
- # Service level list of logs
5
- module LogList
6
- def add_log(level:, source:, detail:, message:, trace: nil)
7
- @logs << Assistant::LogItem.new(level:, source:, detail:, message:, trace:)
8
- end
3
+ # Mixin that gives `Assistant::Service` its log timeline. Owns the
4
+ # `@logs` array and exposes the public helpers (`#add_log`, `#merge_logs`,
5
+ # the `log_item_*` shorthands, `#infos` / `#warnings` / `#errors`) used
6
+ # by service code to record observations and by callers to read the
7
+ # result.
8
+ module Assistant::LogList
9
+ # Append a single {Assistant::LogItem} to the timeline.
10
+ #
11
+ # @param level [Symbol] `:info`, `:warning`, or `:error`
12
+ # @param source [Symbol] subsystem identifier
13
+ # @param detail [Symbol] sub-identifier (often an attribute name)
14
+ # @param message [String] human-readable message
15
+ # @param trace [Array<String>, nil] optional backtrace
16
+ # @return [Array<Assistant::LogItem>] the updated timeline
17
+ # @raise [ArgumentError] if {Assistant::LogItem#initialize} rejects the inputs
18
+ def add_log(level:, source:, detail:, message:, trace: nil)
19
+ @logs << Assistant::LogItem.new(level:, source:, detail:, message:, trace:)
20
+ end
9
21
 
10
- def merge_logs(other_logs)
11
- @logs.concat(other_logs)
12
- end
22
+ # Concatenate an existing log timeline onto this service's `@logs`,
23
+ # preserving insertion order. Used by `Service#call_service` (M-S2).
24
+ #
25
+ # @param logs [Array<Assistant::LogItem>] entries to append
26
+ # @return [Array<Assistant::LogItem>] the updated timeline
27
+ def merge_logs(logs:)
28
+ @logs.concat(logs)
29
+ end
30
+
31
+ # Convenience used by InputBuilder-generated validators to record an
32
+ # initialization-time error for a specific input attribute.
33
+ #
34
+ # @param attr_name [Symbol] the offending input name (recorded as `detail`)
35
+ # @param message [String] human-readable explanation
36
+ # @return [Array<Assistant::LogItem>] the updated timeline
37
+ def log_item_error_initialize(attr_name:, message:)
38
+ @logs << Assistant::LogItem.new(detail: attr_name, level: :error, message:, source: :initialize)
39
+ end
13
40
 
14
- # Convenience used by InputBuilder-generated validators to record an
15
- # initialization-time error for a specific input attribute.
16
- def log_item_error_initialize(attr_name:, message:)
17
- @logs << Assistant::LogItem.new(detail: attr_name, level: :error, message:, source: :initialize)
41
+ ::Assistant::LogItem::VALID_LEVELS.each do |level|
42
+ define_method("#{level}s") do
43
+ @logs.select { |log| log.send("#{level}?") }
18
44
  end
19
45
 
20
- ::Assistant::LogItem::VALID_LEVELS.each do |level|
21
- define_method("#{level}s") do
22
- @logs.select { |log| log.send("#{level}?") }
23
- end
46
+ # Shorthand: log_item_info / log_item_warning / log_item_error.
47
+ # See docs/v1/02-features.md M5.
48
+ define_method("log_item_#{level}") do |source:, detail:, message:, trace: nil|
49
+ add_log(level:, source:, detail:, message:, trace:)
24
50
  end
25
51
  end
26
52
  end
@@ -0,0 +1,48 @@
1
+ # Type signatures for `lib/assistant/log_list.rb`. See
2
+ # docs/v1/01-api-surface.md for the frozen surface.
3
+ #
4
+ # `Assistant::LogList` is mixed into `Assistant::Service`; the `@logs`
5
+ # instance variable it operates on is declared on `Service` itself.
6
+
7
+ module Assistant::LogList
8
+ def add_log: (
9
+ detail: Symbol | String,
10
+ level: Symbol | String,
11
+ message: String | _ToS,
12
+ source: Symbol | String,
13
+ ?trace: untyped
14
+ ) -> Array[Assistant::LogItem]
15
+
16
+ def merge_logs: (logs: Array[Assistant::LogItem]) -> Array[Assistant::LogItem]
17
+
18
+ def log_item_error_initialize: (
19
+ attr_name: Symbol | String,
20
+ message: String | _ToS
21
+ ) -> Array[Assistant::LogItem]
22
+
23
+ # M5 shorthands: `log_item_info`, `log_item_warning`, `log_item_error`.
24
+ def log_item_info: (
25
+ detail: Symbol | String,
26
+ message: String | _ToS,
27
+ source: Symbol | String,
28
+ ?trace: untyped
29
+ ) -> Array[Assistant::LogItem]
30
+
31
+ def log_item_warning: (
32
+ detail: Symbol | String,
33
+ message: String | _ToS,
34
+ source: Symbol | String,
35
+ ?trace: untyped
36
+ ) -> Array[Assistant::LogItem]
37
+
38
+ def log_item_error: (
39
+ detail: Symbol | String,
40
+ message: String | _ToS,
41
+ source: Symbol | String,
42
+ ?trace: untyped
43
+ ) -> Array[Assistant::LogItem]
44
+
45
+ def infos: () -> Array[Assistant::LogItem]
46
+ def warnings: () -> Array[Assistant::LogItem]
47
+ def errors: () -> Array[Assistant::LogItem]
48
+ end