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
@@ -1,61 +1,350 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'execute_callbacks'
3
4
  require_relative 'input_builder'
4
5
  require_relative 'log_list'
5
6
 
6
7
  module Assistant
7
- # Base class for the Assistant gem
8
- class Service
9
- include LogList
8
+ # Core Service base class. Subclasses declare inputs via the
9
+ # InputBuilder DSL, optionally register before/after/around execute
10
+ # hooks via ExecuteCallbacks, and implement `#execute`.
11
+ #
12
+ # @example Minimal service
13
+ # class Greet < Assistant::Service
14
+ # input :name, type: String, required: true
15
+ #
16
+ # def execute
17
+ # "Hello, #{name}!"
18
+ # end
19
+ # end
20
+ #
21
+ # Greet.run(name: 'Ada')
22
+ # # => { result: "Hello, Ada!", status: :ok, warnings: [] }
23
+ class Service # rubocop:disable Metrics/ClassLength
24
+ include Assistant::LogList
25
+
26
+ # Public reader for the full log timeline (info + warning + error), in
27
+ # insertion order. See docs/v1/02-features.md M4.
28
+ #
29
+ # @return [Array<Assistant::LogItem>]
30
+ attr_reader :logs
10
31
 
11
32
  class << self
12
- include InputBuilder
33
+ include Assistant::InputBuilder
34
+ include Assistant::ExecuteCallbacks
13
35
 
36
+ # Convenience: build a service with the given keyword arguments
37
+ # and immediately invoke `#run`, returning the result hash.
38
+ # Equivalent to `new(**inputs).run`.
39
+ #
40
+ # @return [Hash] the result payload — see {Service#run}
14
41
  def run(**)
15
42
  new(**).run
16
43
  end
44
+
45
+ # M-S4: per-subclass `Data` class whose members are the declared
46
+ # input names, in declaration order. Memoised on the subclass and
47
+ # transparently rebuilt if `input_definitions` changes (e.g. a
48
+ # late `input :foo` after the first snapshot call). Used by
49
+ # `Service#input_snapshot`; users normally never touch it.
50
+ #
51
+ # @return [Class] a `Data.define` subclass
52
+ def input_snapshot_class
53
+ keys = input_definitions.keys
54
+ return @input_snapshot_class if @input_snapshot_class && @input_snapshot_class_keys == keys
55
+
56
+ @input_snapshot_class_keys = keys
57
+ @input_snapshot_class = Data.define(*keys)
58
+ end
17
59
  end
18
60
 
61
+ # @param args [Hash] keyword arguments matching the declared inputs.
62
+ # Unknown keys are accepted but excluded from {#input_snapshot}.
19
63
  def initialize(**args)
20
64
  @inputs = args
65
+ apply_input_defaults
21
66
  @logs = []
22
67
  end
23
68
 
69
+ # Execute the validation + execute pipeline once and return the
70
+ # result payload. Idempotent: calling `#run` a second time returns
71
+ # an updated payload based on the memoised `#result`, but `#execute`
72
+ # itself runs only once (M-S1 hook chain is gated by {#result}'s
73
+ # `||=`).
74
+ #
75
+ # @return [Hash{Symbol => Object}] either
76
+ # - `{ result: Object, status: :ok | :with_warnings, warnings: Array<LogItem> }`
77
+ # on success, or
78
+ # - `{ errors: Array<LogItem>, result: nil, status: :with_errors }`
79
+ # when any error has been logged before or during validation.
24
80
  def run
81
+ @run_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+ notify(:service_started)
83
+
25
84
  validate_inputs
26
85
  validate
86
+ notify(:service_validated)
27
87
 
28
- if errors.empty?
29
- { result:, status:, warnings: }
30
- else
31
- { errors:, result: nil, status: :with_errors }
32
- end
88
+ return failed_payload if errors.any?
89
+
90
+ # Trigger `#execute` (through the M-S1 hook chain) eagerly so any
91
+ # error logged by a `before_/after_/around_execute` hook influences
92
+ # the terminal event and the payload's `:status` field.
93
+ result
94
+ errors.empty? ? executed_payload : failed_payload
33
95
  end
34
96
 
