assistant 0.0.2 → 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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +13 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +39 -0
  4. data/.github/dependabot.yml +4 -0
  5. data/.github/workflows/ci.yml +140 -0
  6. data/.github/workflows/docs.yml +64 -0
  7. data/.github/workflows/release.yml +46 -0
  8. data/.gitignore +5 -1
  9. data/.markdownlint.json +6 -0
  10. data/.opencode/.gitignore +4 -0
  11. data/.opencode/opencode.json +13 -0
  12. data/.opencode/skills/create-pr/SKILL.md +138 -0
  13. data/.opencode/skills/ruby-services/SKILL.md +81 -0
  14. data/.rubocop.yml +40 -148
  15. data/.ruby-version +1 -1
  16. data/.yardopts +17 -0
  17. data/CHANGELOG.md +434 -0
  18. data/CONTRIBUTING.md +131 -0
  19. data/Gemfile +10 -0
  20. data/Gemfile.lock +264 -94
  21. data/README.md +125 -16
  22. data/Rakefile +53 -3
  23. data/SECURITY.md +50 -0
  24. data/Steepfile +49 -0
  25. data/_config.yml +87 -0
  26. data/assistant.gemspec +33 -20
  27. data/docs/api-reference.md +264 -0
  28. data/docs/changelog.md +26 -0
  29. data/docs/deprecations.md +86 -0
  30. data/docs/examples/cli-handler.md +17 -0
  31. data/docs/examples/composing-services.md +17 -0
  32. data/docs/examples/execute-callbacks.md +17 -0
  33. data/docs/examples/index.md +29 -0
  34. data/docs/examples/instrumentation-notifier.md +17 -0
  35. data/docs/examples/rails-service.md +17 -0
  36. data/docs/examples/rbs-generator.md +17 -0
  37. data/docs/examples/sidekiq-worker.md +17 -0
  38. data/docs/getting-started.md +136 -0
  39. data/docs/guides/composing-services.md +222 -0
  40. data/docs/guides/index.md +25 -0
  41. data/docs/guides/inputs.md +333 -0
  42. data/docs/guides/logging-and-results.md +202 -0
  43. data/docs/guides/rbs-and-types.md +16 -0
  44. data/docs/guides/validation.md +180 -0
  45. data/docs/index.md +69 -0
  46. data/docs/roadmap.md +33 -0
  47. data/exe/assistant-rbs +7 -0
  48. data/lib/assistant/execute_callbacks.rb +103 -0
  49. data/lib/assistant/execute_callbacks.rbs +30 -0
  50. data/lib/assistant/input_builder/accessors.rb +36 -0
  51. data/lib/assistant/input_builder/accessors.rbs +10 -0
  52. data/lib/assistant/input_builder/default_option.rb +41 -0
  53. data/lib/assistant/input_builder/default_option.rbs +11 -0
  54. data/lib/assistant/input_builder/dsl.rb +37 -0
  55. data/lib/assistant/input_builder/dsl.rbs +12 -0
  56. data/lib/assistant/input_builder/optional_option.rb +45 -0
  57. data/lib/assistant/input_builder/optional_option.rbs +10 -0
  58. data/lib/assistant/input_builder/registry.rb +27 -0
  59. data/lib/assistant/input_builder/registry.rbs +13 -0
  60. data/lib/assistant/input_builder/require_validator.rb +104 -0
  61. data/lib/assistant/input_builder/require_validator.rbs +24 -0
  62. data/lib/assistant/input_builder/type_validator.rb +47 -0
  63. data/lib/assistant/input_builder/type_validator.rbs +18 -0
  64. data/lib/assistant/input_builder.rb +28 -0
  65. data/lib/assistant/input_builder.rbs +15 -0
  66. data/lib/assistant/log_item.rb +75 -17
  67. data/lib/assistant/log_item.rbs +40 -0
  68. data/lib/assistant/log_list.rb +44 -12
  69. data/lib/assistant/log_list.rbs +48 -0
  70. data/lib/assistant/rbs_generator/cli.rb +109 -0
  71. data/lib/assistant/rbs_generator/cli.rbs +24 -0
  72. data/lib/assistant/rbs_generator/renderer.rb +67 -0
  73. data/lib/assistant/rbs_generator/renderer.rbs +11 -0
  74. data/lib/assistant/rbs_generator/writer.rb +65 -0
  75. data/lib/assistant/rbs_generator/writer.rbs +24 -0
  76. data/lib/assistant/rbs_generator.rb +38 -0
  77. data/lib/assistant/rbs_generator.rbs +5 -0
  78. data/lib/assistant/refinements/string_blankness.rb +14 -0
  79. data/lib/assistant/refinements/string_blankness.rbs +6 -0
  80. data/lib/assistant/service.rb +328 -8
  81. data/lib/assistant/service.rbs +86 -0
  82. data/lib/assistant/version.rb +5 -1
  83. data/lib/assistant/version.rbs +5 -0
  84. data/lib/assistant.rb +53 -4
  85. data/lib/assistant.rbs +25 -0
  86. data/mise.toml +6 -0
  87. data/sig/examples/greeter.rbs +14 -0
  88. metadata +128 -112
  89. data/.circleci/config.yml +0 -45
  90. data/.fasterer.yml +0 -19
  91. data/.rspec +0 -3
  92. data/.rubocop_todo.yml +0 -0
