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,333 @@
1
+ ---
2
+ title: Inputs
3
+ parent: Guides
4
+ nav_order: 1
5
+ ---
6
+
7
+ <!-- markdownlint-disable MD013 MD024 -->
8
+ # Inputs
9
+
10
+ > **TL;DR** — Declare every input with `input :name, type: Type` at the
11
+ > top of your service class. Inputs are positional in the DSL but the
12
+ > service is constructed with keyword arguments. Use `required:`,
13
+ > `optional:`, `default:`, `allow_nil:`, `if:`, and array types
14
+ > (`type: [String, Symbol]`) to describe the shape exactly. `Steep`
15
+ > users get per-class RBS signatures via `bundle exec assistant-rbs`.
16
+
17
+ This guide covers every option you can pass to `input` (and the bulk
18
+ `inputs` helper). See [`api-reference.md`](../api-reference.md#class-methods)
19
+ for the canonical signatures and stability labels.
20
+
21
+ ## The DSL at a glance
22
+
23
+ ```ruby
24
+ class CreateUser < Assistant::Service
25
+ input :email, type: String, required: true
26
+ input :name, type: String, required: true
27
+ input :age, type: Integer, allow_nil: true, default: nil
28
+ input :role, type: Symbol, default: :member
29
+ inputs %i[street city], type: String, optional: true
30
+
31
+ def execute
32
+ # email, name, age, role, street, city are all readers here
33
+ { email:, name:, age:, role:, street:, city: }
34
+ end
35
+ end
36
+ ```
37
+
38
+ Three things to notice:
39
+
40
+ 1. **`input` and `inputs` take a leading positional name** (`:email`,
41
+ `%i[street city]`). Every other DSL option is a keyword argument.
42
+ This is the only place in the gem where a positional argument
43
+ survives the M12 keyword-only sweep — see
44
+ [`api-reference.md`](../api-reference.md#class-methods).
45
+ 2. **The constructor is keyword-only.** You call
46
+ `CreateUser.run(email: 'a@b.com', name: 'Alice')`, never
47
+ `CreateUser.run('a@b.com', 'Alice')`.
48
+ 3. **Per-input methods are generated for you.** For every `input :name`
49
+ you get `#name`, `#name?`, `#valid_type_name?`, and (when
50
+ `required: true`) `#valid_required_name?`. See
51
+ [`api-reference.md`](../api-reference.md#generated-per-input-methods).
52
+
53
+ ## `type:` — the only required option
54
+
55
+ Every input must declare a `type:`. The most common values are plain
56
+ classes:
57
+
58
+ ```ruby
59
+ input :email, type: String
60
+ input :age, type: Integer
61
+ input :tags, type: Array
62
+ ```
63
+
64
+ A `type:` mismatch logs an error and short-circuits `#execute`:
65
+
66
+ ```ruby
67
+ class TouchEmail < Assistant::Service
68
+ input :email, type: String
69
+
70
+ def execute
71
+ email.upcase
72
+ end
73
+ end
74
+
75
+ TouchEmail.run(email: 42)
76
+ # => { result: nil, status: :with_errors,
77
+ # errors: [#<LogItem detail: :email,
78
+ # message: "Service argument with name email is not a String but Integer">] }
79
+ ```
80
+
81
+ ### Multi-type inputs (M3)
82
+
83
+ Pass an array of classes when more than one is acceptable:
84
+
85
+ ```ruby
86
+ class TouchIdentifier < Assistant::Service
87
+ input :id, type: [String, Integer]
88
+
89
+ def execute
90
+ id.to_s
91
+ end
92
+ end
93
+
94
+ TouchIdentifier.run(id: 'abc').fetch(:result) # => "abc"
95
+ TouchIdentifier.run(id: 42).fetch(:result) # => "42"
96
+ TouchIdentifier.run(id: :nope).fetch(:status) # => :with_errors
97
+ ```
98
+
99
+ The error message lists every accepted type.
100
+
101
+ ## `required: true`
102
+
103
+ Mark an input required and the gem generates a
104
+ `#valid_required_<name>?` validator. Missing or whitespace-only string
105
+ values log an error:
106
+
107
+ ```ruby
108
+ class CreateUser < Assistant::Service
109
+ input :email, type: String, required: true
110
+
111
+ def execute
112
+ { email: }
113
+ end
114
+ end
115
+
116
+ CreateUser.run(email: '')
117
+ # => { result: nil, status: :with_errors,
118
+ # errors: [#<LogItem detail: :email,
119
+ # message: "Service is missing argument with name email">] }
120
+
121
+ CreateUser.run(email: 'a@b.com')
122
+ # => { result: { email: "a@b.com" }, status: :ok, warnings: [] }
123
+ ```
124
+
125
+ The deprecated 0.x name `#valid_require_<name>?` still works in 1.x —
126
+ calls emit a one-time `Kernel.warn` per call site and delegate to the
127
+ canonical predicate. See [`docs/deprecations.md`](../deprecations.md).
128
+
129
+ ## `default:` (M1)
130
+
131
+ Provide a fallback when the caller omits an input. Pass a callable
132
+ (method, lambda, or proc) to compute the default lazily — `assistant`
133
+ warns if you pass a mutable literal like `[]` or `{}` that would be
134
+ shared across calls.
135
+
136
+ ```ruby
137
+ class TouchRole < Assistant::Service
138
+ input :role, type: Symbol, default: :member
139
+
140
+ def execute
141
+ role
142
+ end
143
+ end
144
+
145
+ TouchRole.run.fetch(:result) # => :member
146
+ TouchRole.run(role: :admin).fetch(:result) # => :admin
147
+ ```
148
+
149
+ Lazy defaults are invoked with no arguments:
150
+
151
+ ```ruby
152
+ input :token, type: String, default: -> { SecureRandom.uuid }
153
+ ```
154
+
155
+ A `default:` provider that takes arguments raises `ArgumentError` at
156
+ class-definition time.
157
+
158
+ ## `allow_nil:` (M2)
159
+
160
+ By default, `nil` for a typed input logs a type-mismatch error.
161
+ `allow_nil: true` makes `nil` a legal value:
162
+
163
+ ```ruby
164
+ class TouchAge < Assistant::Service
165
+ input :age, type: Integer, allow_nil: true, default: nil
166
+
167
+ def execute
168
+ age
169
+ end
170
+ end
171
+
172
+ TouchAge.run.fetch(:result) # => nil
173
+ TouchAge.run(age: nil).fetch(:result) # => nil
174
+ TouchAge.run(age: 30).fetch(:result) # => 30
175
+ ```
176
+
177
+ Combine with `default:` to express "optional integer that defaults to
178
+ nil and may be set to nil explicitly".
179
+
180
+ ## `optional: true` (M7)
181
+
182
+ `optional: true` is a shorthand for "skip the presence check entirely;
183
+ do not generate `#valid_required_name?`". It is mutually exclusive
184
+ with `required: true`:
185
+
186
+ ```ruby
187
+ class TouchNickname < Assistant::Service
188
+ input :nickname, type: String, optional: true
189
+
190
+ def execute
191
+ nickname.to_s.upcase
192
+ end
193
+ end
194
+
195
+ TouchNickname.run.fetch(:result) # => ""
196
+ TouchNickname.run(nickname: 'ada').fetch(:result) # => "ADA"
197
+ ```
198
+
199
+ If you actually want a typed-but-nullable value, prefer
200
+ `allow_nil: true` plus `default: nil`; reserve `optional: true` for
201
+ inputs whose absence simply means "don't bother".
202
+
203
+ ## `if:` — conditional requirement
204
+
205
+ `if:` combined with `required: true` makes the presence check fire
206
+ only when the predicate returns truthy. The predicate is called with
207
+ the input's current value:
208
+
209
+ ```ruby
210
+ class CreateUser < Assistant::Service
211
+ input :role, type: Symbol, default: :member
212
+ input :email, type: String, required: true, if: ->(_value) { caller_wants_email? }
213
+
214
+ def execute
215
+ { email:, role: }
216
+ end
217
+
218
+ private
219
+
220
+ def caller_wants_email?
221
+ role == :admin
222
+ end
223
+ end
224
+ ```
225
+
226
+ > **Predicate semantics.** Under the hood the validator requires
227
+ > `email` to be present **and** the predicate to return truthy. If you
228
+ > want the inverse — "email is allowed-but-not-required when role is
229
+ > admin" — combine `optional: true` with a manual `validate` check
230
+ > instead. See [`validation.md`](./validation.md) for the manual route.
231
+
232
+ ## `inputs` — bulk declaration
233
+
234
+ `inputs` takes a list of names and applies the same `type:` /
235
+ `options` to all of them. Use it when several inputs share a shape:
236
+
237
+ ```ruby
238
+ class ShipAddress < Assistant::Service
239
+ inputs %i[street city zip], type: String, required: true
240
+
241
+ def execute
242
+ "#{street}, #{city} #{zip}"
243
+ end
244
+ end
245
+ ```
246
+
247
+ This is exactly equivalent to writing three `input` calls.
248
+
249
+ ## Reading inputs back: `#input_snapshot`
250
+
251
+ `#input_snapshot` returns a frozen `Data` instance carrying the
252
+ post-default, post-`allow_nil` values. It's useful for forwarding the
253
+ inputs of one service into another, for instrumentation, or for tests
254
+ that want a structural snapshot:
255
+
256
+ ```ruby
257
+ class CreateUser < Assistant::Service
258
+ input :email, type: String, required: true
259
+ input :role, type: Symbol, default: :member
260
+
261
+ def execute
262
+ [input_snapshot.email, input_snapshot.role]
263
+ end
264
+ end
265
+
266
+ CreateUser.run(email: 'a@b.com').fetch(:result)
267
+ # => ["a@b.com", :member]
268
+ ```
269
+
270
+ See [`composing-services.md`](./composing-services.md) for a worked
271
+ example that snapshots the outer service's inputs into an inner one.
272
+
273
+ ## Using `assistant-rbs` for Steep users
274
+
275
+ `Assistant::Service` is metaprogramming-heavy: per-input methods are
276
+ generated at class-definition time by `Service.input`, which means a
277
+ generic `.rbs` for `Service` can't know that your `CreateUser#email`
278
+ returns `String`. That's R1 in
279
+ [`docs/v1/05-quality-and-tooling.md`](https://github.com/ramongr/assistant/blob/main/docs/v1/05-quality-and-tooling.md).
280
+
281
+ The bundled `assistant-rbs` CLI (M11) closes the gap by emitting
282
+ per-class `.rbs` files. Run it once after editing your services:
283
+
284
+ ```sh
285
+ bundle exec assistant-rbs lib --output sig
286
+ ```
287
+
288
+ For the `CreateUser` example above it writes
289
+ `sig/CreateUser.rbs` with:
290
+
291
+ ```rbs
292
+ class CreateUser < Assistant::Service
293
+ def email: () -> String
294
+ def email?: () -> bool
295
+ def role: () -> Symbol
296
+ def role?: () -> bool
297
+ end
298
+ ```
299
+
300
+ Multi-type inputs produce union types
301
+ (`String | Integer`), and `allow_nil: true` produces nullable types
302
+ (`String?`). The generator is idempotent — re-running with no input
303
+ changes is a no-op.
304
+
305
+ The CLI is labelled **Experimental** for 1.0 because its output
306
+ format may evolve in 1.x; see
307
+ [`api-reference.md`](../api-reference.md#assistant-rbs-cli) for the
308
+ stability label.
309
+
310
+ ## Common pitfalls
311
+
312
+ - **Passing a positional name to the constructor.** `Service.new('a')`
313
+ always raises. Call `Service.new(email: 'a')`, or just use
314
+ `Service.run(email: 'a')`.
315
+ - **Sharing a mutable default literal.** `default: []` would share
316
+ one array across calls; the gem warns and recommends a lambda
317
+ (`default: -> { [] }`).
318
+ - **Mixing `required: true` with `optional: true`.** They contradict
319
+ each other; the gem raises at class-definition time.
320
+ - **Expecting `if:` to inhibit presence.** The validator requires
321
+ presence *and* the predicate. Use `optional: true` plus a `validate`
322
+ hook when you need the inverse.
323
+
324
+ ## See also
325
+
326
+ - [Validation guide](./validation.md) — `validate` hook, when to log a
327
+ warning vs. error.
328
+ - [Logging and results](./logging-and-results.md) — `LogItem`,
329
+ `log_item_*` shorthands, the result hash.
330
+ - [Composing services](./composing-services.md) — `call_service`,
331
+ callbacks, `#input_snapshot` between services.
332
+ - [API reference: class methods](../api-reference.md#class-methods).
333
+ - [API reference: generated per-input methods](../api-reference.md#generated-per-input-methods).
@@ -0,0 +1,202 @@
1
+ ---
2
+ title: Logging and results
3
+ parent: Guides
4
+ nav_order: 3
5
+ ---
6
+
7
+ <!-- markdownlint-disable MD013 MD024 -->
8
+ # Logging and results
9
+
10
+ > **TL;DR** — Every service maintains a `#logs` timeline of
11
+ > `Assistant::LogItem`s. Use `log_item_info / _warning / _error` to
12
+ > add entries, `#logs` / `#infos` / `#warnings` / `#errors` to read
13
+ > them, and the result hash returned by `.run` to consume the
14
+ > service from outside. `LogItem.new` raises `ArgumentError` for
15
+ > invalid attributes (M10) — prefer the helpers.
16
+
17
+ This guide covers the data model, the writer helpers, the reader
18
+ predicates, and the shape of the result hash. See the
19
+ [Validation guide](./validation.md) for *when* to log a warning vs.
20
+ an error.
21
+
22
+ ## `Assistant::LogItem` at a glance
23
+
24
+ Every entry on `#logs` is an `Assistant::LogItem` with the following
25
+ fields:
26
+
27
+ | Field | Type | Notes |
28
+ |------------|--------------------------------|----------------------------------------------------------------|
29
+ | `level` | `Symbol` | One of `:info`, `:warning`, `:error`. |
30
+ | `source` | `Symbol` | High-level subsystem (`:initialize`, `:execute`, `:hook`, ...).|
31
+ | `detail` | `Symbol` | Finer-grained tag; usually an input attribute name. |
32
+ | `message` | `String` | Human-readable text. |
33
+ | `trace` | `Array<String>` or `nil` | Optional backtrace captured at construction. |
34
+
35
+ Constraints (enforced strictly in 1.0 — M10):
36
+
37
+ - `source != detail`.
38
+ - `source` and `detail` must each be non-empty.
39
+ - `message` must contain at least one non-whitespace character.
40
+ - `level` must be one of `Assistant::LogItem::VALID_LEVELS`.
41
+
42
+ ```ruby
43
+ Assistant::LogItem::VALID_LEVELS
44
+ # => [:info, :warning, :error]
45
+ ```
46
+
47
+ ## Writing log entries
48
+
49
+ The three shorthand helpers (M5) are the recommended call sites
50
+ inside `#validate` and `#execute`:
51
+
52
+ ```ruby
53
+ class CreateUser < Assistant::Service
54
+ input :email, type: String, required: true
55
+ input :age, type: Integer, allow_nil: true, default: nil
56
+
57
+ def validate
58
+ return if email.include?('@')
59
+
60
+ log_item_error(source: :validate, detail: :email, message: 'invalid email')
61
+ end
62
+
63
+ def execute
64
+ log_item_info(source: :execute, detail: :age, message: "age=#{age.inspect}")
65
+ log_item_warning(source: :execute, detail: :age, message: 'age missing') if age.nil?
66
+
67
+ { id: 42, email:, age: }
68
+ end
69
+ end
70
+ ```
71
+
72
+ `add_log(level:, source:, detail:, message:, trace: nil)` is the
73
+ generic form when you need to set the level dynamically:
74
+
75
+ ```ruby
76
+ level = problem.severe? ? :error : :warning
77
+ add_log(level:, source: :execute, detail: :payment, message: problem.to_s)
78
+ ```
79
+
80
+ `#log_item_error_initialize(attr_name:, message:)` is used internally
81
+ by the generated `valid_required_*?` / `valid_type_*?` validators to
82
+ record per-input errors. Service code can call it directly when an
83
+ ad-hoc validation needs the same `:initialize` source as the
84
+ declarative checks.
85
+
86
+ ## Reading log entries
87
+
88
+ A service exposes three readers, one per level, plus the full
89
+ timeline:
90
+
91
+ | Method | Returns |
92
+ |---------------|--------------------------------------|
93
+ | `#logs` | `Array<LogItem>` — every entry, in insertion order. |
94
+ | `#infos` | `Array<LogItem>` — entries with `level == :info`. |
95
+ | `#warnings` | `Array<LogItem>` — entries with `level == :warning`.|
96
+ | `#errors` | `Array<LogItem>` — entries with `level == :error`. |
97
+
98
+ ```ruby
99
+ service = CreateUser.new(email: 'a@b.com')
100
+ service.run
101
+
102
+ service.logs.size # => however many entries
103
+ service.infos.first.message
104
+ service.warnings.any?
105
+ service.errors.empty?
106
+ ```
107
+
108
+ `#status` is derived from `#errors` and `#warnings`:
109
+
110
+ - `:with_errors` if `#errors.any?`.
111
+ - `:with_warnings` if `#warnings.any?` and no errors.
112
+ - `:ok` otherwise.
113
+
114
+ `#success?` is `true` for `:ok` and `:with_warnings`; `#failure?` is
115
+ `true` only for `:with_errors`.
116
+
117
+ ## The result hash
118
+
119
+ `Service.run` (and `Service#run`) returns one of two shapes:
120
+
121
+ ```ruby
122
+ # Success — status is :ok or :with_warnings
123
+ { result: <Object>, status: :ok | :with_warnings, warnings: Array<LogItem> }
124
+
125
+ # Failure — :with_errors
126
+ { result: nil, status: :with_errors, errors: Array<LogItem> }
127
+ ```
128
+
129
+ The success shape always includes `:warnings` (possibly empty); the
130
+ failure shape always includes `:errors` (always non-empty) and
131
+ `result: nil`. Pattern-matching is the cleanest way to consume it:
132
+
133
+ ```ruby
134
+ case CreateUser.run(email: 'a@b.com')
135
+ in { result:, status: :ok }
136
+ result
137
+ in { result:, status: :with_warnings, warnings: }
138
+ WarningsLogger.log(warnings)
139
+ result
140
+ in { errors:, status: :with_errors }
141
+ raise Errors::InvalidRequest, errors.map(&:message).join(', ')
142
+ end
143
+ ```
144
+
145
+ `#infos` are intentionally **not** part of the result hash. They live
146
+ on the service instance for inspection (and for tests), but the
147
+ public contract is the warnings/errors split.
148
+
149
+ ## Merging logs across services
150
+
151
+ `#merge_logs(logs:)` concatenates another timeline onto the current
152
+ service's `#logs`. It's mostly used by `#call_service` (see
153
+ [`composing-services.md`](./composing-services.md)), but you can call
154
+ it directly when you need to forward log items from a non-`Service`
155
+ collaborator:
156
+
157
+ ```ruby
158
+ def execute
159
+ outcome = MyLibrary.do_thing
160
+ merge_logs(logs: outcome.log_items.map { |item| Assistant::LogItem.new(**item) })
161
+ outcome.value
162
+ end
163
+ ```
164
+
165
+ > **M12.** `#merge_logs` is keyword-only in 1.0. Passing positional
166
+ > arguments raises `ArgumentError`. The
167
+ > [migration guide](https://github.com/ramongr/assistant/blob/main/docs/v1/06-migration-0x-to-1.md) covers the
168
+ > mechanical rewrite.
169
+
170
+ ## Inspecting an entry
171
+
172
+ Every `LogItem` has a `#item` method that returns a `Hash` view —
173
+ handy for JSON serialization or test assertions:
174
+
175
+ ```ruby
176
+ service.errors.first.item
177
+ # => { level: :error, source: :validate, detail: :email,
178
+ # message: "invalid email", trace: nil }
179
+ ```
180
+
181
+ ## Common pitfalls
182
+
183
+ - **Pushing onto `@logs` directly.** Don't — always go through the
184
+ helpers so the M10 strict construction runs and so future
185
+ middleware (e.g. an instrumentation hook around `#add_log`) can
186
+ see the entry.
187
+ - **Using `LogItem.new` with `source == detail`.** Raises
188
+ `ArgumentError`. Pick distinct symbols.
189
+ - **Treating `#infos` as part of the contract.** They're for
190
+ introspection only; the result hash never includes them.
191
+ - **Calling `merge_logs(other.logs)` (positional).** M12 requires
192
+ the keyword form: `merge_logs(logs: other.logs)`.
193
+
194
+ ## See also
195
+
196
+ - [Validation guide](./validation.md) — choosing warning vs. error,
197
+ conditional checks, `#validate` mechanics.
198
+ - [Composing services](./composing-services.md) — how `#call_service`
199
+ merges inner logs into the outer timeline.
200
+ - [API reference: LogItem](../api-reference.md#assistantlogitem).
201
+ - [API reference: LogList](../api-reference.md#assistantloglist).
202
+ - [Migration guide](https://github.com/ramongr/assistant/blob/main/docs/v1/06-migration-0x-to-1.md) for M10 + M12.
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: RBS and types
3
+ parent: Guides
4
+ nav_order: 5
5
+ ---
6
+
7
+ # RBS and types
8
+
9
+ > **Status:** placeholder — full content lands in
10
+ > [P4](https://github.com/ramongr/assistant/blob/main/docs/v1/08-github-pages.md) of the GitHub Pages plan. For now,
11
+ > see the [RBS subsection in the Inputs guide](inputs.md#using-assistant-rbs-for-steep-users)
12
+ > and the [Recipe in the 0.x → 1.x migration guide](https://github.com/ramongr/assistant/blob/main/docs/v1/06-migration-0x-to-1.md#recipe-binassistant-rbs-for-steep-users).
13
+
14
+ This page will cover the `bin/assistant-rbs` per-class generator (M11),
15
+ the R1 metaprogramming limitation that motivates it, and how to wire
16
+ the generated `.rbs` files into a Steep-checked project.