97
+ # Memoised return value of {#execute}, threaded through the
98
+ # registered before/around/after execute hooks (M-S1).
99
+ #
100
+ # @return [Object] whatever the subclass's `#execute` returned
35
101
  def result
36
- @result ||= execute
102
+ @result ||= run_execute_with_callbacks
37
103
  end
38
104
 
105
+ # @return [Boolean] true when no `:error` entries have been logged
39
106
  def success?
40
107
  errors.empty?
41
108
  end
42
109
 
110
+ # @return [Boolean] true when at least one `:error` entry has been logged
43
111
  def failure?
44
112
  errors.any?
45
113
  end
46
114
 
115
+ # Terminal status for the success path. Failure runs use
116
+ # `:with_errors` directly in the result payload (see {#run}).
117
+ #
118
+ # @return [Symbol] `:ok` when there are no warnings, otherwise `:with_warnings`
47
119
  def status
48
120
  warnings.empty? ? :ok : :with_warnings
49
121
  end
50
122
 
123
+ # M-S2: instantiate `klass`, run it, merge its log timeline into the
124
+ # current service, and return the inner service instance.
125
+ #
126
+ # The full log timeline of the inner service (info + warning +
127
+ # error) is concatenated onto the outer service's `@logs` via
128
+ # `merge_logs`. Because the outer service's `errors`, `warnings`,
129
+ # and `status` are derived by filtering `@logs`, an inner error
130
+ # automatically downgrades the outer terminal status to
131
+ # `:with_errors`, and inner warnings surface as `:with_warnings`
132
+ # when no errors are present — without any special handling in
133
+ # the caller.
134
+ #
135
+ # The returned inner instance exposes `#result`, `#success?`,
136
+ # `#failure?`, etc. so the caller can branch on the inner outcome:
137
+ #
138
+ # @example
139
+ # def execute
140
+ # other = call_service(OtherService, foo: 1)
141
+ # return if failure?
142
+ # other.result + 1
143
+ # end
144
+ #
145
+ # `call_service` does **not** rescue exceptions raised by the inner
146
+ # service's `#execute` or by `Assistant.notifier`; those propagate
147
+ # to the caller, matching the base `Service#run` contract. To run
148
+ # an inner service that may raise, wrap the call in a `begin/rescue`
149
+ # and use `add_log(level: :error, …)` to record the failure.
150
+ #
151
+ # @param klass [Class<Assistant::Service>] the inner service class
152
+ # @param inputs [Hash] keyword arguments forwarded to `klass.new`
153
+ # @return [Assistant::Service] the inner service instance, already run
154
+ # @raise [ArgumentError] if `klass` is not a subclass of {Assistant::Service}
155
+ def call_service(klass, **inputs)
156
+ unless klass.is_a?(Class) && klass <= Assistant::Service
157
+ raise ArgumentError, "call_service expects an Assistant::Service subclass, got #{klass.inspect}"
158
+ end
159
+
160
+ inner = klass.new(**inputs)
161
+ inner.run
162
+ merge_logs(logs: inner.logs)
163
+ inner
164
+ end
165
+
166
+ # M-S4: a read-only `Data` view over the declared inputs of this
167
+ # service, post-`default:` / post-`allow_nil:`. Members are the
168
+ # input names declared via `Service.input` / `Service.inputs`, in
169
+ # declaration order; values are read from `@inputs` after
170
+ # `apply_input_defaults` has run, so callers see the same values
171
+ # the per-input getters expose.
172
+ #
173
+ # @example
174
+ # class Greet < Assistant::Service
175
+ # input :name, type: String, required: true
176
+ # input :loud, type: TrueClass, default: false
177
+ # end
178
+ #
179
+ # Greet.new(name: 'Ada').input_snapshot
180
+ # # => #<data name="Ada", loud=false>
181
+ #
182
+ # The returned object is a `Data` instance, so it is structurally
183
+ # immutable: no member can be reassigned. Member values that are
184
+ # themselves mutable (e.g. an `Array` passed as an input) keep
185
+ # their normal mutability — the snapshot does not deep-freeze.
186
+ #
187
+ # Only declared inputs appear in the snapshot. Extra keyword
188
+ # arguments accepted by `#initialize` (which live in `@inputs`
189
+ # but have no `input :foo` declaration) are intentionally excluded
190
+ # so the snapshot's shape matches the public DSL.
191
+ #
192
+ # A declared input with no default and no caller-supplied value
193
+ # appears with a `nil` member value, mirroring the behaviour of
194
+ # the per-input getter.
195
+ #
196
+ # @return [Data] read-only view over `input_definitions.keys`
197
+ def input_snapshot
198
+ keys = self.class.input_definitions.keys
199
+ self.class.input_snapshot_class.new(**keys.to_h { |name| [name, @inputs[name]] })
200
+ end
201
+
51
202
  private