data/CHANGELOG.md ADDED
@@ -0,0 +1,434 @@
1
+ <!-- markdownlint-disable MD043 -->
2
+ # Changelog
3
+
4
+ All notable changes to this project will be documented in this file.
5
+
6
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+
9
+ ## [Unreleased]
10
+
11
+ ### Added
12
+
13
+ - **GitHub Pages — P2 scaffolding (Jekyll + just-the-docs)**: stood up
14
+ the parallel docs site per
15
+ [`docs/v1/08-github-pages.md`](docs/v1/08-github-pages.md). Ships
16
+ `_config.yml` (just-the-docs theme, Lunr search, dark-mode toggle,
17
+ `gh_edit_link`, `jekyll-seo-tag` + `jekyll-relative-links` plugins),
18
+ an optional `:docs` Bundler group in [`Gemfile`](Gemfile) pinning
19
+ `jekyll ~> 4.3`, `just-the-docs ~> 0.10`, and
20
+ `jekyll-relative-links ~> 0.7`, the
21
+ [`.github/workflows/docs.yml`](.github/workflows/docs.yml) workflow
22
+ (PR builds `bundle exec jekyll build --strict_front_matter`; pushes
23
+ to `main` deploy via `actions/deploy-pages@v4`), `docs:install` /
24
+ `docs:build` / `docs:serve` Rake tasks driving the Jekyll
25
+ toolchain, per-page front matter on every site page (Home, Getting
26
+ started, Guides + 5 sub-pages, API reference, Deprecations, Examples
27
+ + 7 sub-pages, Roadmap, Changelog), and a new `docs/guides/index.md`
28
+ landing for the Guides section. The site builds locally with `rake
29
+ docs:build` and deploys to `https://ramongr.github.io/assistant/` on
30
+ every push to `main`. Pages source = `GitHub Actions` was enabled in
31
+ repo settings after the original mkdocs PR (#177) merged; no further
32
+ manual step is needed for the Jekyll cut-over.
33
+
34
+ _This entry supersedes the original mkdocs-based P2 scaffolding —
35
+ the mkdocs stack lived for one PR before being replaced with Jekyll
36
+ to match the gem's primary toolchain, drop the Python build
37
+ dependency, and avoid the `Pygments==2.19.1` pin that worked around
38
+ a 2.20.0 `HtmlFormatter` regression._
39
+
40
+ ## [1.0.0.rc1] - 2026-06-15
41
+
42
+ ### Added
43
+
44
+ - **D2 (follow-up)**: four user-facing guides under `docs/guides/` —
45
+ [`inputs.md`](docs/guides/inputs.md),
46
+ [`validation.md`](docs/guides/validation.md),
47
+ [`logging-and-results.md`](docs/guides/logging-and-results.md),
48
+ [`composing-services.md`](docs/guides/composing-services.md).
49
+ Each guide is mirrored by a `test/docs/<guide>_examples_test.rb`
50
+ integration test so the runnable examples can't silently drift from
51
+ the actual behaviour. `inputs.md` includes the "Using
52
+ `bin/assistant-rbs` for Steep users" subsection that closes the R1
53
+ user-facing-note item in
54
+ [`docs/v1/05-quality-and-tooling.md`](docs/v1/05-quality-and-tooling.md).
55
+ `.yardopts` extra-files list extended to include the four new pages
56
+ so they ship with the rendered YARD output.
57
+
58
+ - **bin/ smoke**: new `bin-smoke` job in `.github/workflows/ci.yml`
59
+ exercises `bin/setup` against a cold bundle, syntax-checks the three
60
+ developer scripts (`bash -n bin/setup`, `ruby -c bin/{console,version}`),
61
+ runs `bin/version --help`, and pipes a short ruby snippet through
62
+ `bin/console` to confirm `Assistant::VERSION` resolves. Closes the
63
+ `bin/` smoke item in
64
+ [`docs/v1/05-quality-and-tooling.md`](docs/v1/05-quality-and-tooling.md).
65
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) gains a `bin/ developer scripts`
66
+ section documenting each script's purpose and noting that none of the
67
+ three ship in the packaged gem (only `exe/assistant-rbs` does).
68
+
69
+ ### Changed
70
+
71
+ - **Release prep**: gemspec polished for the 1.0 cut. `spec.summary`
72
+ rewritten to match the README elevator pitch
73
+ (`Tiny, dependency-free soft-fail service objects for Ruby`),
74
+ `spec.description` expanded into a 3-sentence heredoc covering
75
+ soft-fail semantics, the uniform result shape, the RBS / Steep
76
+ posture, and the zero-runtime-deps guarantee. Added
77
+ `spec.metadata['documentation_uri']`
78
+ (`https://rubydoc.info/gems/assistant`) and
79
+ `spec.metadata['bug_tracker_uri']`
80
+ (`https://github.com/ramongr/assistant/issues`). The `spec.files`
81
+ glob now excludes `examples/`, `docs/v1/`, and `docs/v1.x/` from the
82
+ packaged gem so internal planning material and runnable samples no
83
+ longer ship to RubyGems (Q9 decision in
84
+ [`docs/v1/07-risks-and-open-questions.md`](docs/v1/07-risks-and-open-questions.md)).
85
+ No behaviour change; `Assistant::VERSION` is unchanged.
86
+
87
+ ### Changed (Breaking)
88
+
89
+ - **M12**: `LogList#merge_logs` and every internal
90
+ `Assistant::InputBuilder` helper now take their name / list
91
+ parameter as a keyword argument (`logs:` / `name:` / `names:`)
92
+ instead of a leading positional. The two public DSL entry points
93
+ `Service.input` and `Service.inputs` are **deliberately exempt** —
94
+ `input :foo, type: X` reads better as a class-body declaration than
95
+ `input name: :foo, type: X`, so their leading positional `attr_name`
96
+ / `attr_names` stays. Hard break for the rest, no runtime shim:
97
+ - `Service.input(:foo, type: String)` — **unchanged**
98
+ - `Service.inputs(%i[a b], type: Integer)` — **unchanged**
99
+ - `host.merge_logs(other.logs)` → `host.merge_logs(logs: other.logs)`
100
+ The old positional `merge_logs` raises `ArgumentError` at call time
101
+ ("wrong number of arguments ... required keyword: logs"). For users
102
+ who don't compose log lists directly (i.e. who only use
103
+ `Service#call_service` for service composition), no source change is
104
+ required. Migration is mechanical and `git grep`-able; see
105
+ [`docs/v1/06-migration-0x-to-1.md`](docs/v1/06-migration-0x-to-1.md).
106
+ The full helper sweep also touches the M13-split per-concern
107
+ modules: `process_default_option`, `validate_default!`,
108
+ `warn_on_mutable_default`, `process_optional_option`,
109
+ `validate_optional!`, `register_input_definition`, `input_getter_meth`,
110
+ `input_checker_meth`, `input_type_validator_meth`, `type_validator_body`,
111
+ `type_mismatch_message_builder`, `input_require_validator_meth`,
112
+ `input_require_conditional_meth`, and the two private
113
+ `RequireValidator#define_required_(conditional_)?validator` helpers
114
+ are all keyword-only. Internal-only `Service#input_supplied?` keeps
115
+ its positional shape (private, not part of the documented surface).
116
+ RBS signatures across `lib/assistant/input_builder/*.rbs` (other than
117
+ `dsl.rbs`) and `lib/assistant/log_list.rbs` updated to match.
118
+
119
+ ### Added
120
+
121
+ - **D2** (entry pages): shipped `docs/getting-started.md` and
122
+ `docs/api-reference.md`. `docs/getting-started.md` walks from
123
+ `gem install` to a working `CreateUser` service across three runs
124
+ (one `:ok`, one `:with_warnings`, one `:with_errors`) and links out
125
+ to the four follow-up guides. `docs/api-reference.md` is the
126
+ hand-written, curated reference for every Frozen symbol on
127
+ `Assistant`, `Assistant::Service`, `Assistant::LogItem`,
128
+ `Assistant::LogList`, the execute callbacks, `#call_service`,
129
+ the notifier, `#input_snapshot`, and the `assistant-rbs` CLI;
130
+ `docs/v1/01-api-surface.md` remains the source of truth for
131
+ stability labels. `README.md` documentation index and the
132
+ `.yardopts` extra-files list now include both new pages. The four
133
+ topic guides (`inputs.md`, `validation.md`, `logging-and-results.md`,
134
+ `composing-services.md`) ship in a follow-up D2 PR alongside
135
+ `test/docs/` example tests.
136
+ - **D3**: every public Frozen symbol enumerated in
137
+ [`docs/v1/01-api-surface.md`](docs/v1/01-api-surface.md) now carries
138
+ YARD documentation (`@param`, `@return`, `@raise`, `@example` where
139
+ meaningful). Internal helpers are documented too, so
140
+ `bundle exec yard stats --list-undoc` reports **100%** documented
141
+ public methods (52 / 52, plus 7 / 7 attributes and 9 / 9 constants).
142
+ Shipped together with a top-level `.yardopts` (markdown markup,
143
+ `lib/**/*.rb` as the source, README + repo-hygiene files as extra
144
+ files), the new `yard` development dependency in `assistant.gemspec`,
145
+ and a `rake yard` task that builds the site into `doc/` and exits
146
+ non-zero if coverage drops below 100%. `rake ci` now runs
147
+ `test + rubocop + steep + yard`.
148
+ - **D4**: shipped the repository-hygiene files called for in
149
+ [`docs/v1/03-documentation.md`](docs/v1/03-documentation.md). New
150
+ `CONTRIBUTING.md` documents the clone / `bin/setup` flow, the local
151
+ pipeline (`rake test`, `rubocop`, `steep check`, `rake ci`), branch
152
+ naming, commit-tag conventions, and PR template expectations. New
153
+ `SECURITY.md` declares 1.x as the supported line, 0.x as EOL on the
154
+ `1.0.0` release, gives `cerberus.ramon@gmail.com` as the private
155
+ report channel, and commits to a 7-day first-response /
156
+ 30-day-fix-or-mitigation-plan SLA. New
157
+ `.github/PULL_REQUEST_TEMPLATE.md` enforces the
158
+ `Scope / What ships / Verification / Out of scope` body shape and the
159
+ `CHANGELOG entry / tests added / docs updated / rake ci is green`
160
+ checklist on every pull request.
161
+ - **D1**: rewrote the top-level `README.md`. Replaced the bundler-template
162
+ `TODO:` placeholders and `[USERNAME]/assistant` URLs with an elevator
163
+ pitch, status badges (CI, gem version, downloads, Ruby version,
164
+ license), `bundle add` / `gem install` instructions, a runnable
165
+ 60-second `CreateUser` example covering required inputs, defaults,
166
+ `allow_nil:`, `validate`, and the `log_item_warning` /
167
+ `log_item_error` shorthands, a "why another service-object gem?"
168
+ comparison against Interactor and dry-transaction, a documentation
169
+ index pointing at `docs/v1/01-api-surface.md`, the migration guide,
170
+ deprecations, examples, the changelog, and the roadmap, plus a
171
+ refreshed Development section listing `rake test`, `rubocop`, and
172
+ `steep check`. (D1, v1 plan)
173
+
174
+ - `Assistant::Service#input_snapshot` — returns a frozen `Data`
175
+ instance whose members are the declared input names (via
176
+ `Service.input` / `Service.inputs`), in declaration order, with
177
+ values read from `@inputs` after `apply_input_defaults` has run. The
178
+ snapshot therefore reflects post-`default:` and post-`allow_nil:`
179
+ values, matching what the per-input getters expose. Only declared
180
+ inputs appear; extra keyword arguments accepted by `#initialize`
181
+ (which live in `@inputs` but have no `input :foo` declaration) are
182
+ intentionally excluded so the snapshot's shape mirrors the public
183
+ DSL. A declared input with no default and no caller-supplied value
184
+ surfaces as `nil`. The returned `Data` is structurally immutable
185
+ (no member reassignment); member values that are themselves mutable
186
+ (e.g. an `Array`) keep their normal mutability — the snapshot does
187
+ not deep-freeze. Each call returns a fresh `Data` instance backed
188
+ by a per-subclass `Data` class memoised on
189
+ `Service.input_snapshot_class` (rebuilt transparently if the
190
+ subclass declares more inputs after the first snapshot call).
191
+ Useful for passing a read-only view of inputs to helpers,
192
+ collaborators, or test assertions without exposing the mutable
193
+ `@inputs` hash.
194
+
195
+ - `Assistant::Service#call_service(klass, **inputs)` — instance-level
196
+ helper for composing services. Constructs an instance of `klass`
197
+ (asserted to be an `Assistant::Service` subclass; raises
198
+ `ArgumentError` otherwise), invokes `inner.run`, merges the inner
199
+ service's full log timeline (info + warning + error) onto the outer
200
+ service via `merge_logs`, and returns the inner instance. Because
201
+ `Service#errors` / `#warnings` / `#status` are derived by filtering
202
+ `@logs`, inner errors automatically downgrade the outer terminal
203
+ status to `:with_errors` and inner warnings surface as
204
+ `:with_warnings` (when no errors are present), without any branching
205
+ in the caller. Exceptions raised by the inner service's `#execute`
206
+ or by `Assistant.notifier` are not rescued; they propagate to the
207
+ caller, matching the base `Service#run` contract. The inner service
208
+ fires its own `:service_started`/`:service_validated`/
209
+ `:service_executed`/`:service_failed` events independently of the
210
+ outer lifecycle. (M-S2, v1 plan)
211
+
212
+ - `before_execute`, `after_execute { |result| }`, and
213
+ `around_execute { |&blk| ... }` class-level DSL on
214
+ `Assistant::Service` for wrapping `#execute` with reusable hooks.
215
+ Hooks are `instance_exec`'d on the service (so `self` is the service
216
+ instance) and execute after validation in declaration order; the
217
+ first-declared `around_execute` is the outermost layer. Hooks are
218
+ inherited at subclass-definition time via an array snapshot — later
219
+ additions on the parent do not bleed into existing subclasses. Errors
220
+ raised inside any hook are caught, never propagate out of `#run`,
221
+ and are logged via `add_log(level: :error, source: :hook, detail:
222
+ <hook_type>, message: "<ErrorClass>: <message>", trace: backtrace)`.
223
+ A hook-logged error downgrades the terminal lifecycle event to
224
+ `:service_failed` and the run payload to `{ errors:, result: nil,
225
+ status: :with_errors }`; the actual execute return value remains
226
+ accessible via `service.result`. (M-S1, v1 plan)
227
+
228
+ - `Assistant.notifier` and `Assistant.notifier=` — module-level
229
+ configuration accessor for an instrumentation callable. The default
230
+ notifier is a frozen no-op lambda (`Assistant::DEFAULT_NOTIFIER`);
231
+ the setter accepts any object responding to `#call(event, payload)`
232
+ or `nil` to reset to the default. Passing anything else raises
233
+ `ArgumentError` immediately. `Service#run` now fires four frozen
234
+ events around its lifecycle: `:service_started` at entry,
235
+ `:service_validated` after `validate_inputs` + `validate`, and
236
+ exactly one of `:service_executed` (no logged errors) or
237
+ `:service_failed` (errors present) before returning. Every payload
238
+ carries `{ service_class:, duration_s: }`; `duration_s` is a `Float`
239
+ measured against `Process::CLOCK_MONOTONIC` from the start of `#run`.
240
+ Notifier exceptions (`StandardError`) are caught and surfaced via
241
+ `Kernel.warn`; subsequent events still fire. (M-S3, v1 plan)
242
+
243
+ - `bin/assistant-rbs` (shipped as `exe/assistant-rbs`) — a CLI that
244
+ loads user-supplied Ruby paths and emits an `.rbs` file per
245
+ `Assistant::Service` subclass into a configurable output directory
246
+ (default `sig/`). Each generated file declares the per-input getter
247
+ (`def <name>: () -> Type`) and predicate (`def <name>?: () -> bool`)
248
+ pairs derived from `Service.input_definitions`, including multi-type
249
+ unions (`(A | B)`) and `allow_nil:` (`(A | B)?`). Output is marked
250
+ with a header sentinel and is idempotent: rerunning leaves unchanged
251
+ files alone (`[unchanged]`) and refuses to overwrite hand-written
252
+ `.rbs` files that lack the sentinel (`[skipped]`). Namespaced classes
253
+ are emitted with nested `module` declarations so the generated file is
254
+ self-contained. Use `--output DIR`, `--quiet`, and `--help`. The
255
+ generator only emits sigs for `Service` subclasses introduced by the
256
+ paths it was asked to load (snapshot diff via `ObjectSpace`).
257
+ An `examples/greeter.rb` + generated `sig/examples/greeter.rbs`
258
+ fixture is type-checked by Steep as the acceptance test. The CLI
259
+ itself is Experimental; the generated `.rbs` content tracks the
260
+ Frozen `Service.input` surface. (M11, v1 plan)
261
+ - Hand-written RBS signatures for the frozen public surface defined in
262
+ `docs/v1/01-api-surface.md`: `Assistant::VERSION`, `Assistant::LogItem`,
263
+ `Assistant::LogList`, `Assistant::Service` (excluding the per-input
264
+ methods generated by `Service.input`), `Assistant::InputBuilder` plus
265
+ its `Registry`, `DefaultOption`, `OptionalOption`, `Accessors`,
266
+ `RequireValidator`, `TypeValidator`, and `Dsl` submodules, and a
267
+ namespace shim for `Assistant::Refinements::StringBlankness`. Files
268
+ live alongside the Ruby source as `lib/**/*.rbs` and ship with the
269
+ gem (already covered by `git ls-files`). A `Steepfile` adds a `:lib`
270
+ target type-checked by Steep in CI; `steep check` runs against the
271
+ subset of files that do not rely on Ruby refinements or
272
+ `define_method`. The per-input surface generated by `Service.input`
273
+ is documented in the RBS comments and will be emitted by
274
+ `bin/assistant-rbs` (M11). Adds `steep` as a development dependency
275
+ and a `steep` job to `.github/workflows/ci.yml`. (M8, v1 plan)
276
+ - `Assistant::Service.input` now accepts a `default:` option. The provider
277
+ may be a literal value or a zero-arity `Proc`/`Lambda`; anything else
278
+ that responds to `#call` (e.g. a `Method` object) is rejected with
279
+ `ArgumentError` at class-definition time. Procs are invoked once per
280
+ `Service` instance, with no arguments. A default fires when the input
281
+ key is absent, or when the value is an explicit `nil` and the input is
282
+ not declared `allow_nil: true` — with `allow_nil: true`, an explicit
283
+ `nil` from the caller is honoured and the default is skipped. Defaulted values
284
+ are subject to the same type, `required:`, and `if:` validation as
285
+ caller-supplied values. Mutable literal defaults (unfrozen `Array` /
286
+ `Hash`) emit a `Kernel.warn` at class-definition time, since they are
287
+ shared across every instance of the `Service` subclass. (M1, v1 plan)
288
+ - `Assistant::Service.input_definitions` — per-subclass hash exposing the
289
+ original `input` declaration options (including `:default`) for
290
+ introspection. Experimental; subject to change before 1.0.0.
291
+ - `Assistant::Service.input` now accepts `allow_nil: true`. When set,
292
+ any supplied value for that key short-circuits both `valid_type_<name>?`
293
+ and `valid_require_<name>?` — i.e. `nil` is accepted, and type-checking
294
+ is effectively disabled for the input. When `allow_nil:` is omitted
295
+ (default), behaviour is unchanged from 0.1.0 — an absent or `nil` value
296
+ silently passes type checks, and a `nil` on a `required:` input is
297
+ still treated as missing. (M2, v1 plan)
298
+ - `Assistant::Service.input` now accepts an array for `type:`, e.g.
299
+ `input :amount, type: [Integer, Float]`. The generated
300
+ `valid_type_<name>?` validator passes when the input matches **any** of
301
+ the listed types. Single-type declarations keep the original
302
+ `"… is not a X but Y"` error message; multi-type produces
303
+ `"… is not one of [A, B] but Y"`. (M3, v1 plan)
304
+ - `Assistant::Service#logs` public reader exposing the full log timeline
305
+ (info + warning + error) in insertion order. Callers no longer need to
306
+ reach into `@logs` via `instance_variable_get`. (M4, v1 plan)
307
+ - `Assistant::LogList#log_item_info`, `#log_item_warning`, and
308
+ `#log_item_error` shorthands. These wrap `add_log(level: ..., …)` so
309
+ service authors stop hand-rolling the level keyword on every call.
310
+ (M5, v1 plan)
311
+ - `Assistant::Service.input` now accepts an `optional:` flag. `optional: true`
312
+ is explicit sugar for the default behaviour (no `required:` validator is
313
+ generated); `optional: false` is equivalent to `required: true`. Declaring
314
+ `required: true` and `optional: true` together raises `ArgumentError` at
315
+ class-definition time, as does a non-boolean `optional:` value. The flag is
316
+ retained in `Service.input_definitions` for introspection and composes with
317
+ `default:` (M1) and `allow_nil:` (M2) without surprises. (M7, v1 plan)
318
+
319
+ ### Changed
320
+
321
+ - `Assistant::LogItem.new` now raises `ArgumentError` when constructed with
322
+ invalid attributes instead of returning an invalid object. Validation runs at
323
+ the end of initialization and reports every failing attribute in one message
324
+ (level, source, detail, message). The `#valid?` predicate family remains for
325
+ introspection and returns `true` for normally constructed instances.
326
+ `LogList#add_log` now inherits this fail-fast behaviour because it constructs
327
+ `LogItem` internally. (M10, v1 plan)
328
+ - `Assistant::InputBuilder` split into per-concern submodules under
329
+ `lib/assistant/input_builder/` (`Registry`, `DefaultOption`,
330
+ `OptionalOption`, `Accessors`, `RequireValidator`, `TypeValidator`,
331
+ `Dsl`). The umbrella `Assistant::InputBuilder` `include`s each
332
+ submodule; the public surface (`Service` extends `Assistant::InputBuilder`)
333
+ is unchanged. The `using Assistant::Refinements::StringBlankness`
334
+ refinement now activates only inside the `Accessors` submodule.
335
+ Tests mirror the lib layout under `test/assistant/input_builder/`.
336
+ Removes the temporary `Metrics/ModuleLength: Max: 150` override from
337
+ `.rubocop.yml`. (M13, v1 plan)
338
+ - For each `input :name, required: true` declaration, `Service` subclasses
339
+ now generate `#valid_required_<name>?` as the canonical requirement
340
+ validator (and `#valid_required_conditional_<name>?` when `if:` is also
341
+ given). The pre-existing `#valid_require_<name>?` / `#valid_require_conditional_<name>?`
342
+ predicates remain as deprecated aliases — they delegate to the
343
+ canonical method and emit a `Kernel.warn` once per textual call site
344
+ pointing at the canonical replacement. `Service#validate_inputs`
345
+ invokes only the canonical names, so internal framework code never
346
+ triggers the deprecation warning. See
347
+ [`docs/deprecations.md`](docs/deprecations.md). (M9, v1 plan)
348
+ - `lib/assistant.rb` now requires every core building block explicitly in
349
+ dependency order (`version`, `log_item`, `log_list`,
350
+ `refinements/string_blankness`, `input_builder`, `service`). After a bare
351
+ `require "assistant"`, `Assistant::LogList`, `Assistant::InputBuilder`, and
352
+ `Assistant::Refinements::StringBlankness` are reachable without first
353
+ loading `Assistant::Service`. (M6, v1 plan)
354
+
355
+ ### Deprecated
356
+
357
+ - `Assistant::Service#valid_require_<name>?` (use
358
+ `#valid_required_<name>?` instead). Scheduled for removal in
359
+ `assistant 2.0`. (M9, v1 plan)
360
+ - `Assistant::Service#valid_require_conditional_<name>?` (use
361
+ `#valid_required_conditional_<name>?` instead). Scheduled for removal
362
+ in `assistant 2.0`. (M9, v1 plan)
363
+
364
+ ### Migration
365
+
366
+ `1.0.0` is a stabilisation release. Three small breaking changes have
367
+ to be addressed; every one is mechanical and `git grep`-able. The full
368
+ recipe lives in
369
+ [`docs/v1/06-migration-0x-to-1.md`](docs/v1/06-migration-0x-to-1.md).
370
+
371
+ 1. **`LogList#merge_logs` is keyword-only (M12, B3)** — rewrite every
372
+ `merge_logs(other.logs)` call site to `merge_logs(logs: other.logs)`.
373
+ The two public DSL entry points `Service.input` and `Service.inputs`
374
+ keep their leading positional `attr_name` / `attr_names`; only
375
+ `merge_logs` and the internal `InputBuilder` helpers changed.
376
+ 2. **`LogItem.new` raises on invalid attrs (M10, B1)** — audit any
377
+ direct `LogItem.new(...)` call sites. The gem's own call sites are
378
+ already correct; fixtures that exercised the old "constructs but
379
+ `valid? == false`" path need updating. Prefer the `add_log` /
380
+ `log_item_*` helpers in regular code.
381
+ 3. **`valid_require_*?` is deprecated (M9, B2)** — rename direct calls
382
+ to the new `valid_required_*?` form. Users who don't call these
383
+ predicates directly (driven internally by `validate_inputs`) need
384
+ no source change; the old name still works in 1.x with a one-time
385
+ `Kernel.warn` per call site, and is removed in 2.0.
386
+
387
+ Pin to `~> 1.0` in your `Gemfile` once the upgrade lands.
388
+
389
+ ## [0.1.0] - 2026-05-07
390
+
391
+ ### Added
392
+
393
+ - `LogList#log_item_error_initialize` helper, used by `InputBuilder`-generated
394
+ validators (previously redefined on every `input` declaration).
395
+ - GitHub Actions CI workflow (`.github/workflows/ci.yml`) running Minitest and
396
+ RuboCop.
397
+ - GitHub Actions release workflow (`.github/workflows/release.yml`) using
398
+ RubyGems trusted publishing (OIDC) on `v*.*.*` tags.
399
+ - Direct test coverage for `LogList#warnings`, `#errors`, `#merge_logs`,
400
+ `Service#success?`, `#failure?`, `#status`, `#result` memoization,
401
+ conditional requirement behavior, the `inputs(...)` plural DSL form, and
402
+ `LogItem#trace`/`#item`.
403
+
404
+ ### Changed
405
+
406
+ - Standardized on Ruby 3.4 (`.ruby-version`, gemspec `required_ruby_version`,
407
+ RuboCop `TargetRubyVersion`).
408
+ - `InputBuilder` no longer requires `active_support`; the previous use of
409
+ `Object#present?` is replaced with plain Ruby checks. Whitespace-only
410
+ strings continue to be treated as missing via a scoped
411
+ `Assistant::Refinements::StringBlankness` refinement that adds
412
+ `String#whitespace?` and is activated inside `InputBuilder`. The method is
413
+ intentionally named to avoid colliding with ActiveSupport's `String#blank?`.
414
+ - `assistant.gemspec` `changelog_uri` now points at `CHANGELOG.md` instead of
415
+ `CODE_OF_CONDUCT.md`.
416
+ - Migrated the test suite from RSpec to Minitest (`test/**/*_test.rb`),
417
+ exposed via `rake test` (the new default rake task).
418
+ - Replaced the largely-dead RuboCop config (a fork of RuboCop's own internal
419
+ config) with a focused configuration for this gem; `rubocop-rspec` is
420
+ replaced with `rubocop-minitest`.
421
+
422
+ ### Removed
423
+
424
+ - CircleCI configuration (`.circleci/`); replaced by GitHub Actions.
425
+ - Dead `@keys = []` instance variable in `Assistant::Service#initialize`.
426
+ - `active_support` and `active_support/core_ext/object` requires from
427
+ `lib/assistant/input_builder.rb`.
428
+ - RSpec, FactoryBot, Faker, `rspec-collection_matchers`,
429
+ `rspec_junit_formatter`, `rubocop-faker`, and `rubocop-rspec` development
430
+ dependencies; replaced by `minitest` and `rubocop-minitest`.
431
+
432
+ ## [0.0.2] - 2023-11-27
433
+
434
+ - Initial public release.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,131 @@
1
+ <!-- markdownlint-disable MD013 -->
2
+ # Contributing to `assistant`
3
+
4
+ Thanks for taking the time to contribute. `assistant` is a small, dependency-free
5
+ service-object gem and it intends to stay that way; please read this guide
6
+ end-to-end before opening your first pull request.
7
+
8
+ By participating you agree to abide by the project's
9
+ [Code of Conduct](./CODE_OF_CONDUCT.md).
10
+
11
+ ## Quick start
12
+
13
+ ```sh
14
+ git clone https://github.com/ramongr/assistant.git
15
+ cd assistant
16
+ bin/setup # bundle install + any future bootstrap steps
17
+ bundle exec rake # default task runs the test suite
18
+ ```
19
+
20
+ If `bin/console` is more your speed, that boots an IRB session with the gem
21
+ preloaded.
22
+
23
+ ## `bin/` developer scripts
24
+
25
+ The three checked-in scripts under `bin/` are developer conveniences. They
26
+ are smoke-tested on every CI run by the `bin/ scripts smoke` job, so they
27
+ should always work on a fresh clone.
28
+
29
+ | Script | What it does | Usage |
30
+ |---------------|--------------------------------------------------------------------|------------------------------------------------|
31
+ | `bin/setup` | Bash wrapper that runs `bundle install` on a cold checkout. | `./bin/setup` |
32
+ | `bin/console` | Boots IRB with `bundler/setup` + `require 'assistant'` preloaded. | `bin/console` |
33
+ | `bin/version` | Bumps `Assistant::VERSION` in `lib/assistant/version.rb`. | `bin/version --patch` &#124; `--minor` &#124; `--major` &#124; `--help` |
34
+
35
+ These scripts are **not packaged with the gem** — they live under `bin/`,
36
+ not `exe/`. The only executable that ships in the gem is
37
+ `exe/assistant-rbs` (M11). See [`assistant.gemspec`](./assistant.gemspec)
38
+ for the `spec.executables` derivation.
39
+
40
+ ## Local checks
41
+
42
+ Before you push, run the full local pipeline. The CI pipeline mirrors these
43
+ tools and rejects pull requests that do not match.
44
+
45
+ ```sh
46
+ bundle exec rake test # Minitest
47
+ bundle exec rubocop # style + lint
48
+ bundle exec steep check # RBS / type-check
49
+ bundle exec rake ci # convenience aggregate (test + rubocop + steep)
50
+ ```
51
+
52
+ SimpleCov runs automatically as part of `rake test` and writes its report to
53
+ `coverage/`. Coverage is reported in CI but is **not a hard gate**; the long-
54
+ term targets (≥98% line, ≥95% branch) are documented in
55
+ [`docs/v1/05-quality-and-tooling.md`](./docs/v1/05-quality-and-tooling.md).
56
+
57
+ ## Branch naming
58
+
59
+ | Branch | Use it for |
60
+ |------------------------------|------------------------------------------------------------|
61
+ | `feature/m<n>-<slug>` | A roadmap milestone from `docs/v1/02-features.md`. |
62
+ | `feature/m-s<n>-<slug>` | A promoted "Should" item (M-S1, M-S2, …). |
63
+ | `docs/<slug>` | Documentation-only changes (D1–D5, status sweeps, guides). |
64
+ | `chore/<slug>` | Tooling / housekeeping with no roadmap milestone. |
65
+ | `fix/<slug>` or `bug/<slug>` | Bug fixes that are not part of a milestone. |
66
+ | `refactor/<slug>` | Internal refactors with no behavior change. |
67
+
68
+ ## Commit message style
69
+
70
+ ```
71
+ <TAG>: <imperative summary, ≤72 chars>
72
+
73
+ <wrapped body explaining the WHAT and the WHY (not the HOW).
74
+ Reference roadmap milestones or issues by ID.>
75
+ ```
76
+
77
+ `<TAG>` is one of:
78
+
79
+ - `M<n>` — a roadmap milestone (e.g. `M11: bin/assistant-rbs per-class RBS
80
+ generator`). Look up the number in [`docs/v1/02-features.md`](./docs/v1/02-features.md).
81
+ - `M-S<n>` — a promoted "Should" item.
82
+ - `D<n>` — a documentation milestone from
83
+ [`docs/v1/03-documentation.md`](./docs/v1/03-documentation.md).
84
+ - `chore:`, `docs:`, `refactor:`, `fix:` — when the change does not map to a
85
+ roadmap entry.
86
+
87
+ Wrap the body at ~72 columns. Do **not** paste tool output or file lists;
88
+ `git diff` already shows that.
89
+
90
+ ## Pull requests
91
+
92
+ Open the PR against `main`. The PR description follows the template at
93
+ [`.github/PULL_REQUEST_TEMPLATE.md`](./.github/PULL_REQUEST_TEMPLATE.md) and
94
+ should always cover:
95
+
96
+ - **Scope** — which milestone / issue this implements (link the roadmap line).
97
+ - **What this PR ships** — bullet list of the public-facing changes.
98
+ - **Verification** — paste the green tail of `rake test`, `rubocop`, and
99
+ `steep check` (or `rake ci`).
100
+ - **Out of scope** — what was deliberately left for later.
101
+
102
+ Each pull request must:
103
+
104
+ - [ ] Add or update a `CHANGELOG.md` entry under `[Unreleased]`.
105
+ - [ ] Add or update tests in `test/`.
106
+ - [ ] Update docs in `docs/` and the relevant `docs/v1/*.md` checklist if a
107
+ roadmap item is closing.
108
+ - [ ] Pass `bundle exec rake ci` locally.
109
+
110
+ CI will run on every push; the `Steep`, `RuboCop`, `Minitest (Ruby 3.4)`, and
111
+ `Minitest (Ruby 4.0)` checks are required by branch protection.
112
+
113
+ ## Reporting bugs
114
+
115
+ Open an issue at <https://github.com/ramongr/assistant/issues>. Please include:
116
+
117
+ - The version of `assistant` you are using.
118
+ - Your Ruby version.
119
+ - A minimal, runnable reproduction (ideally a service definition plus the
120
+ exact call that fails).
121
+ - The full `result` hash or backtrace.
122
+
123
+ For security-sensitive reports, follow [`SECURITY.md`](./SECURITY.md) instead
124
+ of the public tracker.
125
+
126
+ ## Code of Conduct
127
+
128
+ Everyone interacting in this project's codebases, issue trackers, chat rooms,
129
+ and mailing lists is expected to follow the
130
+ [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md). Reports go to
131
+ `cerberus.ramon@gmail.com`.
data/Gemfile CHANGED
@@ -6,3 +6,13 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
7
  # Specify your gem's dependencies in assistant.gemspec
8
8
  gemspec
9
+
10
+ # Documentation site (GitHub Pages) toolchain. Optional so regular
11
+ # contributors don't pull Jekyll on every `bundle install`. CI's Docs
12
+ # workflow installs this group via `BUNDLE_WITH=docs`. See
13
+ # `docs/v1/08-github-pages.md` and `Rakefile`'s `docs:*` tasks.
14
+ group :docs, optional: true do
15
+ gem 'jekyll', '~> 4.3'
16
+ gem 'jekyll-relative-links', '~> 0.7'
17
+ gem 'just-the-docs', '~> 0.10'
18
+ end