52
203
 
204
+ # Build the success-path result hash and fire the terminal
205
+ # `:service_executed` event. Triggers `#execute` lazily via `#result`.
206
+ def executed_payload
207
+ payload = { result:, status:, warnings: }
208
+ notify(:service_executed)
209
+ payload
210
+ end
211
+
212
+ # Build the failure-path result hash and fire the terminal
213
+ # `:service_failed` event. Does not invoke `#execute`.
214
+ def failed_payload
215
+ payload = { errors:, result: nil, status: :with_errors }
216
+ notify(:service_failed)
217
+ payload
218
+ end
219
+
220
+ # M1: apply input defaults declared via `input :name, default: ...`.
221
+ # A default fires when the key is absent, or when the value is an
222
+ # explicit `nil` and the input is not `allow_nil: true` (M2). Procs
223
+ # are invoked with no arguments (zero-arity enforced at
224
+ # class-definition time); literals are used as-is. Defaulted values
225
+ # are subject to the same type / required / if validation as
226
+ # caller-supplied values.
227
+ def apply_input_defaults
228
+ input_definitions_needing_default.each do |attr_name, options|
229
+ provider = options[:default]
230
+ @inputs[attr_name] = provider.is_a?(Proc) ? provider.call : provider
231
+ end
232
+ end
233
+
234
+ # Input definitions that declare a `:default` and whose key was not
235
+ # already supplied by the caller (with `allow_nil:` honoured).
236
+ def input_definitions_needing_default
237
+ self.class.input_definitions.select do |attr_name, options|
238
+ options.key?(:default) && !input_supplied?(attr_name, options)
239
+ end
240
+ end
241
+
242
+ # An explicit nil counts as "not supplied" so the default fires,
243
+ # unless the input opted into `allow_nil: true` — in which case the
244
+ # caller's nil is honoured and the default is skipped.
245
+ def input_supplied?(attr_name, options)
246
+ @inputs.key?(attr_name) && (options[:allow_nil] == true || !@inputs[attr_name].nil?)
247
+ end
248
+
53
249
  def validate_inputs
54
- methods.grep(/valid_(require|type|require_conditional)_\w+\?$/).each do |validation_method|
250
+ # M9: regex matches only the canonical `valid_required_*?` /
251
+ # `valid_required_conditional_*?` / `valid_type_*?` predicates so
252
+ # the deprecated `valid_require_*?` aliases are not auto-invoked
253
+ # (which would emit the M9 deprecation warning from inside the
254
+ # framework itself).
255
+ methods.grep(/valid_(required|type)_\w+\?$/).each do |validation_method|
55
256
  send(validation_method)
56
257
  end
57
258
  end
58
259
 
260
+ # M-S3: dispatch a frozen-set instrumentation event to the configured
261
+ # `Assistant.notifier`. Payload always includes `:service_class` and
262
+ # `:duration_s` (Float seconds since the start of `#run`). The notifier
263
+ # is treated as untrusted infra: any `StandardError` it raises is
264
+ # caught and surfaced via `Kernel.warn` so a misconfigured notifier
265
+ # cannot tear down every service in the process. Non-`StandardError`
266
+ # exceptions (e.g. `SystemExit`, `Interrupt`) are intentionally
267
+ # allowed to propagate.
268
+ def notify(event)
269
+ duration_s = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @run_started_at
270
+ Assistant.notifier.call(event, { service_class: self.class, duration_s: duration_s })
271
+ rescue StandardError => e
272
+ Kernel.warn "assistant: notifier raised during #{event} for #{self.class}: #{e.class}: #{e.message}"
273
+ end
274
+
275
+ # M-S1: thread `#execute` through the registered before/around/after
276
+ # hook chains. Called once via `#result`'s memoization, so each
277
+ # service instance runs hooks at most once even if `#result` is
278
+ # invoked repeatedly.
279
+ def run_execute_with_callbacks
280
+ run_before_execute_hooks
281
+ result = run_around_execute_chain
282
+ run_after_execute_hooks(result)
283
+ result
284
+ end
285
+
286
+ # M-S1: invoke every registered `before_execute` hook in declaration
287
+ # order. Each hook is independent — a `StandardError` from one is
288
+ # logged via `add_log(level: :error, source: :hook, …)` and the
289
+ # remaining hooks still fire.
290
+ def run_before_execute_hooks
291
+ self.class.before_execute_hooks.each do |hook|
292
+ hook.bind_call(self)
293
+ rescue StandardError => e
294
+ log_hook_error(:before_execute, e)
295
+ end
296
+ end
297
+
298
+ # M-S1: invoke every registered `after_execute` hook in declaration
299
+ # order, passing the execute result as the single positional arg.
300
+ def run_after_execute_hooks(execute_result)
301
+ self.class.after_execute_hooks.each do |hook|
302
+ hook.bind_call(self, execute_result)
303
+ rescue StandardError => e
304
+ log_hook_error(:after_execute, e)
305
+ end
306
+ end
307
+
308
+ # M-S1: build the around-hook chain from the innermost layer (the
309
+ # raw `#execute` call) outwards. Declaration order wraps: the
310
+ # first-declared hook is the outermost layer. If an around hook
311
+ # raises before yielding to its continuation, that layer (and any
312
+ # inner layers, including `#execute` itself) does not run; the
313
+ # layer returns `nil` and outer hooks still wrap normally.
314
+ def run_around_execute_chain
315
+ chain = -> { execute }
316
+ self.class.around_execute_hooks.reverse_each do |hook|
317
+ chain = wrap_around_layer(hook, chain)
318
+ end
319
+ chain.call
320
+ end
321
+
322
+ # Build a single around layer: a lambda that runs `hook` with
323
+ # `inner` as its continuation. Hook exceptions raised before the
324
+ # continuation runs are caught, logged, and the layer returns nil.
325
+ def wrap_around_layer(hook, inner)
326
+ lambda do
327
+ hook.bind_call(self, &inner)
328
+ rescue StandardError => e
329
+ log_hook_error(:around_execute, e)
330
+ nil
331
+ end
332
+ end
333
+
334
+ # M-S1: shared hook-error logger. Stamps `source: :hook` and uses
335
+ # the hook type as `detail:` so callers can grep their log timeline
336
+ # by either field. The exception's backtrace is preserved on the
337
+ # `LogItem#trace` field for downstream diagnostics.
338
+ def log_hook_error(hook_type, exception)
339
+ add_log(
340
+ level: :error,
341
+ source: :hook,
342
+ detail: hook_type,
343
+ message: "#{exception.class}: #{exception.message}",
344
+ trace: exception.backtrace
345
+ )
346
+ end
347
+
59
348
  attr_reader :inputs
60
349
 
61
350
  def execute; end
@@ -1,5 +1,86 @@
1
- # frozen_string_literal: true
1
+ # Type signatures for `lib/assistant/service.rb`. See
2
+ # docs/v1/01-api-surface.md for the frozen 1.0 surface.
3
+ #
4
+ # NOTE: per-input methods generated by `Service.input :name, type: T`
5
+ # (`#name`, `#name?`, `#valid_type_name?`, `#valid_required_name?`,
6
+ # `#valid_required_conditional_name?`, and the deprecated
7
+ # `#valid_require_name?` / `#valid_require_conditional_name?` aliases)
8
+ # are *not* declared here because the names are only known at runtime.
9
+ # Users can run `bin/assistant-rbs` (M11, ships with 1.0) to generate
10
+ # per-class signatures for their own `Assistant::Service` subclasses.
2
11
 
3
12
  class Assistant::Service
13
+ include Assistant::LogList
4
14
 
15
+ extend Assistant::InputBuilder
16
+ extend Assistant::ExecuteCallbacks
17
+
18
+ @inputs: Hash[Symbol, untyped]
19
+ @logs: Array[Assistant::LogItem]
20
+ @result: untyped
21
+ @run_started_at: Float
22
+
23
+ attr_reader logs: Array[Assistant::LogItem]
24
+
25
+ def self.run: (**untyped) -> Hash[Symbol, untyped]
26
+
27
+ # M-S4: per-subclass Data class whose members are the declared input
28
+ # names. Memoised; rebuilt when `input_definitions` changes.
29
+ def self.input_snapshot_class: () -> Class
30
+
31
+ def initialize: (**untyped) -> void
32
+
33
+ def run: () -> Hash[Symbol, untyped]
34
+
35
+ def result: () -> untyped
36
+
37
+ def success?: () -> bool
38
+
39
+ def failure?: () -> bool
40
+
41
+ def status: () -> Symbol
42
+
43
+ # M-S2: run a child `Assistant::Service` subclass, merge its log
44
+ # timeline into `self`, return the inner instance. Raises
45
+ # ArgumentError if `klass` is not an `Assistant::Service` subclass.
46
+ def call_service: (Class klass, **untyped inputs) -> Assistant::Service
47
+
48
+ # M-S4: read-only Data view over declared inputs, post-default and
49
+ # post-allow_nil. Only declared inputs are members; undeclared
50
+ # kwargs from `#initialize` are excluded.
51
+ def input_snapshot: () -> Data
52
+
53
+ private
54
+
55
+ attr_reader inputs: Hash[Symbol, untyped]
56
+
57
+ def executed_payload: () -> Hash[Symbol, untyped]
58
+
59
+ def failed_payload: () -> Hash[Symbol, untyped]
60
+
61
+ def apply_input_defaults: () -> void
62
+
63
+ def input_definitions_needing_default: () -> Hash[Symbol, Hash[Symbol, untyped]]
64
+
65
+ def input_supplied?: (Symbol attr_name, Hash[Symbol, untyped] options) -> bool
66
+
67
+ def validate_inputs: () -> Array[Symbol]
68
+
69
+ def notify: (Symbol event) -> void
70
+
71
+ def run_execute_with_callbacks: () -> untyped
72
+
73
+ def run_before_execute_hooks: () -> Array[UnboundMethod]
74
+
75
+ def run_after_execute_hooks: (untyped execute_result) -> Array[UnboundMethod]
76
+
77
+ def run_around_execute_chain: () -> untyped
78
+
79
+ def wrap_around_layer: (UnboundMethod hook, ^() -> untyped inner) -> ^() -> untyped
80
+
81
+ def log_hook_error: (Symbol hook_type, Exception exception) -> void
82
+
83
+ def execute: () -> untyped
84
+
85
+ def validate: () -> void
5
86
  end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Assistant
4
- VERSION = '0.1.0'
4
+ # Semantic version of the `assistant` gem. Follows the semver contract
5
+ # documented in `docs/v1/01-api-surface.md` from 1.0.0 onward.
6
+ #
7
+ # @return [String]
8
+ VERSION = '1.0.0.rc1'
5
9
  end
@@ -0,0 +1,5 @@
1
+ # Type signatures for `lib/assistant/version.rb`. See docs/v1/01-api-surface.md.
2
+
3
+ module Assistant
4
+ VERSION: String
5
+ end
data/lib/assistant.rb CHANGED
@@ -1,9 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Predeclare the namespace shells that multi-segment files
4
+ # (`module Assistant::Refinements::StringBlankness`, etc.) need to exist
5
+ # before they load, and declare the M-S3 instrumentation notifier
6
+ # accessor. Two sibling submodules in one wrapper avoid the
7
+ # `Style/CompactModuleNesting` single-child collapse trigger.
8
+ module Assistant
9
+ # M-S3: frozen no-op default notifier. Identity-compared by
10
+ # `Assistant.notifier=` so callers can detect the unconfigured state
11
+ # if they ever need to. See docs/v1/02-features.md (M-S3) and
12
+ # docs/v1/01-api-surface.md.
13
+ DEFAULT_NOTIFIER = ->(_event, _payload) {}
14
+ DEFAULT_NOTIFIER.freeze
15
+
16
+ # Namespace for the InputBuilder DSL and its submodules. Populated by
17
+ # `lib/assistant/input_builder*.rb`.
18
+ module InputBuilder; end
19
+
20
+ # Namespace for refinements bundled with the gem (`Refinements::*`).
21
+ module Refinements; end
22
+
23
+ @notifier = DEFAULT_NOTIFIER
24
+
25
+ class << self
26
+ # Reader for the configured instrumentation callable. Always returns
27
+ # a callable; returns `DEFAULT_NOTIFIER` when never assigned or when
28
+ # explicitly reset with `Assistant.notifier = nil`.
29
+ attr_reader :notifier
30
+
31
+ # Writer for the instrumentation callable. Accepts any object
32
+ # responding to `#call(event, payload)`, or `nil` to reset to the
33
+ # built-in no-op default. Anything else raises `ArgumentError`
34
+ # immediately so misconfiguration surfaces at boot rather than at
35
+ # the first service run.
36
+ def notifier=(callable)
37
+ @notifier =
38
+ if callable.nil?
39
+ DEFAULT_NOTIFIER
40
+ elsif callable.respond_to?(:call)
41
+ callable
42
+ else
43
+ raise ArgumentError,
44
+ "Assistant.notifier= expected nil or an object responding to #call, got #{callable.inspect}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # Core building blocks for the Assistant gem. Listed alphabetically so the
51
+ # top-level entry point exposes every public constant after a bare
52
+ # `require "assistant"` (M6).
53
+ require 'assistant/execute_callbacks'
54
+ require 'assistant/input_builder'
3
55
  require 'assistant/log_item'
56
+ require 'assistant/log_list'
57
+ require 'assistant/refinements/string_blankness'
4
58
  require 'assistant/service'
5
59
  require 'assistant/version'
6
-
7
- # Main Assistant lib entry point
8
- module Assistant
9
- end
data/lib/assistant.rbs ADDED
@@ -0,0 +1,25 @@
1
+ # Type signatures for `lib/assistant.rb` — declares the top-level
2
+ # `Assistant` namespace, the empty namespace shells for
3
+ # `Assistant::InputBuilder` and `Assistant::Refinements` that the
4
+ # multi-segment submodule files reopen, and the M-S3 instrumentation
5
+ # notifier accessor. See docs/v1/01-api-surface.md.
6
+
7
+ module Assistant
8
+ # M-S3: instrumentation notifier. The setter accepts any object
9
+ # responding to `#call(event, payload)` or `nil` (reset to default).
10
+ # The contract is documented in docs/v1/01-api-surface.md; we use
11
+ # `untyped` for the setter parameter because the runtime check is
12
+ # `respond_to?(:call)` rather than a structural Proc type.
13
+ DEFAULT_NOTIFIER: ^(Symbol, Hash[Symbol, untyped]) -> void
14
+
15
+ self.@notifier: ^(Symbol, Hash[Symbol, untyped]) -> void
16
+
17
+ def self.notifier: () -> ^(Symbol, Hash[Symbol, untyped]) -> void
18
+ def self.notifier=: (untyped callable) -> ^(Symbol, Hash[Symbol, untyped]) -> void
19
+
20
+ module InputBuilder
21
+ end
22
+
23
+ module Refinements
24
+ end
25
+ end
data/mise.toml CHANGED
@@ -2,3 +2,5 @@
2
2
  opencode="latest"
3
3
  ruby="3.4"
4
4
  gh="latest"
5
+ node="latest"
6
+ npm="latest"
@@ -0,0 +1,14 @@
1
+ # Generated by assistant-rbs; do not edit.
2
+
3
+ module Examples
4
+ class Greeter < Assistant::Service
5
+
6
+ def name: () -> String
7
+ def name?: () -> bool
8
+ def age: () -> (Integer | Float)
9
+ def age?: () -> bool
10
+ def nickname: () -> String?
11
+ def nickname?: () -> bool
12
+
13
+ end
14
+ end