actionable-rb 1.9.0

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/.claude-plugin/marketplace.json +15 -0
  3. data/.claude-plugin/plugin.json +9 -0
  4. data/.rspec +3 -0
  5. data/AGENTS.md +150 -0
  6. data/CHANGELOG.md +766 -0
  7. data/CODE_OF_CONDUCT.md +132 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +555 -0
  10. data/UPGRADING.md +957 -0
  11. data/USAGE.md +1291 -0
  12. data/docs/getting_started.md +611 -0
  13. data/lib/actionable/action.rb +1220 -0
  14. data/lib/actionable/batch_result.rb +92 -0
  15. data/lib/actionable/errors.rb +36 -0
  16. data/lib/actionable/history.rb +91 -0
  17. data/lib/actionable/input_dispatch.rb +94 -0
  18. data/lib/actionable/job.rb +157 -0
  19. data/lib/actionable/measurement.rb +64 -0
  20. data/lib/actionable/rails/after_commit.rb +142 -0
  21. data/lib/actionable/rails/proxy_validator.rb +59 -0
  22. data/lib/actionable/rails/transactions.rb +95 -0
  23. data/lib/actionable/rails.rb +24 -0
  24. data/lib/actionable/railtie.rb +10 -0
  25. data/lib/actionable/rbs.rb +502 -0
  26. data/lib/actionable/registry.rb +64 -0
  27. data/lib/actionable/result.rb +166 -0
  28. data/lib/actionable/results/failure.rb +43 -0
  29. data/lib/actionable/results/skipped.rb +30 -0
  30. data/lib/actionable/results/success.rb +14 -0
  31. data/lib/actionable/rspec/coverage/report.rb +264 -0
  32. data/lib/actionable/rspec/coverage/tracker.rb +330 -0
  33. data/lib/actionable/rspec/coverage.rb +45 -0
  34. data/lib/actionable/rspec/matchers.rb +266 -0
  35. data/lib/actionable/rspec/stubs.rb +250 -0
  36. data/lib/actionable/rspec.rb +40 -0
  37. data/lib/actionable/rubocop/config.yml +8 -0
  38. data/lib/actionable/rubocop/cop/actionable/verb_not_last.rb +62 -0
  39. data/lib/actionable/rubocop/plugin.rb +31 -0
  40. data/lib/actionable/rubocop.rb +25 -0
  41. data/lib/actionable/runner.rb +300 -0
  42. data/lib/actionable/steps/action.rb +76 -0
  43. data/lib/actionable/steps/base.rb +107 -0
  44. data/lib/actionable/steps/case.rb +124 -0
  45. data/lib/actionable/steps/method.rb +39 -0
  46. data/lib/actionable/steps.rb +27 -0
  47. data/lib/actionable/value_match.rb +22 -0
  48. data/lib/actionable/verb_position.rb +574 -0
  49. data/lib/actionable/version.rb +6 -0
  50. data/lib/actionable-rb.rb +6 -0
  51. data/lib/actionable.rb +59 -0
  52. data/sig/actionable/dsl.rbs +42 -0
  53. data/sig/actionable/rails.rbs +16 -0
  54. data/sig/actionable.rbs +2593 -0
  55. data/skills/actionable/SKILL.md +214 -0
  56. metadata +132 -0
@@ -0,0 +1,2593 @@
1
+ # Actionable: typed, composable Ruby service objects.
2
+ #
3
+ # This is the core entry point. Per decision D14, every core file gets an
4
+ # explicit +require_relative+ here — no autoload, no Zeitwerk. The optional
5
+ # adapters (+actionable/rails+, +actionable/rspec+) are separate requires and
6
+ # are deliberately NOT loaded here, keeping the core Rails-free.
7
+ module Actionable
8
+ HALT: Symbol
9
+ VERSION: String
10
+
11
+ # Turn the first-run verb-position check (owner rulings R5, R10) on or
12
+ # off for the whole process. Off, {Action.run} never parses a method's
13
+ # source and never loads Prism; the per-call code check in +fail+ / +skip+
14
+ # stays on. Meant as an escape hatch while upgrading (set it in an
15
+ # initializer), with the +Actionable/VerbNotLast+ cop as the only guard
16
+ # left. A class first run while off is checked on its first run after the
17
+ # check is turned back on.
18
+ #
19
+ # Actionable.verb_position_check = false
20
+ #
21
+ # _@param_ `enabled` — +false+ to skip the check
22
+ #
23
+ # _@return_ — whether the check is now on
24
+ def self.verb_position_check=: (bool enabled) -> bool
25
+
26
+ # Whether {Action.run} checks where each step and hook method places its
27
+ # plain recording verbs on an action's first run. On by default.
28
+ def self.verb_position_check?: () -> bool
29
+
30
+ # Run +block+ inside the halt catch point.
31
+ #
32
+ # Returns the block's value when nothing is thrown, or the value passed with
33
+ # +throw Actionable::HALT, value+ when the pipeline halts (+nil+ for a bare
34
+ # +throw Actionable::HALT+). A raised StandardError is deliberately NOT
35
+ # caught — it propagates to the caller (decision D4).
36
+ #
37
+ # _@return_ — the block's value, or the thrown halt value
38
+ def catch_halt: () ?{ () -> Object } -> Object
39
+
40
+ # Run +block+ inside the halt catch point.
41
+ #
42
+ # Returns the block's value when nothing is thrown, or the value passed with
43
+ # +throw Actionable::HALT, value+ when the pipeline halts (+nil+ for a bare
44
+ # +throw Actionable::HALT+). A raised StandardError is deliberately NOT
45
+ # caught — it propagates to the caller (decision D4).
46
+ #
47
+ # _@return_ — the block's value, or the thrown halt value
48
+ def self.catch_halt: () ?{ () -> Object } -> Object
49
+
50
+ # _@return_ — the process-wide action registry
51
+ def self.registry: () -> Registry
52
+
53
+ # Runs an action inside a background job and maps its result to a queue
54
+ # disposition: a success or skip is done (+:ack+); a failure either retries
55
+ # (transient) or is discarded (permanent). Genuine exceptions are never
56
+ # swallowed — they propagate so the queue's own retry/backoff handles them
57
+ # (invariant 2).
58
+ module Job
59
+ DEFAULT_PERMANENT_CODES: ::Array[Symbol]
60
+
61
+ # Map a result to a queue disposition.
62
+ #
63
+ # With +honor_retry: true+, a failure or skip whose metadata has a +retry:+
64
+ # key is decided by it instead: truthy is +:retry+, falsy is +:discard+ for
65
+ # a failure and +:ack+ for a skip. Results without the key, and successes,
66
+ # map as usual.
67
+ #
68
+ # _@param_ `result` — the action's result
69
+ #
70
+ # _@param_ `permanent` — failure codes treated as non-retryable
71
+ #
72
+ # _@param_ `honor_retry` — act on the result's +retry:+ metadata
73
+ #
74
+ # _@return_ — +:ack+ for a success or skip; +:discard+
75
+ # for a failure whose code is +permanent+; +:retry+ for any other failure
76
+ def disposition: (Result result, ?permanent: ::Array[Symbol], ?honor_retry: bool) -> Symbol
77
+
78
+ # Map a result to a queue disposition.
79
+ #
80
+ # With +honor_retry: true+, a failure or skip whose metadata has a +retry:+
81
+ # key is decided by it instead: truthy is +:retry+, falsy is +:discard+ for
82
+ # a failure and +:ack+ for a skip. Results without the key, and successes,
83
+ # map as usual.
84
+ #
85
+ # _@param_ `result` — the action's result
86
+ #
87
+ # _@param_ `permanent` — failure codes treated as non-retryable
88
+ #
89
+ # _@param_ `honor_retry` — act on the result's +retry:+ metadata
90
+ #
91
+ # _@return_ — +:ack+ for a success or skip; +:discard+
92
+ # for a failure whose code is +permanent+; +:retry+ for any other failure
93
+ def self.disposition: (Result result, ?permanent: ::Array[Symbol], ?honor_retry: bool) -> Symbol
94
+
95
+ # _@param_ `result`
96
+ #
97
+ # _@return_ — whether the result carries +retry:+ metadata
98
+ def retry_declared?: (Result result) -> bool
99
+
100
+ # _@param_ `result`
101
+ #
102
+ # _@return_ — whether the result carries +retry:+ metadata
103
+ def self.retry_declared?: (Result result) -> bool
104
+
105
+ # _@param_ `result` — a result whose metadata has +retry:+
106
+ def metadata_disposition: ((Failure | Skipped) result) -> Symbol
107
+
108
+ # _@param_ `result` — a result whose metadata has +retry:+
109
+ def self.metadata_disposition: ((Failure | Skipped) result) -> Symbol
110
+
111
+ # Raised by {Mixin#run_actionable} for a retryable failure, so the host
112
+ # queue (Sidekiq / ActiveJob) sees a raised error and applies its retry
113
+ # policy. Carries the offending result.
114
+ class RetryableFailure < StandardError
115
+ # _@param_ `result` — a {Failure}, or a {Skipped} retried because its metadata says +retry: true+ (see {RetryMetadata#honor_retry_metadata})
116
+ def initialize: (Result result) -> void
117
+
118
+ # _@return_ — the failed result that triggered the retry
119
+ attr_reader result: Result
120
+ end
121
+
122
+ # The +honor_retry_metadata+ class macro (owner ruling P10). Extended onto
123
+ # {Actionable::Action} by +actionable/job+, so an action can opt in to having
124
+ # {Mixin#run_actionable} act on the +retry:+ metadata of its declared codes.
125
+ # Metadata stays neutral by default (decisions D20/D21).
126
+ module RetryMetadata
127
+ # Opt this action in (or, with +enabled: false+, back out) to {Job}
128
+ # honoring the +retry:+ metadata of its declared failures and skips:
129
+ # +retry: true+ retries the job, +retry: false+ does not. A code with no
130
+ # +retry:+ key keeps the default mapping. Inherited; a subclass can turn
131
+ # it off.
132
+ #
133
+ # class Reindex < Actionable::Action
134
+ # honor_retry_metadata
135
+ # skips :not_ready, retry: true # the job retries until the index is ready
136
+ # end
137
+ #
138
+ # _@param_ `enabled`
139
+ def honor_retry_metadata: (?enabled: bool) -> void
140
+
141
+ # Whether {Job} honors this action's +retry:+ metadata, inherited from
142
+ # ancestors when not set locally. +false+ unless an ancestor opted in.
143
+ def honor_retry_metadata?: () -> bool
144
+ end
145
+
146
+ # Mix into a Sidekiq worker or ActiveJob job. Call {#run_actionable} from
147
+ # +perform+; it runs the action, records the result on {#actionable_result},
148
+ # and raises {RetryableFailure} for a retryable failure (so the queue
149
+ # retries) — returning normally for +:ack+ and +:discard+.
150
+ #
151
+ # class ProjectEventJob
152
+ # include Sidekiq::Job
153
+ # include Actionable::Job::Mixin
154
+ # def perform(event_id) = run_actionable(ProjectApiEvent, event_id: event_id)
155
+ # end
156
+ module Mixin
157
+ # Run +action_class+ with the given arguments, map the outcome, and either
158
+ # return the result (+:ack+ / +:discard+) or raise {RetryableFailure}
159
+ # (+:retry+). A genuine exception inside the action propagates unchanged.
160
+ # When the action opted in with +honor_retry_metadata+, its declared
161
+ # +retry:+ metadata decides (see {Job.disposition}).
162
+ #
163
+ # _@param_ `action_class`
164
+ #
165
+ # _@param_ `args` — positional arguments, forwarded to +.run+
166
+ #
167
+ # _@param_ `kwargs` — keyword arguments, forwarded to +.run+
168
+ #
169
+ # _@return_ — the run's result (for an ack/discard outcome)
170
+ def run_actionable: (singleton(Actionable::Action) action_class, *Object args, **Object kwargs) -> Result
171
+
172
+ # _@return_ — failure codes this job treats as non-retryable;
173
+ # override to customize. Defaults to {DEFAULT_PERMANENT_CODES}.
174
+ def actionable_permanent_codes: () -> ::Array[Symbol]
175
+
176
+ # _@return_ — the result of the most recent {#run_actionable}
177
+ attr_reader actionable_result: Result?
178
+ end
179
+ end
180
+
181
+ # Generates RBS for a *user* action class — track 2 of the RBS strategy
182
+ # (decision D13). Sord covers the library's static classes, but it can't see
183
+ # +CreateInvoice.run(amount:)+ or +result.invoice+: those depend on the
184
+ # action's declared +input+/+output+ schemas. This walks those schemas (via
185
+ # FieldStruct's metadata and +FieldStruct::RBS.type_for+) and emits:
186
+ #
187
+ # * a typed +.run+ signature from the input schema (or +(*untyped)+ when
188
+ # free-form), with an overload taking a built input struct;
189
+ # * for a fixed +input+ schema, a nested +InputSchema+ struct and a typed
190
+ # +#input+ reader returning it. Like +Output+ and +Result+ it exists only
191
+ # in RBS, so generation raises +ArgumentError+ when Ruby already resolves
192
+ # +Action::InputSchema+;
193
+ # * a nullable instance variable per output field (+@total: ::Integer?+),
194
+ # so steps that assign it type-check. The generated RBS owns these: a
195
+ # host sig must not declare them again, since RBS rejects an instance
196
+ # variable declared twice;
197
+ # * a nested +Output+ struct with a typed accessor per output field;
198
+ # * a nested +Result+ with +output+ and the convenience-delegation readers,
199
+ # which +.run+ returns;
200
+ # * when the action declares failures with +fails+, a +failure_code+ type
201
+ # alias listing every code it can fail with: the declared ones, then the
202
+ # {Action::GEM_FAILURE_CODES} (decision D20);
203
+ # * when the action declares skips with +skips+, a +skip_code+ type alias
204
+ # listing every code it can skip with (decision D21). The gem emits no skip
205
+ # codes of its own, so the alias holds the declared ones only.
206
+ #
207
+ # Under typed outcomes (decision D23) +.run+ returns the action's own
208
+ # +Success | Skipped | Failure+ instead, each typed with its +code+ and an
209
+ # +output+ that names what the outcome can carry: a nested class per code
210
+ # that declares an output schema (+AlreadyActiveSkipOutput+,
211
+ # +InvalidFailureOutput+), and a Hash for a code without one. A failure's
212
+ # output also admits every skip schema and +Output+, since a broken one ends
213
+ # the run as +invalid_output+ carrying it.
214
+ #
215
+ # class CreateInvoice < Actionable::Action
216
+ # input { required :amount, :integer }
217
+ # output { required :invoice, :string }
218
+ # end
219
+ #
220
+ # puts Actionable::RBS.generate(CreateInvoice)
221
+ # # class CreateInvoice < ::Actionable::Action
222
+ # # def self.run: (amount: ::Integer) -> Result
223
+ # # | (InputSchema input) -> Result
224
+ # # def input: () -> InputSchema
225
+ # # @invoice: ::String?
226
+ # # class InputSchema < ::FieldStruct::Base ... end
227
+ # # class Output < ::FieldStruct::Base ... end
228
+ # # class Result < ::Actionable::Result ... end
229
+ # # end
230
+ #
231
+ # The action names its real superclass, and every named ancestor up to
232
+ # {Actionable::Action} is declared before it, member-less, so the output
233
+ # resolves without the host generating its abstract bases.
234
+ #
235
+ # The emitted +Result+/+Output+/+InputSchema+ are generated type aids: at
236
+ # runtime +.run+ returns a {Success}/{Failure}/{Skipped} that responds to
237
+ # these via output delegation, and the schemas are anonymous classes. Field
238
+ # types come from +FieldStruct::RBS.type_for+, so they match FieldStruct's own
239
+ # generator, and describe a valid instance. Referenced field types appear as
240
+ # qualified names (+::Invoice+) — generate RBS for those (e.g. via
241
+ # +FieldStruct::RBS+) so they resolve.
242
+ module RBS
243
+
244
+ # Generate RBS source for a user action class.
245
+ #
246
+ # _@param_ `klass`
247
+ #
248
+ # _@return_ — RBS for the action, wrapped in its module nesting
249
+ def self.generate: (singleton(Actionable::Action) klass) -> String
250
+
251
+ # The generated input type is named +InputSchema+, nested in the action.
252
+ # It exists only in RBS, so a Ruby constant that +klass::InputSchema+
253
+ # already resolves to (on the action, an ancestor class or an included
254
+ # module; +Object+'s top-level constants don't resolve through +klass::+)
255
+ # would make the RBS name lie.
256
+ #
257
+ # _@param_ `klass`
258
+ def self.check_input_schema_name!: (singleton(Actionable::Action) klass) -> void
259
+
260
+ # _@param_ `klass`
261
+ #
262
+ # _@return_ — whether the generated RBS declares +InputSchema+: for a
263
+ # fixed +input+ schema, not a discriminated +input_for+ one
264
+ def self.input_schema_class?: (singleton(Actionable::Action) klass) -> bool
265
+
266
+ # Declares every named ancestor between +klass+ and {Actionable::Action},
267
+ # so the action's superclass resolves on its own: a host often never
268
+ # generates its abstract base actions. Each is member-less, except that a
269
+ # typed ancestor also declares its result classes empty, for a typed
270
+ # subclass's result classes to subclass. Being empty, each declaration
271
+ # merges with the ancestor's full signature when that is generated too, and
272
+ # repeats byte-identically across the subclasses of one base, which RBS
273
+ # accepts across files.
274
+ #
275
+ # _@param_ `klass`
276
+ #
277
+ # _@return_ — one namespaced block per ancestor, outermost first
278
+ def self.ancestor_blocks: (singleton(Actionable::Action) klass) -> String
279
+
280
+ # _@param_ `ancestor`
281
+ #
282
+ # _@return_ — the ancestor's member-less class block
283
+ def self.ancestor_block: (singleton(Actionable::Action) ancestor) -> String
284
+
285
+ # _@param_ `klass`
286
+ #
287
+ # _@return_ — the named classes strictly
288
+ # between +klass+ and {Actionable::Action}, nearest first
289
+ def self.named_ancestors: (singleton(Actionable::Action) klass) -> ::Array[singleton(Actionable::Action)]
290
+
291
+ # _@param_ `klass`
292
+ #
293
+ # _@return_ — +class Name < ::Superclass+, naming the nearest named
294
+ # superclass (anonymous ones have no RBS name)
295
+ def self.class_head: (singleton(Actionable::Action) klass) -> String
296
+
297
+ # _@param_ `parent`
298
+ #
299
+ # _@return_ — whether a subclass's result classes subclass +parent+'s
300
+ def self.typed_parent?: (Class parent) -> bool
301
+
302
+ # _@param_ `klass`
303
+ def self.class_block: (singleton(Actionable::Action) klass) -> String
304
+
305
+ # The class block of an action with typed outcomes (decision D23).
306
+ #
307
+ # _@param_ `klass`
308
+ def self.typed_class_block: (singleton(Actionable::Action) klass) -> String
309
+
310
+ # The sections every action's class block opens with: the code aliases,
311
+ # +.run+, the typed +#input+ reader, the output instance variables, and the
312
+ # +InputSchema+ class.
313
+ #
314
+ # _@param_ `klass`
315
+ #
316
+ # _@param_ `return_type` — what +.run+ returns
317
+ def self.leading_sections: (singleton(Actionable::Action) klass, String return_type) -> ::Array[String]
318
+
319
+ # One instance variable per output field, which steps assign and the
320
+ # {Runner} captures into the output. Each is nullable: it is unset until a
321
+ # step assigns it. The generator owns these declarations, since RBS rejects
322
+ # the same instance variable declared in two files.
323
+ #
324
+ # _@param_ `schema` — the success output schema
325
+ def self.output_ivar_lines: (singleton(FieldStruct::Base) schema) -> ::Array[String]
326
+
327
+ # The nested output class of every skip, then every failure, code that
328
+ # declares an output schema, named after the code.
329
+ #
330
+ # _@param_ `klass`
331
+ #
332
+ # _@return_ — name, schema
333
+ def self.outcome_schemas: (singleton(Actionable::Action) klass) -> ::Array[[String, singleton(FieldStruct::Base)]]
334
+
335
+ # Every output class name must be an RBS class name, and unique: two codes
336
+ # that camelize alike (+:ready+ / +:ready_+, +:expired?+ /
337
+ # +:expired_predicate+) would otherwise emit one class for two schemas.
338
+ #
339
+ # _@param_ `klass`
340
+ #
341
+ # _@param_ `kind` — +skip+ or +failure+
342
+ #
343
+ # _@param_ `entries` — code, class name, schema
344
+ def self.check_outcome_names!: (singleton(Actionable::Action) klass, String kind, ::Array[[Symbol, String, singleton(FieldStruct::Base)]] entries) -> void
345
+
346
+ # The +Success+, +Skipped+ and +Failure+ blocks of a typed action.
347
+ #
348
+ # _@param_ `klass`
349
+ def self.outcome_blocks: (singleton(Actionable::Action) klass) -> ::Array[String]
350
+
351
+ # The readers a result delegates to its output struct, one per field any
352
+ # of +schemas+ declares. A field several declare types as the union of
353
+ # their types, nullable when any declares it optional, or when +nullable+.
354
+ # A failure's readers are all nullable: an +invalid_output+ failure carries
355
+ # the output that broke its schema, so a required field can be missing.
356
+ #
357
+ # _@param_ `schemas`
358
+ #
359
+ # _@param_ `nullable` — make every reader nullable
360
+ def self.delegator_lines: (::Array[singleton(FieldStruct::Base)] schemas, ?nullable: bool) -> ::Array[String]
361
+
362
+ # _@param_ `klass`
363
+ #
364
+ # _@return_ — the +Success+ block's signatures
365
+ def self.success_lines: (singleton(Actionable::Action) klass) -> ::Array[String]
366
+
367
+ # _@param_ `declared` — the declared codes
368
+ #
369
+ # _@param_ `name` — the alias naming them
370
+ #
371
+ # _@return_ — the alias, or +::Symbol+ when none are declared
372
+ def self.code_type: (::Hash[untyped, untyped] declared, String name) -> String
373
+
374
+ # _@param_ `types` — the output classes an outcome can carry
375
+ #
376
+ # _@return_ — them and a Hash, as one RBS type
377
+ def self.union: (::Array[String] types) -> String
378
+
379
+ # _@param_ `klass`
380
+ #
381
+ # _@param_ `name` — +Success+, +Skipped+ or +Failure+
382
+ #
383
+ # _@param_ `lines` — the block's method signatures
384
+ #
385
+ # _@return_ — the class block, subclassing the parent action's own
386
+ # result class when the parent has typed outcomes, else the gem's
387
+ def self.outcome_block: (singleton(Actionable::Action) klass, String name, ::Array[String] lines) -> String
388
+
389
+ # _@param_ `parent` — the superclass of the action being generated
390
+ #
391
+ # _@param_ `name` — +Success+, +Skipped+ or +Failure+
392
+ #
393
+ # _@return_ — the parent's result class when it has typed outcomes,
394
+ # else the gem's
395
+ def self.outcome_base: (Class parent, String name) -> String
396
+
397
+ # A code's trailing +?+ / +!+ becomes +Predicate+ / +Bang+, since neither
398
+ # may appear in a constant; +:x+ and +:x?+ get different names.
399
+ #
400
+ # _@param_ `code`
401
+ #
402
+ # _@return_ — +:already_active+ as +AlreadyActive+, +:expired?+ as
403
+ # +ExpiredPredicate+, +:done!+ as +DoneBang+
404
+ def self.camelize: (Symbol code) -> String
405
+
406
+ # _@param_ `klass`
407
+ #
408
+ # _@param_ `return_type` — what +.run+ returns
409
+ #
410
+ # _@return_ — the +.run+ signature: typed kwargs from the input schema
411
+ # (or +(*untyped)+ free-form), plus an overload taking a built
412
+ # +InputSchema+ instance, which +.run+ passes through
413
+ def self.run_signature: (singleton(Actionable::Action) klass, return_type: String) -> String
414
+
415
+ # _@param_ `klass` — an action that declares failures
416
+ #
417
+ # _@return_ — the +failure_code+ alias: a union of the declared codes
418
+ # and the codes the gem emits itself, as Symbol literal types
419
+ def self.failure_code_alias: (singleton(Actionable::Action) klass) -> String
420
+
421
+ # _@param_ `klass` — an action that declares skips
422
+ #
423
+ # _@return_ — the +skip_code+ alias: a union of the declared skip codes
424
+ # as Symbol literal types
425
+ def self.skip_code_alias: (singleton(Actionable::Action) klass) -> String
426
+
427
+ # _@param_ `field`
428
+ #
429
+ # _@return_ — +name: Type+ (required), +?name: Type+ (required with a
430
+ # default, so it may be omitted) or +?name: Type?+ (optional: it may be
431
+ # omitted or passed +nil+; +run(note: nil)+ succeeds).
432
+ def self.run_param: (FieldStruct::Field field) -> String
433
+
434
+ # A nested struct class with FieldStruct's reader and writer shape: the
435
+ # reader nullable when optional, the writer +(T? value) -> T?+.
436
+ #
437
+ # _@param_ `schema` — an output or input schema
438
+ #
439
+ # _@param_ `name` — the nested class name
440
+ #
441
+ # _@return_ — the nested class block
442
+ def self.output_block: (singleton(FieldStruct::Base) schema, ?String name) -> String
443
+
444
+ # _@param_ `schema` — the output schema
445
+ #
446
+ # _@return_ — the nested +Result+ class block with output delegation
447
+ def self.result_block: (singleton(FieldStruct::Base) schema) -> String
448
+
449
+ # The field's RBS type, as FieldStruct's own generator emits it: made
450
+ # nullable when optional, or as +nullable+ says (D13).
451
+ #
452
+ # _@param_ `field`
453
+ #
454
+ # _@param_ `nullable` — +nil+ follows the field's optionality
455
+ def self.rbs_type: (FieldStruct::Field field, ?nullable: bool?) -> String
456
+
457
+ # _@param_ `type` — an RBS type expression
458
+ #
459
+ # _@return_ — +type+ made nullable, unless it already admits +nil+
460
+ def self.nullable: (String _type) -> String
461
+
462
+ # Wrap a class block in the nesting implied by its qualified name,
463
+ # indenting two spaces per level. A namespace that is a class opens as
464
+ # +class+ (no superclass, so it merges with the class's own declaration),
465
+ # since RBS rejects a class and a module of the same name.
466
+ #
467
+ # _@param_ `full_name`
468
+ #
469
+ # _@param_ `inner`
470
+ #
471
+ # _@return_ — RBS source ending in a trailing newline
472
+ def self.wrap_namespace: (String full_name, String inner) -> String
473
+
474
+ # _@param_ `text`
475
+ #
476
+ # _@return_ — +text+ with every non-blank line indented two spaces
477
+ def self.indent: (String text) -> String
478
+ end
479
+
480
+ # RSpec matchers and stubs for actions (decision D12).
481
+ module RSpec
482
+ # The refusal for a stub, +and_fail+ or +and_skip+ naming a code an action
483
+ # that declares failures (decision D20) or skips (decision D21) doesn't
484
+ # declare.
485
+ #
486
+ # _@param_ `action_class`
487
+ #
488
+ # _@param_ `code`
489
+ #
490
+ # _@param_ `outcome` — which declarations the code was checked against
491
+ def self.undeclared_code_message: (singleton(Actionable::Action) action_class, (Symbol | String) code, ?outcome: Symbol) -> String
492
+
493
+ # Helpers mixed into example groups by +actionable/rspec+ that stub an
494
+ # action's +.run+ to return a real {Success} / {Failure} built from a hash,
495
+ # so callers can assert on +result.code+/+output+/+errors+ without executing
496
+ # the action (decision D12).
497
+ #
498
+ # +allow_*+ permits the call (a stub); +stub_*+ also sets a call expectation
499
+ # (a mock, verified on the example) — mirroring RSpec's allow/expect split.
500
+ #
501
+ # A stubbed failure of an action that declares failures with
502
+ # {Actionable::Action.fails} carries the declared metadata (and message,
503
+ # when none is given) for its code, and a code the action doesn't declare
504
+ # raises +ArgumentError+, so a stub can't return what the action never
505
+ # could (decision D20). A stubbed skip follows {Actionable::Action.skips}
506
+ # the same way (decision D21), the default +:skipped+ included.
507
+ module Stubs
508
+ # _@param_ `action_class` — the action whose +.run+ is stubbed
509
+ #
510
+ # _@param_ `opts` — {#actionable_success_result}'s keywords (+message:+, +output:+, +errors:+)
511
+ #
512
+ # _@return_ — the stubbed result
513
+ def allow_actionable_success: (singleton(Actionable::Action) action_class, **Object opts) -> Success
514
+
515
+ # _@param_ `action_class` — the action whose +.run+ is stubbed
516
+ #
517
+ # _@param_ `opts` — {#actionable_success_result}'s keywords (+message:+, +output:+, +errors:+)
518
+ #
519
+ # _@return_ — the stubbed result
520
+ def stub_actionable_success: (singleton(Actionable::Action) action_class, **Object opts) -> Success
521
+
522
+ # _@param_ `action_class` — the action whose +.run+ is stubbed
523
+ #
524
+ # _@param_ `opts` — {#actionable_failure_result}'s keywords; +code:+ defaults to +:error+
525
+ #
526
+ # _@return_ — the stubbed result
527
+ def allow_actionable_failure: (singleton(Actionable::Action) action_class, **Object opts) -> Failure
528
+
529
+ # _@param_ `action_class` — the action whose +.run+ is stubbed
530
+ #
531
+ # _@param_ `opts` — {#actionable_failure_result}'s keywords; +code:+ defaults to +:error+
532
+ #
533
+ # _@return_ — the stubbed result
534
+ def stub_actionable_failure: (singleton(Actionable::Action) action_class, **Object opts) -> Failure
535
+
536
+ # _@param_ `action_class` — the action whose +.run+ is stubbed
537
+ #
538
+ # _@param_ `opts` — {#actionable_skip_result}'s keywords; +code:+ defaults to +:skipped+
539
+ #
540
+ # _@return_ — the stubbed result
541
+ def allow_actionable_skip: (singleton(Actionable::Action) action_class, **Object opts) -> Skipped
542
+
543
+ # _@param_ `action_class` — the action whose +.run+ is stubbed
544
+ #
545
+ # _@param_ `opts` — {#actionable_skip_result}'s keywords; +code:+ defaults to +:skipped+
546
+ #
547
+ # _@return_ — the stubbed result
548
+ def stub_actionable_skip: (singleton(Actionable::Action) action_class, **Object opts) -> Skipped
549
+
550
+ # Build a real {Success} from a hash (no stubbing).
551
+ #
552
+ # _@param_ `message`
553
+ #
554
+ # _@param_ `output`
555
+ #
556
+ # _@param_ `errors`
557
+ def actionable_success_result: (?message: String?, ?output: (::Hash[Symbol, Object] | FieldStruct::Base), ?errors: ::Hash[Symbol, (String | ::Array[String])]) -> Success
558
+
559
+ # Build a real {Failure} from a hash (no stubbing).
560
+ #
561
+ # _@param_ `code`
562
+ #
563
+ # _@param_ `message`
564
+ #
565
+ # _@param_ `output`
566
+ #
567
+ # _@param_ `errors`
568
+ #
569
+ # _@param_ `metadata` — the failure's per-code metadata
570
+ def actionable_failure_result: (
571
+ ?code: Symbol,
572
+ ?message: String?,
573
+ ?output: (::Hash[Symbol, Object] | FieldStruct::Base),
574
+ ?errors: ::Hash[Symbol, (String | ::Array[String])],
575
+ ?metadata: ::Hash[Symbol, Object]
576
+ ) -> Failure
577
+
578
+ # Build a real {Skipped} from a hash (no stubbing).
579
+ #
580
+ # _@param_ `code`
581
+ #
582
+ # _@param_ `message`
583
+ #
584
+ # _@param_ `output`
585
+ #
586
+ # _@param_ `metadata` — the skip's per-code metadata
587
+ def actionable_skip_result: (
588
+ ?code: Symbol,
589
+ ?message: String?,
590
+ ?output: (::Hash[Symbol, Object] | FieldStruct::Base),
591
+ ?metadata: ::Hash[Symbol, Object]
592
+ ) -> Skipped
593
+
594
+ # A {Failure} for +action_class+: the declared metadata and message for
595
+ # its code, or a refusal when the action declares failures but not it.
596
+ #
597
+ # _@param_ `action_class`
598
+ #
599
+ # _@param_ `code`
600
+ #
601
+ # _@param_ `opts` — the rest of {#actionable_failure_result}'s keywords
602
+ def declared_failure_result: (singleton(Actionable::Action) action_class, ?code: (Symbol | String), **Object opts) -> Failure
603
+
604
+ # A {Skipped} for +action_class+: the declared metadata and message for
605
+ # its code, or a refusal when the action declares skips but not it.
606
+ #
607
+ # _@param_ `action_class`
608
+ #
609
+ # _@param_ `code`
610
+ #
611
+ # _@param_ `opts` — the rest of {#actionable_skip_result}'s keywords
612
+ def declared_skip_result: (singleton(Actionable::Action) action_class, ?code: (Symbol | String), **Object opts) -> Skipped
613
+
614
+ # Permit +action_class.run+, answering +result+ as a real run would.
615
+ #
616
+ # _@param_ `action_class`
617
+ #
618
+ # _@param_ `result`
619
+ #
620
+ # _@return_ — the result the stub answers
621
+ def allow_run: (singleton(Actionable::Action) action_class, Result result) -> Result
622
+
623
+ # Expect +action_class.run+ (verified on the example), answering +result+
624
+ # as a real run would.
625
+ #
626
+ # _@param_ `action_class`
627
+ #
628
+ # _@param_ `result`
629
+ #
630
+ # _@return_ — the result the stub answers
631
+ def expect_run: (singleton(Actionable::Action) action_class, Result result) -> Result
632
+
633
+ # The stubbed result as a real run would return it (decision D23): its
634
+ # output shaped by the schema it is held to, then as the action's own
635
+ # result class when it has typed outcomes.
636
+ #
637
+ # _@param_ `action_class`
638
+ #
639
+ # _@param_ `result`
640
+ def as_action_result: (singleton(Actionable::Action) action_class, Result result) -> Result
641
+
642
+ # Mirror the runner: output for a code with a schema becomes that schema
643
+ # (refused when it breaks it, since a real run would end as
644
+ # +invalid_output+ instead), and under typed outcomes a code without one
645
+ # carries an empty output (refused when output is given, as a real run
646
+ # refuses it). The gem's own failure codes keep what they are given.
647
+ #
648
+ # _@param_ `action_class`
649
+ #
650
+ # _@param_ `result` — updated in place
651
+ def shape_output: (singleton(Actionable::Action) action_class, Result result) -> void
652
+
653
+ # _@param_ `action_class`
654
+ #
655
+ # _@param_ `result`
656
+ #
657
+ # _@return_ — the schema the result's output
658
+ # is held to: its code's, or the success schema under typed outcomes
659
+ def stubbed_output_schema: (singleton(Actionable::Action) action_class, Result result) -> singleton(FieldStruct::Base)?
660
+
661
+ # The stub's output as an instance of +schema+, built from the fields of
662
+ # +result+'s output that the schema declares.
663
+ #
664
+ # _@param_ `action_class`
665
+ #
666
+ # _@param_ `result`
667
+ #
668
+ # _@param_ `schema`
669
+ def build_stubbed_output: (singleton(Actionable::Action) action_class, Result result, singleton(FieldStruct::Base) schema) -> FieldStruct::Base
670
+
671
+ # The stub's response: +result+, noted as a stubbed run when outcome
672
+ # coverage is loaded (decision D22), so a report can tell an outcome
673
+ # only ever stubbed from one never reached at all.
674
+ #
675
+ # _@param_ `action_class`
676
+ #
677
+ # _@param_ `result`
678
+ #
679
+ # _@return_ — +result+
680
+ def stubbed_run: (singleton(Actionable::Action) action_class, Result result) -> Result
681
+
682
+ # Add +errors+ to +result+'s errors.
683
+ #
684
+ # _@param_ `result`
685
+ #
686
+ # _@param_ `errors`
687
+ #
688
+ # _@return_ — +result+
689
+ def apply_errors: (Result result, ::Hash[Symbol, (String | ::Array[String])] errors) -> Result
690
+ end
691
+
692
+ # RSpec matcher methods mixed into example groups by +actionable/rspec+.
693
+ module Matchers
694
+ # Run an action and assert on its outcome.
695
+ #
696
+ # expect(CreateInvoice).to perform_actionable(amount: 5).and_succeed
697
+ # expect(CreateInvoice).to perform_actionable(bad).and_fail(:invalid_input)
698
+ # expect(Boom).to perform_actionable.and_raise(RuntimeError)
699
+ #
700
+ # _@param_ `args` — positional args forwarded to +.run+
701
+ #
702
+ # _@param_ `kwargs` — keyword args forwarded to +.run+
703
+ def perform_actionable: (*Object args, **Object kwargs) ?{ (Result? result, StandardError? exception) -> Object } -> PerformActionable
704
+ end
705
+
706
+ # The matcher object returned by {Matchers#perform_actionable}. Runs the
707
+ # action under test and checks the configured outcome (decision D12). Bare
708
+ # +perform_actionable+ expects a success; chain +and_succeed+ / +and_fail+ /
709
+ # +and_raise+ to be explicit. Code and message expectations match with +===+,
710
+ # so exact values, Regexps, and classes all work.
711
+ class PerformActionable
712
+ DEFAULT_BACKTRACE_QTY: Integer
713
+
714
+ # _@param_ `args` — positional args forwarded to +.run+
715
+ #
716
+ # _@param_ `kwargs` — keyword args forwarded to +.run+
717
+ #
718
+ # _@param_ `block` — extra assertions, called with the result and exception
719
+ def initialize: (::Array[Object] args, ::Hash[Symbol, Object] kwargs, Proc? block) -> void
720
+
721
+ # Expect a successful result, optionally matching +message+.
722
+ #
723
+ # _@param_ `message` — matched against the result's message with +===+
724
+ def and_succeed: (?Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
725
+
726
+ # Expect a failed result, optionally matching +code+ and +message+. On an
727
+ # action that declares failures, a Symbol +code+ it doesn't declare raises
728
+ # +ArgumentError+ (decision D20).
729
+ #
730
+ # _@param_ `code` — matched against the result's code with +==+
731
+ #
732
+ # _@param_ `message` — matched against the result's message with +===+
733
+ def and_fail: (?Object? code, ?Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
734
+
735
+ # Expect a skipped result, optionally matching the reason +code+ and
736
+ # +message+ (decision D17). On an action that declares skips, a Symbol
737
+ # +code+ it doesn't declare raises +ArgumentError+ (decision D21).
738
+ #
739
+ # _@param_ `code` — matched against the result's code with +==+
740
+ #
741
+ # _@param_ `message` — matched against the result's message with +===+
742
+ def and_skip: (?Object? code, ?Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
743
+
744
+ # Expect the run to raise +error_class+, optionally matching +message+.
745
+ #
746
+ # _@param_ `error_class`
747
+ #
748
+ # _@param_ `message` — matched against the exception's message with +===+
749
+ def and_raise: (?singleton(Exception) error_class, ?Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
750
+
751
+ # _@param_ `action_class`
752
+ def matches?: (singleton(Actionable::Action) action_class) -> bool
753
+
754
+ # _@param_ `action_class`
755
+ def does_not_match?: (singleton(Actionable::Action) action_class) -> bool
756
+
757
+ def failure_message: () -> String
758
+
759
+ def failure_message_when_negated: () -> String
760
+
761
+ def description: () -> String
762
+
763
+ # _@param_ `outcome` — +:success+, +:failure+, +:skip+ or +:raise+
764
+ #
765
+ # _@param_ `message`
766
+ def configure: (Symbol outcome, Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
767
+
768
+ # An +and_fail+ or +and_skip+ code the action could never end with is a
769
+ # broken spec, not a failed expectation, so it raises rather than not
770
+ # matching.
771
+ def ensure_code_declared!: () -> void
772
+
773
+ # Run the action, keeping its result or the error it raised.
774
+ def perform: () -> void
775
+
776
+ def primary_match?: () -> bool?
777
+
778
+ def succeeded?: () -> bool?
779
+
780
+ def failed?: () -> bool?
781
+
782
+ def skipped?: () -> bool?
783
+
784
+ def raised?: () -> bool
785
+
786
+ # _@param_ `actual`
787
+ def message_matches?: (String? actual) -> bool
788
+
789
+ def block_match?: () -> bool
790
+
791
+ def expectation_phrase: () -> String?
792
+
793
+ def code_suffix: () -> String
794
+
795
+ def message_suffix: () -> String
796
+
797
+ def actual_phrase: () -> String
798
+
799
+ def formatted_backtrace: () -> String
800
+
801
+ def short_backtrace?: () -> bool
802
+
803
+ def backtrace_qty: () -> Integer
804
+ end
805
+ end
806
+
807
+ # Namespace for the concrete step types an action's pipeline is built from
808
+ # (decision D3): {Steps::Method} now, {Steps::Action} and {Steps::Case} later.
809
+ module Steps
810
+ # Build the right step type from a +step+ declaration's target, inferring
811
+ # the type from the argument (decision D2): a Symbol/String becomes a
812
+ # {Steps::Method}, an action +Class+ becomes a {Steps::Action}. The case
813
+ # block type ({Steps::Case}) joins in a later slice.
814
+ #
815
+ # _@param_ `target` — the step target
816
+ #
817
+ # _@param_ `options` — step options (e.g. +:if+, +:unless+, and for action steps +:input+ / +:expose+)
818
+ #
819
+ # _@return_ — the constructed step
820
+ def self.build: ((Symbol | String | singleton(Actionable::Action)) target, **Object options) -> Steps::Base
821
+
822
+ # Shared behavior for every step: a +name+, an +options+ hash, value
823
+ # identity for Set deduplication (keyed by +[type, name]+, decision D2),
824
+ # and the +:if+ / +:unless+ skip logic. Subclasses implement {#call}.
825
+ #
826
+ # @abstract Subclass and implement {#call}.
827
+ class Base
828
+ # _@param_ `name` — the step name (see {#name})
829
+ #
830
+ # _@param_ `options` — step options
831
+ def initialize: (Object name, **Object options) -> void
832
+
833
+ # Run the step against an action instance.
834
+ #
835
+ # _@param_ `_instance` — the action instance the step runs against
836
+ def call: (Object _instance) -> void
837
+
838
+ # Instance method names this step needs the action to implement, used by
839
+ # the run-start definition guard (decision D18). None for the base; concrete
840
+ # types that call instance methods override this.
841
+ def required_methods: () -> ::Array[Symbol]
842
+
843
+ # Instance methods whose bodies run as this step, for the recording-verb
844
+ # position check (owner ruling R5): not guards, not a case step's value
845
+ # source. None for the base; a nested action is checked on its own run.
846
+ def body_methods: () -> ::Array[Symbol]
847
+
848
+ # _@return_ — a short tag identifying the step type, for introspection
849
+ # (+Actionable::Action.describe+). Concrete types override it.
850
+ def kind: () -> Symbol
851
+
852
+ # Whether this step should be skipped for the given instance, per its
853
+ # +:if+ / +:unless+ guards. A guard is a Symbol naming a (possibly
854
+ # private) instance method, or a callable invoked with the instance.
855
+ # When both guards are present, either one asking to skip wins.
856
+ #
857
+ # _@param_ `instance` — the action instance to evaluate guards against
858
+ def skip?: (Object _instance) -> bool
859
+
860
+ # Value identity: same concrete class AND same name. This keys steps in
861
+ # the per-action Set so a redeclared step dedups (decision D2).
862
+ #
863
+ # _@param_ `other`
864
+ def ==: (Object other) -> bool
865
+
866
+ # _@return_ — consistent with {#==}, so steps work as Set members
867
+ def hash: () -> Integer
868
+
869
+ # Resolve a guard against the instance. A Symbol/String names an instance
870
+ # method (called via +__send__+, so private predicates work); anything
871
+ # else is treated as a callable receiving the instance.
872
+ #
873
+ # _@param_ `guard`
874
+ #
875
+ # _@param_ `instance`
876
+ def evaluate_guard: ((Symbol | String | untyped) guard, Object _instance) -> bool
877
+
878
+ # _@return_ — the step's name — a Symbol for a {Steps::Method},
879
+ # an action Class for a {Steps::Action}
880
+ attr_reader name: Object
881
+
882
+ # _@return_ — the declared step options (e.g. +:if+,
883
+ # +:unless+, and step-type-specific keys)
884
+ attr_reader options: ::Hash[Symbol, Object]
885
+ end
886
+
887
+ # A step that branches to one of several targets based on a value
888
+ # (decision D3, declared with +case_step+). The switch value is read from
889
+ # the action via the named method; each branch's target is an ordinary step
890
+ # (a method or a nested action), so branches compose like any other step.
891
+ #
892
+ # case_step :status do
893
+ # on 'active', :handle_active
894
+ # on %w[trial grace], :handle_trial # Array membership
895
+ # on(/\A4\d\d\z/, :handle_client_err # Regexp
896
+ # default :handle_unknown
897
+ # end
898
+ class Case < Actionable::Steps::Base
899
+ # Build a case step from a +case_step+ declaration.
900
+ #
901
+ # _@param_ `value_source` — the method whose value is switched on
902
+ #
903
+ # _@param_ `options` — step options (e.g. +:if+, +:unless+)
904
+ def self.define: ((Symbol | String) value_source, **Object options) -> Case
905
+
906
+ # _@param_ `value_source` — the method whose value is switched on
907
+ #
908
+ # _@param_ `branches` — ordered [match, step] pairs
909
+ #
910
+ # _@param_ `default_step` — the fallback step
911
+ #
912
+ # _@param_ `options` — step options
913
+ def initialize: (
914
+ (Symbol | String) value_source,
915
+ ::Array[[Object, Steps::Base]] branches,
916
+ Steps::Base? default_step,
917
+ **Object options
918
+ ) -> void
919
+
920
+ # Evaluate the switch value and run the first matching branch (or the
921
+ # default). No-op when nothing matches and no default was declared.
922
+ #
923
+ # The branch is chosen by value alone; its +:if+ / +:unless+ guards then
924
+ # decide whether its target runs, exactly as for a plain step. A matched
925
+ # branch whose guard opts out is a no-op: it does not fall through to a
926
+ # later branch or to the default.
927
+ #
928
+ # _@param_ `instance`
929
+ def call: (Actionable::Action _instance) -> void
930
+
931
+ # _@return_ — the switch-value method plus every branch
932
+ # target's required methods
933
+ def required_methods: () -> ::Array[Symbol]
934
+
935
+ # _@return_ — every branch target's body methods (never the
936
+ # switch-value method, which only returns the value)
937
+ def body_methods: () -> ::Array[Symbol]
938
+
939
+ def kind: () -> Symbol
940
+
941
+ # _@return_ — every branch's step, the default included
942
+ def branch_steps: () -> ::Array[Steps::Base]
943
+
944
+ # _@param_ `value` — the switch value
945
+ #
946
+ # _@return_ — the first matching branch's step
947
+ def match: (Object value) -> Steps::Base?
948
+
949
+ # Collects +on+/+default+ branch declarations inside a +case_step+ block.
950
+ class Builder
951
+ def initialize: () -> void
952
+
953
+ # Register a branch. +value+ matches the switch value by +==+, by
954
+ # +Regexp#match?+ when it is a Regexp, or by membership when it is an
955
+ # Array. +target+ is any step target (method name or nested action).
956
+ #
957
+ # _@param_ `value`
958
+ #
959
+ # _@param_ `target`
960
+ #
961
+ # _@param_ `options` — step options for the target (+:if+ / +:unless+ guard the branch: a matched branch whose guard opts out does nothing)
962
+ def on: ((Object | Regexp | ::Array[untyped]) value, (Symbol | String | singleton(Actionable::Action)) target, **Object options) -> void
963
+
964
+ # Register the fallback branch, run when no +on+ branch matches.
965
+ #
966
+ # _@param_ `target`
967
+ #
968
+ # _@param_ `options` — step options for the target
969
+ def default: ((Symbol | String | singleton(Actionable::Action)) target, **Object options) -> void
970
+
971
+ # _@return_ — ordered [match, step] pairs
972
+ attr_reader branches: ::Array[[Object, Steps::Base]]
973
+
974
+ # _@return_ — the fallback step, if +default+ was declared
975
+ attr_reader default_step: Steps::Base?
976
+ end
977
+ end
978
+
979
+ # A step that runs another action as part of this one (decision D3,
980
+ # triggered by an action +Class+ step argument).
981
+ #
982
+ # +:input+ threads named parent instance variables into the child's +.run+;
983
+ # +:expose+ limits which of the child's outputs are absorbed back as parent
984
+ # instance variables (default: all). A child failure fails the parent with
985
+ # the child's code, message, errors and metadata, and halts the remaining
986
+ # steps.
987
+ class Action < Actionable::Steps::Base
988
+ # Run the nested action, threading input in and absorbing output back.
989
+ #
990
+ # _@param_ `instance` — the parent action instance
991
+ def call: (Actionable::Action _instance) -> void
992
+
993
+ def kind: () -> Symbol
994
+
995
+ # _@param_ `instance`
996
+ #
997
+ # _@return_ — +{ name => @name }+ for each +:input+ name
998
+ def input_kwargs: (Actionable::Action _instance) -> ::Hash[Symbol, Object]
999
+
1000
+ # Copy the child's exposed output fields onto the parent as instance
1001
+ # variables, so the parent's later steps and output capture see them.
1002
+ # Only fields the child actually set are copied: a field it left nil or
1003
+ # at its default must not overwrite the parent's value.
1004
+ #
1005
+ # _@param_ `instance`
1006
+ #
1007
+ # _@param_ `child`
1008
+ def absorb: (Actionable::Action _instance, Result child) -> void
1009
+
1010
+ # _@param_ `output`
1011
+ #
1012
+ # _@return_ — the child output as a plain hash
1013
+ def output_hash: ((FieldStruct::Base | ::Hash[untyped, untyped] | Object) output) -> ::Hash[Symbol, Object]
1014
+ end
1015
+
1016
+ # A step that calls an instance method on the action (decision D3,
1017
+ # triggered by a Symbol/String step argument).
1018
+ class Method < Actionable::Steps::Base
1019
+ # _@param_ `name` — the instance method to call; normalized to a Symbol
1020
+ #
1021
+ # _@param_ `options` — step options (e.g. +:if+, +:unless+)
1022
+ def initialize: ((Symbol | String) name, **Object options) -> void
1023
+
1024
+ # Invoke the named method on the instance.
1025
+ #
1026
+ # _@param_ `instance` — the action instance
1027
+ #
1028
+ # _@return_ — the method's return value
1029
+ def call: (Object _instance) -> Object
1030
+
1031
+ # _@return_ — the instance method this step calls
1032
+ def required_methods: () -> ::Array[Symbol]
1033
+
1034
+ # _@return_ — the instance method whose body is this step
1035
+ def body_methods: () -> ::Array[Symbol]
1036
+
1037
+ def kind: () -> Symbol
1038
+ end
1039
+ end
1040
+
1041
+ # Base class for service objects. Subclass it, declare an ordered list of
1042
+ # steps with {.step}, implement each step as an instance method, and run it
1043
+ # with {.run} (decisions D1/D2).
1044
+ #
1045
+ # class Greet < Actionable::Action
1046
+ # step :build_message
1047
+ # def build_message = @message = 'hello'
1048
+ # end
1049
+ #
1050
+ # Greet.run # => #<Actionable::Success ...>
1051
+ class Action
1052
+ extend Actionable::Rails::Transactions
1053
+ extend Actionable::Job::RetryMetadata
1054
+ GEM_FAILURE_CODES: ::Array[Symbol]
1055
+ CORE_METHOD_OWNERS: ::Array[Module]
1056
+ CODE_FORMAT: Regexp
1057
+
1058
+ # The ordered, deduplicated set of steps declared on this action.
1059
+ # A +Set+ keyed by step identity ([type, name]) so a redeclared step
1060
+ # collapses to one entry; insertion order is preserved (decision D2).
1061
+ # The last declaration of a step wins and keeps the first one's position
1062
+ # (see {.step}).
1063
+ def self.steps: () -> ::Set[Steps::Base]
1064
+
1065
+ # Declare the action's typed output schema, or read it back.
1066
+ #
1067
+ # With a block, builds an anonymous +FieldStruct::Base+ subclass from the
1068
+ # FieldStruct DSL (+required+/+optional+/…) and stores it as this action's
1069
+ # output schema (decision D6). At the end of a run the {Runner} captures
1070
+ # the instance variables whose names match declared fields, coerces them
1071
+ # through the schema, and assigns the struct to +result.output+. Field names
1072
+ # in {RESERVED_OUTPUT_FIELDS} raise a {DefinitionError}.
1073
+ #
1074
+ # output do
1075
+ # required :invoice, Invoice
1076
+ # optional :receipt, Receipt
1077
+ # end
1078
+ #
1079
+ # _@return_ — the output schema, or +nil+ when
1080
+ # none has been declared (the free-form path)
1081
+ def self.output: () -> singleton(FieldStruct::Base)?
1082
+
1083
+ # Declare the action's typed input schema (decision D7). Builds an
1084
+ # anonymous +FieldStruct::Base+ subclass from the FieldStruct DSL; +.run+
1085
+ # then coerces its arguments into this schema, validates them, and exposes
1086
+ # the struct to steps via the instance's +input+ reader. Declaring input
1087
+ # takes over the constructor role — steps read +input.field+ rather than
1088
+ # capturing positional/keyword args in a custom +initialize+.
1089
+ #
1090
+ # input do
1091
+ # required :amount, :big_decimal
1092
+ # optional :name, :string
1093
+ # end
1094
+ #
1095
+ # Called without a block it reads the declared schema, as {.output} does.
1096
+ #
1097
+ # _@return_ — the input schema, or +nil+ when
1098
+ # none has been declared
1099
+ def self.input: () -> singleton(FieldStruct::Base)?
1100
+
1101
+ # Declare a *discriminated* input schema (decision D19): pick the input
1102
+ # FieldStruct at run time from a discriminator value, instead of a single
1103
+ # fixed schema. The block declares branches with +on(value, Schema)+ and an
1104
+ # optional +default(Schema)+; +.run+ reads the discriminator from its
1105
+ # keyword arguments, chooses the schema, then coerces and validates exactly
1106
+ # like a fixed +input+. An unmatched value with no +default+ short-circuits
1107
+ # to +Failure(:invalid_input)+.
1108
+ #
1109
+ # input_for :event_type do
1110
+ # on 'sale', SaleShape
1111
+ # on 'refund', RefundShape
1112
+ # default UnknownShape
1113
+ # end
1114
+ #
1115
+ # _@param_ `discriminator` — the kwarg whose value selects the schema
1116
+ #
1117
+ # _@return_ — the stored dispatch
1118
+ def self.input_for: ((Symbol | String) discriminator) -> InputDispatch
1119
+
1120
+ # Declare one failure code this action can end with, and the metadata a
1121
+ # failure with that code carries (decision D20).
1122
+ #
1123
+ # The metadata is neutral: arbitrary keywords the gem stores, attaches to
1124
+ # the {Failure} as +result.metadata+, and shows in {.describe}, but never
1125
+ # interprets. A host app gives the keys their meaning (say, +kind:+ for
1126
+ # how an adapter answers the failure). +message+ is the default message
1127
+ # for a +fail+ that gives none.
1128
+ #
1129
+ # fails :refused, 'the card was refused', kind: :rejected
1130
+ # fails :not_found, kind: :not_found
1131
+ #
1132
+ # Declarations are inherited and a subclass can add to them; redeclaring a
1133
+ # code replaces its message and metadata. Once an action declares any
1134
+ # code, failing with an undeclared one raises a {DefinitionError} at the
1135
+ # +fail+ call; the {GEM_FAILURE_CODES} are exempt.
1136
+ #
1137
+ # A block declares the output a failure with this code carries, in the
1138
+ # FieldStruct DSL of {.output} (decision D23); see {.failure_output_schema}.
1139
+ #
1140
+ # _@param_ `code` — the failure code
1141
+ #
1142
+ # _@param_ `message` — the default message for this code
1143
+ #
1144
+ # _@param_ `metadata` — per-code metadata, passed through
1145
+ #
1146
+ # _@return_ — {.declared_failures}
1147
+ def self.fails: ((Symbol | String) code, ?String? message, **Object metadata) -> ::Hash[Symbol, ::Hash[Symbol, Object]]
1148
+
1149
+ # The output schema declared for failure +code+ with a {.fails} block, or
1150
+ # +nil+ when the code has none (decision D23).
1151
+ #
1152
+ # _@param_ `code`
1153
+ def self.failure_output_schema: ((Symbol | String) code) -> singleton(FieldStruct::Base)?
1154
+
1155
+ # The failure codes this action declares with {.fails}, each mapped to its
1156
+ # metadata (decision D20). Empty when the action declares none.
1157
+ def self.declared_failures: () -> ::Hash[Symbol, ::Hash[Symbol, Object]]
1158
+
1159
+ # Whether this action may end with a failure of +code+ (decision D20):
1160
+ # always when it declares no failures, otherwise when it declares +code+
1161
+ # or +code+ is one of the {GEM_FAILURE_CODES}. The check behind the
1162
+ # strictness guard, the RSpec stubs and +and_fail+.
1163
+ #
1164
+ # _@param_ `code`
1165
+ def self.allows_failure_code?: ((Symbol | String) code) -> bool
1166
+
1167
+ # Every {.fails} declaration, keyed by code, as +{message:, metadata:}+.
1168
+ def self.failure_declarations: () -> ::Hash[Symbol, ::Hash[Symbol, Object]]
1169
+
1170
+ # Declare one skip code this action can end with, and the metadata a skip
1171
+ # with that code carries (decision D21). The skip counterpart of {.fails}.
1172
+ #
1173
+ # The metadata is neutral: arbitrary keywords the gem stores, attaches to
1174
+ # the {Skipped} as +result.metadata+, and shows in {.describe}, but never
1175
+ # interprets. +message+ is the default message for a +skip+ that gives
1176
+ # none.
1177
+ #
1178
+ # skips :not_ready, 'the field is not created yet', retry: true
1179
+ # skips :already_synced
1180
+ #
1181
+ # Declarations are inherited and a subclass can add to them; redeclaring a
1182
+ # code replaces its message and metadata. Once an action declares any
1183
+ # skip, skipping with an undeclared code raises a {DefinitionError} at the
1184
+ # +skip+ call, and that includes +skip+'s implicit default +:skipped+.
1185
+ #
1186
+ # A block declares the output a skip with this code carries, in the
1187
+ # FieldStruct DSL of {.output} (decision D23); see {.skip_output_schema}.
1188
+ #
1189
+ # skips :already_active do
1190
+ # required :id, :integer
1191
+ # end
1192
+ #
1193
+ # _@param_ `code` — the skip code
1194
+ #
1195
+ # _@param_ `message` — the default message for this code
1196
+ #
1197
+ # _@param_ `metadata` — per-code metadata, passed through
1198
+ #
1199
+ # _@return_ — {.declared_skips}
1200
+ def self.skips: ((Symbol | String) code, ?String? message, **Object metadata) -> ::Hash[Symbol, ::Hash[Symbol, Object]]
1201
+
1202
+ # The output schema declared for skip +code+ with a {.skips} block, or
1203
+ # +nil+ when the code has none (decision D23).
1204
+ #
1205
+ # _@param_ `code`
1206
+ def self.skip_output_schema: ((Symbol | String) code) -> singleton(FieldStruct::Base)?
1207
+
1208
+ # Turn typed outcomes on (or off) for this action and its descendants
1209
+ # (decision D23). With it on, a skip or failure whose code declares no
1210
+ # output schema carries an empty output instead of a best-effort capture,
1211
+ # and {.run} returns the action's own result classes: +Success+,
1212
+ # +Failure+ and +Skipped+ defined inside it, each a subclass of the gem
1213
+ # class (or of the parent action's own, for a subclass).
1214
+ #
1215
+ # _@param_ `enabled`
1216
+ #
1217
+ # _@return_ — the new setting
1218
+ def self.typed_outcomes: (?enabled: bool) -> bool
1219
+
1220
+ # _@return_ — whether typed outcomes are on (decision D23)
1221
+ def self.typed_outcomes?: () -> bool
1222
+
1223
+ # The skip codes this action declares with {.skips}, each mapped to its
1224
+ # metadata (decision D21). Empty when the action declares none.
1225
+ def self.declared_skips: () -> ::Hash[Symbol, ::Hash[Symbol, Object]]
1226
+
1227
+ # Whether this action may end with a skip of +code+ (decision D21): always
1228
+ # when it declares no skips, otherwise only when it declares +code+. The
1229
+ # gem emits no skip codes of its own, so nothing is exempt, +skip+'s
1230
+ # default +:skipped+ included. The check behind the strictness guard, the
1231
+ # RSpec skip stubs and +and_skip+.
1232
+ #
1233
+ # _@param_ `code`
1234
+ def self.allows_skip_code?: ((Symbol | String) code) -> bool
1235
+
1236
+ # Every {.skips} declaration, keyed by code, as +{message:, metadata:}+
1237
+ # (decision D21).
1238
+ def self.skip_declarations: () -> ::Hash[Symbol, ::Hash[Symbol, Object]]
1239
+
1240
+ # Declare a step. The step type is inferred from +target+ (a Symbol or
1241
+ # String names an instance method — a {Steps::Method}).
1242
+ #
1243
+ # Redeclaring a step of the same type and name (here or in a subclass)
1244
+ # replaces the earlier declaration's options in place: the last
1245
+ # declaration wins and keeps the original position. A subclass's
1246
+ # override never changes the parent's steps.
1247
+ #
1248
+ # _@param_ `target` — the step target
1249
+ #
1250
+ # _@param_ `options` — step options (e.g. +:if+, +:unless+)
1251
+ #
1252
+ # _@return_ — the updated step set
1253
+ def self.step: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
1254
+
1255
+ # Lifecycle hooks that run after the main steps when the run succeeds.
1256
+ def self.success_hooks: () -> ::Set[Steps::Base]
1257
+
1258
+ # Lifecycle hooks that run after the main steps when the run fails.
1259
+ def self.failure_hooks: () -> ::Set[Steps::Base]
1260
+
1261
+ # Lifecycle hooks that run after the main steps when the run was skipped.
1262
+ def self.skip_hooks: () -> ::Set[Steps::Base]
1263
+
1264
+ # Lifecycle hooks that run after the main steps regardless of outcome.
1265
+ def self.always_hooks: () -> ::Set[Steps::Base]
1266
+
1267
+ # Declare a hook that runs at the end of a run iff it succeeded (D2).
1268
+ #
1269
+ # _@param_ `target` — the instance method to run
1270
+ #
1271
+ # _@param_ `options` — step options (e.g. +:if+)
1272
+ def self.on_success: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
1273
+
1274
+ # Declare a hook that runs at the end of a run iff it failed (D2).
1275
+ #
1276
+ # _@param_ `target` — the instance method to run
1277
+ #
1278
+ # _@param_ `options` — step options (e.g. +:if+)
1279
+ def self.on_failure: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
1280
+
1281
+ # Declare a hook that runs at the end of a run iff it was skipped (D17).
1282
+ #
1283
+ # _@param_ `target` — the instance method to run
1284
+ #
1285
+ # _@param_ `options` — step options (e.g. +:if+)
1286
+ def self.on_skip: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
1287
+
1288
+ # Declare a hook that runs at the end of a run regardless of outcome (D2).
1289
+ #
1290
+ # _@param_ `target` — the instance method to run
1291
+ #
1292
+ # _@param_ `options` — step options (e.g. +:if+)
1293
+ def self.always: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
1294
+
1295
+ # Declare a branching step (decision D3). The switch value is read from
1296
+ # +value_source+ (an instance method); the block declares branches with
1297
+ # +on(value, target)+ and an optional +default(target)+.
1298
+ #
1299
+ # case_step :status do
1300
+ # on 'active', :handle_active
1301
+ # default :handle_unknown
1302
+ # end
1303
+ #
1304
+ # _@param_ `value_source` — the method whose value is switched on
1305
+ #
1306
+ # _@param_ `options` — step options (e.g. +:if+, +:unless+)
1307
+ #
1308
+ # _@return_ — the updated step set
1309
+ def self.case_step: ((Symbol | String) value_source, **Object options) -> ::Set[Steps::Base]
1310
+
1311
+ # Enable or read the action's measurement mode (decisions D10/D19). +:all+
1312
+ # records an execution {History}; +:none+ (the default) records nothing for
1313
+ # zero overhead; +:sampled+ records on a fraction of runs (+rate:+ between 0
1314
+ # and 1) for low-overhead production observability. A measuring run cascades
1315
+ # into the nested actions it invokes regardless of their own setting.
1316
+ #
1317
+ # measure :sampled, rate: 0.1 # record ~10% of runs
1318
+ #
1319
+ # _@param_ `mode` — the mode to set; omit to read
1320
+ #
1321
+ # _@param_ `rate` — required for +:sampled+ — the probability (0..1) that a given run is measured
1322
+ #
1323
+ # _@return_ — the resolved mode
1324
+ def self.measure: (?Symbol? mode, ?rate: Numeric?) -> Symbol
1325
+
1326
+ # _@return_ — whether this action always records history
1327
+ def self.measure_all?: () -> bool
1328
+
1329
+ # _@return_ — whether this action samples history
1330
+ def self.measure_sampled?: () -> bool
1331
+
1332
+ # The per-run sampling decision: true with probability {.measure_rate} when
1333
+ # in +:sampled+ mode, false otherwise. Rolled once per run by the {Runner}.
1334
+ def self.measure_sample_hit?: () -> bool
1335
+
1336
+ # A structured, at-a-glance summary of the action — its name, input/output
1337
+ # field metadata, ordered steps (with type), declared failures and skips
1338
+ # (each code's message and metadata, decisions D20/D21), lifecycle hooks, measurement mode,
1339
+ # and transaction config (decision D18). Lets humans and agents
1340
+ # understand an action without reading its source.
1341
+ def self.describe: () -> ::Hash[Symbol, Object]
1342
+
1343
+ # A human/agent-readable rendering of {.describe} — the same information as
1344
+ # a multi-line summary instead of a Hash (decision D18). The input/output
1345
+ # field lines reuse FieldStruct's own +Metadata#describe+ (type,
1346
+ # required-ness, and the options each field's type accepts); declared
1347
+ # failures, declared skips and hook sections are listed only when there are any.
1348
+ #
1349
+ # Greet.describe_text
1350
+ # # => "Greet (measure: none)\n Input: (free-form)\n ..."
1351
+ def self.describe_text: () -> String
1352
+
1353
+ # Run the action. With a declared input schema, coerce the arguments
1354
+ # (keyword args, or a single input-schema instance) into the input struct
1355
+ # and validate it — a required input that is missing or won't coerce
1356
+ # short-circuits to a +Failure(:invalid_input)+ without running any steps
1357
+ # (decision D7). Without an input schema, the arguments are forwarded
1358
+ # verbatim to the action's constructor (the free-form path).
1359
+ #
1360
+ # Under typed outcomes the result is an instance of the action's own
1361
+ # result class for its outcome (decision D23).
1362
+ #
1363
+ # _@param_ `args` — positional arguments: a single input-schema instance, or (free-form) whatever the constructor takes
1364
+ #
1365
+ # _@param_ `kwargs` — keyword arguments: the input fields, or (free-form) whatever the constructor takes
1366
+ #
1367
+ # _@return_ — the run's {Success} or {Failure}
1368
+ def self.run: (*Object args, **Object kwargs) -> Result
1369
+
1370
+ # Run the action once per item in +enumerable+, collecting the per-item
1371
+ # results into a {BatchResult} (decision D19). The single most common
1372
+ # batch/ingest shape: each item runs independently (its own {.run}, its own
1373
+ # outcome), so one item's failure never affects another, and the aggregate
1374
+ # is inspectable via {BatchResult#all_ok?} / +#any_failure?+ / +#partial?+.
1375
+ #
1376
+ # The optional block maps each item to +.run+ arguments: return a Hash for
1377
+ # keyword args, an Array for positional args, or any other value to pass as
1378
+ # a single argument. Without a block, each item is passed to +.run+
1379
+ # directly.
1380
+ #
1381
+ # ImportRow.run_each(rows) { |row| {row: row} }
1382
+ # Charge.run_each(amounts) # => each amount passed to .run
1383
+ #
1384
+ # _@param_ `enumerable` — the items to run
1385
+ #
1386
+ # _@return_ — the per-item results
1387
+ def self.run_each: (::Enumerable[untyped] enumerable) ?{ (Object item) -> (::Hash[untyped, untyped] | ::Array[untyped] | Object) } -> BatchResult
1388
+
1389
+ # The body of {.run}: build the input, then run the steps.
1390
+ #
1391
+ # _@param_ `args`
1392
+ #
1393
+ # _@param_ `kwargs`
1394
+ def self.run_untyped: (::Array[Object] args, ::Hash[Symbol, Object] kwargs) -> Result
1395
+
1396
+ # Define this action's own +Success+, +Failure+ and +Skipped+ (decision
1397
+ # D23), each subclassing the parent action's own when it has them, else
1398
+ # the gem class. Defined once; a later call is a no-op.
1399
+ #
1400
+ # _@return_ — status => result class
1401
+ def self.define_outcome_classes: () -> ::Hash[Symbol, singleton(Result)]
1402
+
1403
+ # _@param_ `const` — the constant to define
1404
+ #
1405
+ # _@param_ `status` — the outcome {Result#status}
1406
+ #
1407
+ # _@param_ `base` — the gem result class
1408
+ def self.define_outcome_class: (Symbol const, Symbol status, singleton(Result) base) -> singleton(Result)
1409
+
1410
+ # The result as this action's own result class for its outcome, under
1411
+ # typed outcomes (decision D23). A result already of that class, or of a
1412
+ # host's own result subclass, is returned as it is; a gem result, or
1413
+ # another action's own (a nested failure that crossed), is rebuilt with
1414
+ # its attributes and errors.
1415
+ #
1416
+ # _@param_ `result`
1417
+ def self.typed_result: (Result result) -> Result
1418
+
1419
+ # Build an output schema from a FieldStruct DSL block, refusing a field
1420
+ # named like a result attribute (decisions D6/D18/D23).
1421
+ #
1422
+ # _@param_ `block` — the FieldStruct field declarations
1423
+ def self.build_output_schema: (Proc block) -> singleton(FieldStruct::Base)
1424
+
1425
+ # One {.fails} / {.skips} declaration: the default message, the frozen
1426
+ # metadata, and the output schema when a block declares one (D20/D21/D23).
1427
+ #
1428
+ # _@param_ `message`
1429
+ #
1430
+ # _@param_ `metadata`
1431
+ #
1432
+ # _@param_ `block`
1433
+ def self.declaration: (String? message, ::Hash[Symbol, Object] metadata, Proc? block) -> ::Hash[Symbol, Object]
1434
+
1435
+ # {.describe}'s view of the declarations: each code's message and
1436
+ # metadata, plus its output fields when it declares an output schema.
1437
+ #
1438
+ # _@param_ `declarations`
1439
+ def self.describe_declarations: (::Hash[Symbol, ::Hash[Symbol, Object]] declarations) -> ::Hash[Symbol, ::Hash[Symbol, Object]]
1440
+
1441
+ # One {.describe_text} block for an input/output schema: +" (free-form)"+
1442
+ # when none is declared, otherwise the FieldStruct +Metadata#describe+
1443
+ # field lines indented one level under their +Input:+/+Output:+ heading.
1444
+ #
1445
+ # _@param_ `schema`
1446
+ def self.schema_summary: (singleton(FieldStruct::Base)? schema) -> String
1447
+
1448
+ # The +Failures:+ or +Skips:+ lines for {.describe_text}, one per declared
1449
+ # code with its metadata and default message, or none when nothing is
1450
+ # declared (decisions D20/D21).
1451
+ #
1452
+ # _@param_ `heading` — the section heading
1453
+ #
1454
+ # _@param_ `declarations` — code => {message:, metadata:}
1455
+ def self.declarations_summary: (String heading, ::Hash[Symbol, ::Hash[Symbol, Object]] declarations) -> ::Array[String]
1456
+
1457
+ # The +input+ value for {.describe}: a fixed schema's field metadata, a
1458
+ # discriminated-input descriptor (+discriminated_on+ + +shapes+), or +nil+
1459
+ # for a free-form action.
1460
+ def self.input_descriptor: () -> ::Hash[untyped, untyped]?
1461
+
1462
+ # The +Input:+ block for {.describe_text}: the fixed-schema field lines, a
1463
+ # one-line discriminated-input summary, or +" (free-form)"+.
1464
+ def self.input_summary: () -> String
1465
+
1466
+ # Run-start guard (decision D18): every method a step needs (method steps,
1467
+ # case value sources and method branch targets, hook steps) must exist on
1468
+ # the instance, or raise a clear {DefinitionError} rather than a deep
1469
+ # +NoMethodError+. This is the earliest reliable point — steps are usually
1470
+ # declared above the methods that implement them.
1471
+ #
1472
+ # _@param_ `instance`
1473
+ def self.ensure_steps_implemented!: (Action _instance) -> void
1474
+
1475
+ # First-run guard (owner ruling R5): in every step and hook method, a
1476
+ # non-bang recording verb (+fail+ / +fail_with+ / +skip+ / +succeed+) must be the
1477
+ # method's last expression, or explicitly end it, since anything after
1478
+ # it still runs (+VerbPosition+ has the rules). Checked once per class,
1479
+ # by parsing each method's source with Prism; skipped where that can't
1480
+ # be done (no Prism, no readable source), and not run at all while
1481
+ # {Actionable.verb_position_check?} is off. A failing (or skipped) check
1482
+ # is not cached, so it raises on every run until fixed.
1483
+ def self.ensure_verbs_last!: () -> void
1484
+
1485
+ # The steps and hooks whose methods {.ensure_verbs_last!} checks. The
1486
+ # Rails adapter adds its +after_commit+ hooks.
1487
+ def self.verb_checked_steps: () -> ::Array[Steps::Base]
1488
+
1489
+ # Whether +instance+ implements +method+ itself (public or private), not
1490
+ # merely inherits it from Ruby's core: steps are called with +__send__+,
1491
+ # so +step :puts+ or +always :exit+ must not reach +Kernel+.
1492
+ #
1493
+ # _@param_ `instance`
1494
+ #
1495
+ # _@param_ `method`
1496
+ def self.implements?: (Action _instance, Symbol method) -> bool
1497
+
1498
+ # Coerce +.run+ arguments into the input struct: pass an existing input
1499
+ # instance through untouched, otherwise build one from the keyword args.
1500
+ # With discriminated input ({.input_for}), the schema is chosen from the
1501
+ # discriminator value first; an unmatched value returns a +Failure+.
1502
+ #
1503
+ # _@param_ `args` — the positional arguments given to {.run}
1504
+ #
1505
+ # _@param_ `kwargs` — the keyword arguments given to {.run}
1506
+ def self.build_input: (::Array[Object] args, ::Hash[Symbol, Object] kwargs) -> (FieldStruct::Base | Failure)
1507
+
1508
+ # Resolve the discriminated schema from the discriminator value, then build
1509
+ # the input the same way {.build_input} does — or return an
1510
+ # +:invalid_input+ {Failure} when nothing matches and there's no default.
1511
+ #
1512
+ # _@param_ `args` — the positional arguments given to {.run}
1513
+ #
1514
+ # _@param_ `kwargs` — the keyword arguments given to {.run}
1515
+ def self.build_dispatched_input: (::Array[Object] args, ::Hash[Symbol, Object] kwargs) -> (FieldStruct::Base | Failure)
1516
+
1517
+ # _@param_ `value` — the unmatched discriminator value
1518
+ #
1519
+ # _@return_ — code +:invalid_input+, naming the discriminator
1520
+ def self.unmatched_discriminator_failure: (Object value) -> Failure
1521
+
1522
+ # _@param_ `input` — the invalid input struct
1523
+ #
1524
+ # _@return_ — code +:invalid_input+, carrying the input's errors
1525
+ def self.invalid_input_failure: (FieldStruct::Base input) -> Failure
1526
+
1527
+ # Add +step+ to +list+, or — when an equal step (same type and name) is
1528
+ # already declared — swap it in at that step's position, so the last
1529
+ # declaration wins (owner ruling P8; invariant 5). +list+ is this class's
1530
+ # own copy (see {.inherited}), so a parent's list is never touched.
1531
+ #
1532
+ # _@param_ `list`
1533
+ #
1534
+ # _@param_ `step`
1535
+ #
1536
+ # _@return_ — +list+
1537
+ def self.declare: (::Set[Steps::Base] list, Steps::Base step) -> ::Set[Steps::Base]
1538
+
1539
+ # Give each subclass its own copy of the inherited step set, so adding
1540
+ # steps to a subclass never mutates the parent's (decision D2).
1541
+ #
1542
+ # _@param_ `subclass`
1543
+ def self.inherited: (Class subclass) -> void
1544
+
1545
+ # Default free-form constructor: accept and ignore any arguments, so an
1546
+ # action with no declared input still runs via {.run}. Subclasses override
1547
+ # this to capture their inputs (decision D7; typed input lands in a later
1548
+ # slice).
1549
+ #
1550
+ # _@param_ `_args` — ignored
1551
+ #
1552
+ # _@param_ `_kwargs` — ignored
1553
+ def initialize: (*Object _args, **Object _kwargs) -> void
1554
+
1555
+ # Record a {Failure} as the run's result without halting — later steps
1556
+ # still run, and the final result reflects the most recent record (decision
1557
+ # D4, "last write wins").
1558
+ #
1559
+ # A code declared with {Action.fails} attaches its metadata to the failure,
1560
+ # and its declared message when +message+ is +nil+ (decision D20). Nothing
1561
+ # but +errors+ follows the message, so an error on any field name, +kind+
1562
+ # included, lands in +errors+.
1563
+ #
1564
+ # Under typed outcomes alone, +output:+ is reserved: it is the failure's
1565
+ # output, held to the schema its code declares (decision D23).
1566
+ #
1567
+ # fail!(:invalid, 'check the form', output: {form_record: record}, name: 'is blank')
1568
+ #
1569
+ # _@param_ `code` — the error code
1570
+ #
1571
+ # _@param_ `message` — human-readable description
1572
+ #
1573
+ # _@param_ `errors` — each keyword is a field => message(s) added to the result's errors collection; under typed outcomes the +output:+ keyword is the failure's output instead
1574
+ #
1575
+ # _@return_ — so a step can branch on the call
1576
+ def fail: (Symbol code, ?String? message, **(String | ::Array[String] | ::Hash[Symbol, Object] | FieldStruct::Base) errors) -> bool
1577
+
1578
+ # Record a {Failure} that absorbs an arbitrary FieldStruct's validation
1579
+ # errors, without halting (decision D19). The same machinery
1580
+ # +Failure(:invalid_input)+ / +Failure(:invalid_output)+ use internally,
1581
+ # exposed as a verb so a step can validate a side struct and fail with its
1582
+ # errors directly.
1583
+ #
1584
+ # def validate = fail_with(EventShape.new(payload), code: :invalid_event)
1585
+ #
1586
+ # _@param_ `source` — any object exposing a FieldStruct-style +errors+ collection (typically a +FieldStruct::Base+ instance)
1587
+ #
1588
+ # _@param_ `code` — the error code; defaults to +:invalid+
1589
+ #
1590
+ # _@param_ `message` — human-readable description
1591
+ #
1592
+ # _@param_ `output` — the failure's output, held to the schema its code declares; typed outcomes only (decision D23)
1593
+ #
1594
+ # _@return_ — so a step can branch on the call
1595
+ def fail_with: (
1596
+ untyped source,
1597
+ ?code: Symbol,
1598
+ ?message: String?,
1599
+ ?output: ::Hash[Symbol, Object]?
1600
+ ) -> bool
1601
+
1602
+ # Record a {Failure} absorbing +source+'s errors and halt the run
1603
+ # (decision D19). The halting companion to {#fail_with}.
1604
+ #
1605
+ # _@param_ `source` — see {#fail_with}
1606
+ #
1607
+ # _@param_ `code` — the error code; defaults to +:invalid+
1608
+ #
1609
+ # _@param_ `message` — human-readable description
1610
+ #
1611
+ # _@param_ `output` — see {#fail_with}
1612
+ def fail_with!: (
1613
+ untyped source,
1614
+ ?code: Symbol,
1615
+ ?message: String?,
1616
+ ?output: ::Hash[Symbol, Object]?
1617
+ ) -> void
1618
+
1619
+ # Record a {Success} as the run's result without halting (decision D4).
1620
+ #
1621
+ # _@param_ `message` — human-readable description
1622
+ #
1623
+ # _@param_ `output` — the success output payload
1624
+ #
1625
+ # _@return_ — so a step can branch on the call
1626
+ def succeed: (?String? message, **Object output) -> bool
1627
+
1628
+ # Record a {Failure} and halt the run, skipping the remaining steps
1629
+ # (decision D4).
1630
+ #
1631
+ # _@param_ `code` — the error code
1632
+ #
1633
+ # _@param_ `message` — human-readable description
1634
+ #
1635
+ # _@param_ `errors` — see {#fail}
1636
+ def fail!: (Symbol code, ?String? message, **(String | ::Array[String] | ::Hash[Symbol, Object] | FieldStruct::Base) errors) -> void
1637
+
1638
+ # Record a {Success} and halt the run, skipping the remaining steps
1639
+ # (decision D4).
1640
+ #
1641
+ # _@param_ `message` — human-readable description
1642
+ #
1643
+ # _@param_ `output` — the success output payload
1644
+ def succeed!: (?String? message, **Object output) -> void
1645
+
1646
+ # Record a {Skipped} as the run's result without halting — the action had
1647
+ # nothing to do (decision D17). Not a failure; later steps still run and the
1648
+ # final result reflects the most recent record (last write wins).
1649
+ #
1650
+ # A code declared with {Action.skips} attaches its metadata to the skip,
1651
+ # and its declared message when +message+ is +nil+ (decision D21). Once the
1652
+ # action declares any skip, an undeclared +code+ raises.
1653
+ #
1654
+ # _@param_ `code` — the skip reason; defaults to +:skipped+
1655
+ #
1656
+ # _@param_ `message` — human-readable description
1657
+ #
1658
+ # _@param_ `output` — the skip output payload — captured best-effort like a {Success}'s output, but never validated (decision D17). Lets an idempotent hit return the existing record's data.
1659
+ #
1660
+ # _@return_ — so a step can branch on the call (the run did not succeed here)
1661
+ def skip: (?Symbol code, ?String? message, **Object output) -> bool
1662
+
1663
+ # Record a {Skipped} and halt the run, skipping the remaining steps — the
1664
+ # common "nothing to do, stop here" case (decision D17).
1665
+ #
1666
+ # _@param_ `code` — the skip reason; defaults to +:skipped+
1667
+ #
1668
+ # _@param_ `message` — human-readable description
1669
+ #
1670
+ # _@param_ `output` — the skip output payload (see {#skip})
1671
+ def skip!: (?Symbol code, ?String? message, **Object output) -> void
1672
+
1673
+ # Build a {Failure} for +code+ carrying what {Action.fails} declared for it:
1674
+ # its metadata, and its message when +message+ is +nil+ (decision D20).
1675
+ #
1676
+ # _@param_ `code` — the error code
1677
+ #
1678
+ # _@param_ `message` — the message given at the call
1679
+ def declared_failure: ((Symbol | String) code, String? message) -> Failure
1680
+
1681
+ # Fail this run with a nested action's failure and halt (decision D20).
1682
+ # {Steps::Action} calls it instead of +fail!+, so the child's failure
1683
+ # crosses intact: its code, message, every error (on any field name, since
1684
+ # no keyword list is involved), its metadata, and its result class with any
1685
+ # extra attributes a host subclass adds. Under the child's metadata sits
1686
+ # what this action declares for the code, so the parent's declaration fills
1687
+ # in keys the child didn't set. The child's output and history stay on the
1688
+ # child. A declaring parent must declare the child's code too.
1689
+ #
1690
+ # Under typed outcomes the child's output crosses as this action's failure
1691
+ # output, so it is held to the schema this action declares for the code,
1692
+ # not the child's (decision D23), and the failure becomes this action's own
1693
+ # +Failure+ when {.run} returns.
1694
+ #
1695
+ # _@param_ `child` — the nested action's failure
1696
+ def adopt_failure!: (Failure child) -> void
1697
+
1698
+ # The class a nested failure crosses as: a host's own result subclass
1699
+ # stays (D20), but a typed child's own +Failure+ becomes the gem class,
1700
+ # since it names the child; a typed parent then makes it its own (D23).
1701
+ #
1702
+ # _@param_ `child`
1703
+ def adopted_failure_class: (Result child) -> singleton(Failure)
1704
+
1705
+ # Whether a nested failure's output crosses into this action: under typed
1706
+ # outcomes, for every code but those the gem builds, which keep the output
1707
+ # they had before typed outcomes (none) (decision D23).
1708
+ #
1709
+ # _@param_ `child`
1710
+ def crosses_output?: (Failure child) -> bool
1711
+
1712
+ # _@param_ `output` — a nested failure's output
1713
+ #
1714
+ # _@return_ — its fields, for this action's schema
1715
+ def crossing_output: ((FieldStruct::Base | ::Hash[untyped, untyped] | Object) output) -> ::Hash[Symbol, Object]
1716
+
1717
+ # A +fail+ / +skip+ code must be an identifier-like Symbol or String
1718
+ # ({CODE_FORMAT}): +fail 'card expired'+ is +Kernel#fail+ habit, and in an
1719
+ # action it would record the sentence as a code and keep running (owner
1720
+ # ruling R5).
1721
+ #
1722
+ # _@param_ `kind`
1723
+ #
1724
+ # _@param_ `code`
1725
+ def ensure_code_identifier!: (Symbol kind, Object code) -> void
1726
+
1727
+ # Strictness (decision D20): once an action declares any failure, every
1728
+ # code it fails with must be declared, so its declarations are its whole
1729
+ # failure contract. An action that declares none is unchecked, and the
1730
+ # {GEM_FAILURE_CODES} are always allowed.
1731
+ #
1732
+ # _@param_ `code`
1733
+ def ensure_failure_declared!: (Symbol code) -> void
1734
+
1735
+ # Under typed outcomes, output given for a skip or failure must have a
1736
+ # schema to go into (decision D23): output for a code with none would be
1737
+ # dropped, so it is refused instead. The failures the gem builds keep
1738
+ # their output, so they need none. +output:+ itself must be a Hash or a
1739
+ # struct, which catches an old +output:+ error message.
1740
+ #
1741
+ # _@param_ `verb` — the declaration that would add the schema
1742
+ #
1743
+ # _@param_ `code`
1744
+ #
1745
+ # _@param_ `output` — the output given, if any
1746
+ def ensure_output_declared!: (Symbol verb, Symbol code, (::Hash[untyped, untyped] | FieldStruct::Base)? output) -> void
1747
+
1748
+ # Strictness (decision D21): once an action declares any skip, every code
1749
+ # it skips with must be declared, the implicit default +:skipped+ included.
1750
+ # An action that declares none is unchecked.
1751
+ #
1752
+ # _@param_ `code`
1753
+ def ensure_skip_declared!: (Symbol code) -> void
1754
+
1755
+ # Halt the run immediately, keeping whatever result was already recorded
1756
+ # (a prior +fail+/+succeed+/+skip+, or auto-{Success} if none). Control flow
1757
+ # only — not an exception (decision D4).
1758
+ def halt!: () -> void
1759
+
1760
+ # Wrap this action's run in +model.transaction(**options)+.
1761
+ #
1762
+ # transactional model: :invoice
1763
+ # transactional model: Invoice, requires_new: false # join an outer one
1764
+ #
1765
+ # Run inside a transaction that is already open, the action gets its own
1766
+ # savepoint (+requires_new: true+) unless +requires_new:+ is given, so a
1767
+ # recorded +Failure+ rolls back its own writes rather than leaving them for
1768
+ # the outer transaction to commit.
1769
+ #
1770
+ # _@param_ `model` — the model whose +transaction+ is used; a Symbol is resolved to a constant (+:invoice+ → +Invoice+)
1771
+ #
1772
+ # _@param_ `options` — options forwarded to +transaction+
1773
+ def self.transactional: (model: (Symbol | Class), **Object options) -> void
1774
+
1775
+ # The transaction config for this action, inherited from ancestors when
1776
+ # not set locally (decision D9: inherited, overridable).
1777
+ #
1778
+ # _@return_ — +{model:, options:}+, or +nil+ when
1779
+ # neither this action nor an ancestor is +transactional+
1780
+ def self.transaction_config: () -> ::Hash[Symbol, Object]?
1781
+
1782
+ # Opt this action in (or, with +enabled: false+, back out) to {Job}
1783
+ # honoring the +retry:+ metadata of its declared failures and skips:
1784
+ # +retry: true+ retries the job, +retry: false+ does not. A code with no
1785
+ # +retry:+ key keeps the default mapping. Inherited; a subclass can turn
1786
+ # it off.
1787
+ #
1788
+ # class Reindex < Actionable::Action
1789
+ # honor_retry_metadata
1790
+ # skips :not_ready, retry: true # the job retries until the index is ready
1791
+ # end
1792
+ #
1793
+ # _@param_ `enabled`
1794
+ def self.honor_retry_metadata: (?enabled: bool) -> void
1795
+
1796
+ # Whether {Job} honors this action's +retry:+ metadata, inherited from
1797
+ # ancestors when not set locally. +false+ unless an ancestor opted in.
1798
+ def self.honor_retry_metadata?: () -> bool
1799
+
1800
+ # _@return_ — the declared output schema, or
1801
+ # +nil+ for a free-form action
1802
+ def self.output_schema: () -> singleton(FieldStruct::Base)?
1803
+
1804
+ # _@return_ — the declared input schema, or
1805
+ # +nil+ for a free-form action
1806
+ def self.input_schema: () -> singleton(FieldStruct::Base)?
1807
+
1808
+ # _@return_ — the discriminated-input dispatch, or +nil+
1809
+ # when input isn't discriminated
1810
+ def self.input_dispatch: () -> InputDispatch?
1811
+
1812
+ # _@return_ — the sampling rate for +:sampled+ mode, else +nil+
1813
+ def self.measure_rate: () -> Float?
1814
+
1815
+ # The result recorded during the run, if any. The control-flow methods
1816
+ # (+fail+/+succeed+/+fail!+/+succeed!+) write here; +nil+ means "no explicit
1817
+ # result", which the {Runner} turns into an auto-{Success}.
1818
+ attr_reader result: Result?
1819
+
1820
+ # The typed input struct for this run, when an +input+ schema is declared
1821
+ # (decision D7); +nil+ for a free-form action. Steps read +input.field+.
1822
+ attr_reader input: FieldStruct::Base?
1823
+ end
1824
+
1825
+ # Base class for every error this gem raises. Rescue +Actionable::Error+ to
1826
+ # catch the whole family. Genuine errors are never caught by the runner for
1827
+ # control flow — they propagate to the caller (decision D4).
1828
+ class Error < StandardError
1829
+ end
1830
+
1831
+ # Raised when an action is misconfigured — a reserved output field name, or a
1832
+ # declared step whose method isn't implemented. Surfaced early and clearly so
1833
+ # humans and agents get an actionable message instead of a deep +NoMethodError+
1834
+ # (decision D18).
1835
+ class DefinitionError < Actionable::Error
1836
+ end
1837
+
1838
+ # The single value object a run returns. {Success} and {Failure} are the
1839
+ # concrete subclasses; +Result+ itself is abstract and defines the shared,
1840
+ # FieldStruct-backed shape (decision D5).
1841
+ #
1842
+ # @abstract Instantiate {Success} or {Failure}, not +Result+ directly.
1843
+ class Result < FieldStruct::Base
1844
+ # _@return_ — +:success+ for a {Success}; the error symbol for a {Failure}
1845
+ def code: () -> Symbol
1846
+
1847
+ # _@return_ — human-readable description of the outcome
1848
+ def message: () -> String?
1849
+
1850
+ # _@return_ — the run's declared, typed output payload (empty by default).
1851
+ # Firms up into an output FieldStruct once output schemas land (D6).
1852
+ def output: () -> Object
1853
+
1854
+ # _@return_ — the run's execution history (empty by default).
1855
+ # Firms up into a +History+ once measurement lands (D10).
1856
+ def history: () -> Object
1857
+
1858
+ # _@return_ — whether the run succeeded. Abstract here; each
1859
+ # subclass flips the predicate that applies to it.
1860
+ def success?: () -> bool
1861
+
1862
+ # _@return_ — whether the run failed.
1863
+ def failure?: () -> bool
1864
+
1865
+ # _@return_ — whether the run was skipped — nothing to do, neither a
1866
+ # success nor a failure (decision D17). Abstract here; {Skipped} flips it.
1867
+ def skipped?: () -> bool
1868
+
1869
+ # _@return_ — alias of {#success?}
1870
+ def successful?: () -> bool
1871
+
1872
+ # _@return_ — alias of {#failure?}
1873
+ def failed?: () -> bool
1874
+
1875
+ # _@return_ — whether the run did not fail — true for both a
1876
+ # {Success} and a {Skipped}. The "happy path didn't error" check.
1877
+ def ok?: () -> bool
1878
+
1879
+ # _@return_ — a compact, field-sorted, deterministic representation —
1880
+ # the declared attributes plus the +errors+ collection, sorted by name,
1881
+ # so equal results always render identically.
1882
+ def inspect: () -> String
1883
+
1884
+ # _@return_ — same deterministic representation as {#inspect}
1885
+ def to_s: () -> String
1886
+
1887
+ # _@return_ — the outcome as a single symbol — +:success+, +:failure+,
1888
+ # or +:skipped+. The coarse-grained companion to the specific {#code}.
1889
+ def status: () -> Symbol
1890
+
1891
+ # A plain-Hash view of the result — +code+, +message+, the +output+ and
1892
+ # +history+ rendered to primitives, and the +errors+ collection as a Hash
1893
+ # (decision D19). The serializable companion to {#inspect}.
1894
+ def to_h: () -> ::Hash[Symbol, Object]
1895
+
1896
+ # A compact element for an HTTP/batch response: the item's position, its
1897
+ # {#status}, the id read off the typed output, and its errors (decision
1898
+ # D19). Lets a controller map a batch of results without hand-rolling the
1899
+ # shape.
1900
+ #
1901
+ # results.each_with_index.map { |r, i| r.to_api_h(index: i) }
1902
+ #
1903
+ # _@param_ `index` — the element's position in a batch
1904
+ #
1905
+ # _@param_ `id_field` — the output field to surface as +:id+ (default +:id+)
1906
+ def to_api_h: (?index: Integer?, ?id_field: Symbol) -> ::Hash[Symbol, Object]
1907
+
1908
+ # Convenience delegation: a declared output field is readable straight off
1909
+ # the result (+result.invoice+ → +result.output.invoice+). Only fields
1910
+ # declared on a typed output schema delegate; incidental data never does
1911
+ # (decision D6).
1912
+ #
1913
+ # _@param_ `name` — the method name asked about (Ruby passes a Symbol)
1914
+ #
1915
+ # _@param_ `include_private` — whether private methods count
1916
+ def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
1917
+
1918
+ # _@param_ `name` — the method called
1919
+ #
1920
+ # _@param_ `args` — its arguments, forwarded to the output
1921
+ #
1922
+ # _@return_ — the output field's value: left untyped on purpose,
1923
+ # like any field reader whose type the schema decides
1924
+ #
1925
+ # _@see_ `#respond_to_missing?`
1926
+ def method_missing: (Symbol name, *Object args) -> untyped
1927
+
1928
+ # _@param_ `name`
1929
+ #
1930
+ # _@return_ — whether +name+ is a declared field on a typed output
1931
+ def delegates_to_output?: (Symbol name) -> bool
1932
+
1933
+ # Read +name+ off the output — a declared field on a typed output, a key on
1934
+ # a Hash output, or +nil+ when absent.
1935
+ #
1936
+ # _@param_ `name`
1937
+ def output_field: (Symbol name) -> Object?
1938
+
1939
+ # The output fields the run actually set (captured instance variables and
1940
+ # +succeed+/+skip+ kwargs), as opposed to fields left nil or at their
1941
+ # default. A parent absorbing a nested action's output copies only these
1942
+ # (decision D3). +nil+ when unknown, e.g. a hand-built result.
1943
+ attr_accessor output_fields_set: ::Array[Symbol]?
1944
+ end
1945
+
1946
+ # Executes an action instance's steps and produces the {Result}.
1947
+ #
1948
+ # The main loop runs inside {Actionable.catch_halt}, so a halting step
1949
+ # (+throw Actionable::HALT+) unwinds out of the loop without running the
1950
+ # remaining steps. Genuine exceptions are not caught — they propagate to the
1951
+ # caller (decision D4). When no step recorded a result, the run auto-succeeds.
1952
+ #
1953
+ # After the main steps the runner picks the lifecycle hook category from the
1954
+ # outcome (+on_success+ vs +on_failure+) and runs it, then the +always+ hooks
1955
+ # (decision D2). Hooks are ordinary methods: they may record via the
1956
+ # control-flow verbs, and a halting verb (+fail!+ / +skip!+ / +succeed!+ /
1957
+ # +halt!+) has its result adopted, then stops the remaining hooks of its own
1958
+ # phase: a halt in an outcome hook skips the rest of that category but the
1959
+ # +always+ hooks still run (seeing the adopted result), and a halt in an
1960
+ # +always+ hook skips the remaining +always+ hooks. The output is refreshed
1961
+ # (re-captured and re-validated) after the main steps and after each hook
1962
+ # (decision D6).
1963
+ #
1964
+ # When the action (or an enclosing measuring run) enables measurement, the
1965
+ # runner records a {History} of every step — section, name, timing, result
1966
+ # code, and nested child history (decision D10).
1967
+ class Runner
1968
+ include Actionable::Rails::RunnerAfterCommit
1969
+ include Actionable::Rails::RunnerTransaction
1970
+
1971
+ # _@param_ `instance` — the action instance to run
1972
+ def initialize: (Action _instance) -> void
1973
+
1974
+ # Run the action's steps and lifecycle hooks, returning the finalized result.
1975
+ #
1976
+ # _@return_ — the recorded result (or a fresh {Success} if none was
1977
+ # set), with its typed output and (when measuring) history assigned
1978
+ def run: () -> Result
1979
+
1980
+ # Run the body, marking measurement active so nested runs cascade.
1981
+ #
1982
+ # _@return_ — the block's value
1983
+ def measured: () ?{ () -> Object } -> Object
1984
+
1985
+ # The main steps, then the outcome and always lifecycle hooks.
1986
+ def perform: () -> void
1987
+
1988
+ # Run each declared step in order, skipping any whose guard opts out.
1989
+ def run_steps: () -> void
1990
+
1991
+ # Run the outcome-appropriate hooks then the +always+ hooks, each phase in
1992
+ # its own halt catch: a hook's halt stops the remaining hooks of its phase
1993
+ # only, so +always+ hooks run even after an outcome hook halts. The
1994
+ # category is fixed by the post-main-steps outcome — it does not re-dispatch
1995
+ # if a hook later flips the result.
1996
+ def run_lifecycle: () -> void
1997
+
1998
+ # Run one hook, then adopt any result it recorded and refresh the output.
1999
+ # A hook that halts is caught here first so its result is adopted too, then
2000
+ # the halt is re-thrown to stop the remaining hooks of the phase.
2001
+ #
2002
+ # _@param_ `section`
2003
+ #
2004
+ # _@param_ `hook`
2005
+ def run_hook: (Symbol section, Steps::Base hook) -> void
2006
+
2007
+ # Run one main step, skipping it when its guard opts out.
2008
+ #
2009
+ # _@param_ `section`
2010
+ #
2011
+ # _@param_ `step`
2012
+ def call_step: (Symbol section, Steps::Base step) -> void
2013
+
2014
+ # Time and record a step into the history when measuring; otherwise just run
2015
+ # it. The result code is recorded only when the step changed the recorded
2016
+ # result, so steps that merely set ivars carry a nil code.
2017
+ #
2018
+ # _@param_ `section`
2019
+ #
2020
+ # _@param_ `step`
2021
+ def record: (Symbol section, Steps::Base step) -> void
2022
+
2023
+ # Capture, coerce, and attach the typed output (decision D6). Free-form
2024
+ # actions (no schema) keep whatever output was recorded. With a schema,
2025
+ # capture the declared-field ivars overlaid with any +succeed+ kwargs
2026
+ # (kwargs win) and coerce them. Only a strict {Success} is validated — its
2027
+ # output failing validation means it can't really have succeeded, so
2028
+ # +@result+ becomes a +Failure(:invalid_output)+.
2029
+ #
2030
+ # A {Failure} or {Skipped} whose code declares its own output schema
2031
+ # (decision D23) is built from its kwargs alone and validated the same way.
2032
+ # Without one it carries an empty output under typed outcomes, and
2033
+ # otherwise a best-effort, unvalidated capture into the success schema
2034
+ # (decisions D6/D17). The failures the gem builds keep their output.
2035
+ def refresh_output: () -> void
2036
+
2037
+ def refresh_success_output: () -> void
2038
+
2039
+ # Build the output struct and note on the result which fields the run
2040
+ # actually set, so a parent absorbing this run copies only those (D3).
2041
+ #
2042
+ # _@param_ `schema`
2043
+ #
2044
+ # _@param_ `attrs`
2045
+ def build_output: (singleton(FieldStruct::Base) schema, ::Hash[Symbol, Object] attrs) -> FieldStruct::Base
2046
+
2047
+ # The result the action instance recorded, refusing anything else stored
2048
+ # in the reserved +@result+ ivar with a clear error instead of a deep
2049
+ # +NoMethodError+.
2050
+ def instance_result: () -> Result?
2051
+
2052
+ # Attach +output+ when it is valid; otherwise the run ends as a
2053
+ # +Failure(:invalid_output)+ carrying it.
2054
+ #
2055
+ # _@param_ `output`
2056
+ def attach_validated: (FieldStruct::Base output) -> void
2057
+
2058
+ # Attach a best-effort output to a failure or skip without validating it
2059
+ # (decisions D6/D17). Assigning a struct makes the result validate it, which
2060
+ # would add an +output+ error, so only the errors the result already had
2061
+ # are kept.
2062
+ #
2063
+ # _@param_ `output`
2064
+ def attach_unvalidated: (FieldStruct::Base output) -> void
2065
+
2066
+ # _@return_ — the output schema the skip or
2067
+ # failure's code declares (decision D23), if any
2068
+ def outcome_schema: () -> singleton(FieldStruct::Base)?
2069
+
2070
+ # A skip or failure's output fields, from the kwargs it was recorded with,
2071
+ # never from instance variables (decision D23). After a hook refresh the
2072
+ # output is already the struct, so its own attributes are reused, limited
2073
+ # to the fields the kwargs set so the set-fields marker stays true.
2074
+ #
2075
+ # _@param_ `schema`
2076
+ def outcome_attributes: (singleton(FieldStruct::Base) schema) -> ::Hash[Symbol, Object]
2077
+
2078
+ # _@return_ — whether the result is a failure the gem itself built
2079
+ # ({Action::GEM_FAILURE_CODES}), whose output is left as it is
2080
+ def gem_failure?: () -> bool
2081
+
2082
+ # Declared-field ivars from the action instance, overlaid with the recorded
2083
+ # +succeed+ / +skip+ kwargs, kwargs winning on conflict.
2084
+ #
2085
+ # _@param_ `schema`
2086
+ def captured_attributes: (singleton(FieldStruct::Base) schema) -> ::Hash[Symbol, Object]
2087
+
2088
+ # The +succeed+ / +skip+ kwargs recorded with the current result. They sit
2089
+ # on +@result.output+ as a Hash only until the first refresh replaces it
2090
+ # with the struct, so they are kept here to overlay every later refresh
2091
+ # (after each hook) as well.
2092
+ def recorded_overrides: () -> ::Hash[Symbol, Object]
2093
+
2094
+ # _@param_ `output` — the invalid output struct
2095
+ #
2096
+ # _@return_ — code +:invalid_output+, with the output's validation
2097
+ # errors copied onto the failure and the (invalid) output attached
2098
+ def invalid_output_failure: (FieldStruct::Base output) -> Failure
2099
+
2100
+ # _@return_ — the running action's transaction config
2101
+ def transaction_config: () -> ::Hash[Symbol, Object]?
2102
+
2103
+ # The configured options, plus +requires_new: true+ when a transaction is
2104
+ # already open and the action did not say otherwise. Joining the outer
2105
+ # transaction would swallow this run's rollback, so a recorded failure's
2106
+ # writes would commit with it (decision D9).
2107
+ #
2108
+ # _@param_ `model`
2109
+ #
2110
+ # _@param_ `options`
2111
+ def transaction_options: (Class model, ::Hash[Symbol, Object] options) -> ::Hash[Symbol, Object]
2112
+
2113
+ # _@param_ `model`
2114
+ #
2115
+ # _@return_ — the resolved model class
2116
+ def resolve_model: ((Symbol | Class) model) -> Class
2117
+
2118
+ def schedule_after_commit: () -> void
2119
+
2120
+ # Run the hooks in order. A halt stops the remaining ones; nothing they
2121
+ # record reaches the already-returned result.
2122
+ #
2123
+ # _@param_ `hooks`
2124
+ def run_after_commit_hooks: (::Array[Steps::Base] hooks) -> void
2125
+ end
2126
+
2127
+ # An ordered record of the steps a run executed, when measurement is enabled
2128
+ # (decision D10). Each entry is a {History::Step}; nested action steps carry
2129
+ # their child's +History+.
2130
+ class History
2131
+ def initialize: () -> void
2132
+
2133
+ # Append a recorded step.
2134
+ #
2135
+ # _@param_ `step`
2136
+ def <<: (Step step) -> self
2137
+
2138
+ # _@return_ — whether any step has been recorded
2139
+ def empty?: () -> bool
2140
+
2141
+ # _@return_ — the summed duration of every recorded step
2142
+ def took: () -> Float
2143
+
2144
+ # _@return_ — each step as a JSON-ready hash
2145
+ def as_json: () -> ::Array[::Hash[untyped, untyped]]
2146
+
2147
+ # _@return_ — the history serialized to JSON via Oj (decision D15).
2148
+ # +:compat+ mode emits plain JSON (string keys, symbols as strings) rather
2149
+ # than Oj's default object-marker encoding.
2150
+ def to_json: () -> String
2151
+
2152
+ # _@return_ — the recorded steps, in execution order
2153
+ attr_reader steps: ::Array[Step]
2154
+
2155
+ # One recorded step: which section it ran in, its name, when it started, how
2156
+ # long it took, the result code it produced (if any), and — for action
2157
+ # steps — the nested child {History}.
2158
+ class Step
2159
+ # _@param_ `section`
2160
+ #
2161
+ # _@param_ `name`
2162
+ #
2163
+ # _@param_ `started_at`
2164
+ def initialize: (section: Symbol, name: (Symbol | String | Class), started_at: Time) -> void
2165
+
2166
+ # _@return_ — a JSON-ready hash (nested history recurses)
2167
+ def as_json: () -> ::Hash[Symbol, Object]
2168
+
2169
+ # _@return_ — +:main+, +:success+, +:failure+, or +:always+
2170
+ attr_reader section: Symbol
2171
+
2172
+ # _@return_ — the step's name
2173
+ attr_reader name: (Symbol | String | Class)
2174
+
2175
+ # _@return_ — wall-clock start time
2176
+ attr_reader started_at: Time
2177
+
2178
+ # _@return_ — elapsed seconds (monotonic)
2179
+ attr_accessor duration: Float
2180
+
2181
+ # _@return_ — the result code this step recorded, if it set one
2182
+ attr_accessor code: Symbol?
2183
+
2184
+ # _@return_ — the nested action's history, for action steps
2185
+ attr_accessor nested: History?
2186
+ end
2187
+ end
2188
+
2189
+ # A read-only map of every named action class, keyed by class name (decision
2190
+ # D11). {Action} registers each subclass through its +inherited+ hook;
2191
+ # anonymous subclasses (which have no name) are skipped. Used for discovery
2192
+ # and tooling — e.g. sweeping all actions to generate RBS.
2193
+ class Registry
2194
+ def initialize: () -> void
2195
+
2196
+ # Register an action class under its name. No-op for anonymous classes.
2197
+ #
2198
+ # _@param_ `klass` — the action subclass
2199
+ def register: (Class klass) -> self
2200
+
2201
+ # _@param_ `name` — an action class name
2202
+ #
2203
+ # _@return_ — the registered class, or +nil+
2204
+ def []: ((String | Symbol) name) -> Class?
2205
+
2206
+ def each: () ?{ (String name, Class klass) -> void } -> (::Enumerator[untyped] | self)
2207
+
2208
+ # _@return_ — the registered class names
2209
+ def keys: () -> ::Array[String]
2210
+
2211
+ # _@return_ — the registered classes
2212
+ def values: () -> ::Array[Class]
2213
+
2214
+ # _@return_ — the number of registered classes
2215
+ def size: () -> Integer
2216
+
2217
+ # _@return_ — whether nothing is registered
2218
+ def empty?: () -> bool
2219
+ end
2220
+
2221
+ # Thread-local measurement context (decision D10). It lets a measuring run
2222
+ # cascade into the nested actions it invokes, and lets an action step attach
2223
+ # its child's history to the step record currently open in the parent runner.
2224
+ #
2225
+ # All state is per-thread and saved/restored around each scope, so concurrent
2226
+ # runs on different threads never interfere.
2227
+ module Measurement
2228
+ # _@return_ — whether the current thread is inside a measuring run
2229
+ def self.active?: () -> bool
2230
+
2231
+ # _@return_ — the step record nested history attaches to
2232
+ def self.open_step: () -> History::Step?
2233
+
2234
+ # Mark measurement active for the block, so nested runs measure too.
2235
+ #
2236
+ # _@return_ — the block's value
2237
+ def self.measuring: () ?{ () -> Object } -> Object
2238
+
2239
+ # Make +record+ the open step for the block, so a nested action step can
2240
+ # attach its child's history to it.
2241
+ #
2242
+ # _@param_ `record`
2243
+ #
2244
+ # _@return_ — the block's value
2245
+ def self.recording: (History::Step record) ?{ () -> Object } -> Object
2246
+
2247
+ # This thread's measurement state: +:active+ and +:open_step+.
2248
+ def self.store: () -> ::Hash[Symbol, Object]
2249
+
2250
+ # Merge +changes+ into the state for the block, restoring it afterwards.
2251
+ #
2252
+ # _@param_ `changes` — the keys to override
2253
+ #
2254
+ # _@return_ — the block's value
2255
+ def self.with: (::Hash[Symbol, Object] changes) ?{ () -> Object } -> Object
2256
+ end
2257
+
2258
+ # Shared branch-value matching for the two places Actionable dispatches on a
2259
+ # value: case steps (decision D3) and discriminated input (decision D19). A
2260
+ # branch value matches by Array membership, by +Regexp#match?+ against the
2261
+ # value's string form, or by +==+.
2262
+ module ValueMatch
2263
+ # _@param_ `branch_value` — the declared branch matcher
2264
+ #
2265
+ # _@param_ `value` — the runtime value being dispatched on
2266
+ #
2267
+ # _@return_ — whether +value+ matches +branch_value+
2268
+ def matches?: ((Object | ::Array[untyped] | Regexp) branch_value, Object value) -> bool
2269
+
2270
+ # _@param_ `branch_value` — the declared branch matcher
2271
+ #
2272
+ # _@param_ `value` — the runtime value being dispatched on
2273
+ #
2274
+ # _@return_ — whether +value+ matches +branch_value+
2275
+ def self.matches?: ((Object | ::Array[untyped] | Regexp) branch_value, Object value) -> bool
2276
+ end
2277
+
2278
+ # The value object returned by {Action.run_each}: an ordered, +Enumerable+
2279
+ # collection of the per-item {Result}s, plus aggregate predicates over them
2280
+ # (decision D19). Each item ran independently — one {Result} per element, in
2281
+ # input order — so a failure in one never affects another.
2282
+ #
2283
+ # batch = ImportRow.run_each(rows) { |row| {row: row} }
2284
+ # batch.all_ok? # => false
2285
+ # batch.failures.size # => 2
2286
+ # batch.to_api_h # => [{index: 0, status: :success, ...}, ...]
2287
+ class BatchResult
2288
+ include ::Enumerable[Result]
2289
+
2290
+ # _@param_ `results`
2291
+ def initialize: (::Array[Result] results) -> void
2292
+
2293
+ def each: () ?{ (Result result) -> void } -> (::Enumerator[untyped] | self)
2294
+
2295
+ # _@param_ `index`
2296
+ #
2297
+ # _@return_ — the result at +index+
2298
+ def []: (Integer index) -> Result?
2299
+
2300
+ # _@return_ — the number of results
2301
+ def size: () -> Integer
2302
+
2303
+ # _@return_ — whether the batch ran no items
2304
+ def empty?: () -> bool
2305
+
2306
+ # _@return_ — whether every result is +ok?+ (success or skip);
2307
+ # vacuously true for an empty batch
2308
+ def all_ok?: () -> bool
2309
+
2310
+ # _@return_ — whether any result is a failure
2311
+ def any_failure?: () -> bool
2312
+
2313
+ # _@return_ — whether the batch is mixed — at least one failure and at
2314
+ # least one +ok?+ result
2315
+ def partial?: () -> bool
2316
+
2317
+ # _@return_ — the successful results
2318
+ def successes: () -> ::Array[Result]
2319
+
2320
+ # _@return_ — the failed results
2321
+ def failures: () -> ::Array[Result]
2322
+
2323
+ # _@return_ — the skipped results
2324
+ def skips: () -> ::Array[Result]
2325
+
2326
+ # The batch as an array of API elements, each tagged with its position
2327
+ # (see {Result#to_api_h}). Ready to render as a JSON array of per-item
2328
+ # statuses.
2329
+ #
2330
+ # _@param_ `id_field` — the output field to surface as +:id+
2331
+ def to_api_h: (?id_field: Symbol) -> ::Array[::Hash[Symbol, Object]]
2332
+
2333
+ # _@return_ — the per-item results, in input order
2334
+ attr_reader results: ::Array[Result]
2335
+ end
2336
+
2337
+ # Runtime selection of an input schema from a discriminator value (decision
2338
+ # D19, declared with +input_for+). Each branch maps a match value to a
2339
+ # FieldStruct schema; an optional +default+ catches the rest. Lets a
2340
+ # polymorphic payload (e.g. +event_type+ → shape) use typed, validated input.
2341
+ #
2342
+ # input_for :event_type do
2343
+ # on 'sale', SaleShape
2344
+ # on 'refund', RefundShape
2345
+ # default UnknownShape
2346
+ # end
2347
+ class InputDispatch
2348
+ # Build a dispatch from an +input_for+ declaration.
2349
+ #
2350
+ # _@param_ `discriminator` — the kwarg whose value selects the schema
2351
+ def self.define: ((Symbol | String) discriminator) -> InputDispatch
2352
+
2353
+ # _@param_ `discriminator`
2354
+ #
2355
+ # _@param_ `branches`
2356
+ #
2357
+ # _@param_ `default_schema`
2358
+ def initialize: (Symbol discriminator, ::Array[[Object, singleton(FieldStruct::Base)]] branches, singleton(FieldStruct::Base)? default_schema) -> void
2359
+
2360
+ # The schema for a discriminator value: the first matching branch, else the
2361
+ # default, else +nil+ (no match and no default).
2362
+ #
2363
+ # _@param_ `value` — the discriminator value
2364
+ def schema_for: (Object value) -> singleton(FieldStruct::Base)?
2365
+
2366
+ # _@return_ — every distinct schema reachable
2367
+ # through this dispatch (branches plus default)
2368
+ def schemas: () -> ::Array[singleton(FieldStruct::Base)]
2369
+
2370
+ # _@return_ — the branch match values, in declaration order
2371
+ def variants: () -> ::Array[Object]
2372
+
2373
+ # _@return_ — the discriminator field read from +.run+'s kwargs
2374
+ attr_reader discriminator: Symbol
2375
+
2376
+ # _@return_ — ordered [match, schema] pairs
2377
+ attr_reader branches: ::Array[[Object, singleton(FieldStruct::Base)]]
2378
+
2379
+ # _@return_ — the fallback schema
2380
+ attr_reader default_schema: singleton(FieldStruct::Base)?
2381
+
2382
+ # Collects +on+/+default+ branch declarations inside an +input_for+ block.
2383
+ class Builder
2384
+ def initialize: () -> void
2385
+
2386
+ # Register a branch. +value+ matches the discriminator by +==+, by
2387
+ # Array membership, or by +Regexp#match?+ (see {ValueMatch}).
2388
+ #
2389
+ # _@param_ `value`
2390
+ #
2391
+ # _@param_ `schema`
2392
+ def on: ((Object | ::Array[untyped] | Regexp) value, singleton(FieldStruct::Base) schema) -> void
2393
+
2394
+ # Register the fallback schema, used when no +on+ branch matches.
2395
+ #
2396
+ # _@param_ `schema`
2397
+ def default: (singleton(FieldStruct::Base) schema) -> void
2398
+
2399
+ # _@return_ — ordered [match, schema] pairs
2400
+ attr_reader branches: ::Array[[Object, singleton(FieldStruct::Base)]]
2401
+
2402
+ # _@return_ — the fallback schema, if declared
2403
+ attr_reader default_schema: singleton(FieldStruct::Base)?
2404
+ end
2405
+ end
2406
+
2407
+ # A failed run's result. Its +code+ is the error symbol describing why the
2408
+ # run failed (e.g. +:not_found+, +:invalid_input+), defaulting to +:error+.
2409
+ class Failure < Actionable::Result
2410
+ # _@return_ — the error code; defaults to +:error+
2411
+ def code: () -> Symbol
2412
+
2413
+ # _@return_ — the metadata the action declared for this
2414
+ # code with {Action.fails} (decision D20), so an adapter can read it off the
2415
+ # result without the action class. Empty when the code isn't declared.
2416
+ # The gem stores and passes it through but never interprets it.
2417
+ def metadata: () -> ::Hash[Symbol, Object]
2418
+
2419
+ def failure?: () -> bool
2420
+
2421
+ # A plain-Hash view of the failure: {Result#to_h} plus its declared
2422
+ # +metadata+ (decision D20).
2423
+ def to_h: () -> ::Hash[Symbol, Object]
2424
+
2425
+ # Copy another object's FieldStruct-style errors onto this failure's own
2426
+ # errors collection. The shared mechanism behind +Failure(:invalid_input)+,
2427
+ # +Failure(:invalid_output)+, and the {Action#fail_with} verb (decision D19).
2428
+ #
2429
+ # _@param_ `source` — any object whose +errors+ responds to +to_h+ (a +FieldStruct::Base+ instance, or another result)
2430
+ #
2431
+ # _@return_ — so callers can build-and-absorb in one expression
2432
+ def absorb_errors_from: (untyped source) -> self
2433
+ end
2434
+
2435
+ # A skipped run's result (decision D17): the action had nothing to do — not a
2436
+ # success (no real work), but not a failure either. Its +code+ is the skip
2437
+ # reason (e.g. +:not_ready+), defaulting to +:skipped+.
2438
+ class Skipped < Actionable::Result
2439
+ # _@return_ — the skip reason; defaults to +:skipped+
2440
+ def code: () -> Symbol
2441
+
2442
+ # _@return_ — the metadata the action declared for this
2443
+ # code with {Action.skips} (decision D21), so an adapter can read it off the
2444
+ # result without the action class. Empty when the code isn't declared.
2445
+ # The gem stores and passes it through but never interprets it.
2446
+ def metadata: () -> ::Hash[Symbol, Object]
2447
+
2448
+ def skipped?: () -> bool
2449
+
2450
+ # A plain-Hash view of the skip: {Result#to_h} plus its declared +metadata+
2451
+ # (decision D21).
2452
+ def to_h: () -> ::Hash[Symbol, Object]
2453
+ end
2454
+
2455
+ # A successful run's result. Its +code+ defaults to +:success+.
2456
+ class Success < Actionable::Result
2457
+ # _@return_ — always +:success+ unless explicitly overridden
2458
+ def code: () -> Symbol
2459
+
2460
+ def success?: () -> bool
2461
+ end
2462
+
2463
+ module Rails
2464
+ # The +after_commit+ hook kind. Prepended onto {Actionable::Action}'s
2465
+ # singleton class by +actionable/rails+, so the core never knows about it
2466
+ # (decision D8).
2467
+ #
2468
+ # An +after_commit+ hook runs once, after the database work of a
2469
+ # successful run has really committed: it is registered with
2470
+ # +ActiveRecord.after_all_transactions_commit+ when the run ends, so it
2471
+ # waits for the outermost open transaction (not the action's own
2472
+ # savepoint), runs immediately when none is open, and never runs if any
2473
+ # enclosing transaction rolls back.
2474
+ module AfterCommit
2475
+ MINIMUM_ACTIVE_RECORD: String
2476
+
2477
+ # Declare a hook that runs after the outermost database transaction
2478
+ # commits, iff the run's final result is a {Success}.
2479
+ #
2480
+ # after_commit :send_receipt
2481
+ # after_commit NotifyWarehouse, input: %i[order]
2482
+ #
2483
+ # The run is over by then: the result has been returned (or is about to
2484
+ # be, when no transaction is open), so the hook cannot change it, a
2485
+ # halting verb only stops the remaining +after_commit+ hooks, and the
2486
+ # hook is not recorded in the history. An exception it raises propagates
2487
+ # from wherever the commit happens: the outer +transaction+ call, or
2488
+ # +.run+ itself when no transaction was open.
2489
+ #
2490
+ # _@param_ `target` — the instance method or nested action to run
2491
+ #
2492
+ # _@param_ `options` — step options (+:if+, +:unless+, and for a nested action +:input+)
2493
+ def after_commit: ((Symbol | String | singleton(Actionable::Action)) target, **Object options) -> ::Set[Steps::Base]
2494
+
2495
+ # Hooks that run after the outermost transaction commits a successful run.
2496
+ def after_commit_hooks: () -> ::Set[Steps::Base]
2497
+
2498
+ # {Actionable::Action.describe}, with the +after_commit+ hooks listed as
2499
+ # one more +:hooks+ section, so {Actionable::Action.describe_text} shows
2500
+ # them too (only when there are any, like the core sections).
2501
+ def describe: () -> ::Hash[Symbol, Object]
2502
+
2503
+ # Give each subclass its own copy of the inherited +after_commit+ hooks.
2504
+ #
2505
+ # _@param_ `subclass`
2506
+ def inherited: (Class subclass) -> void
2507
+
2508
+ # Extend the run-start guard (decision D18) to the +after_commit+ hooks.
2509
+ #
2510
+ # _@param_ `instance`
2511
+ def ensure_steps_implemented!: (Action _instance) -> void
2512
+
2513
+ # Extend the recording-verb position check (owner ruling R5) to the
2514
+ # +after_commit+ hooks.
2515
+ def verb_checked_steps: () -> ::Array[Steps::Base]
2516
+
2517
+ def ensure_after_commit_supported!: () -> void
2518
+ end
2519
+
2520
+ # Prepended onto {Actionable::Runner} (outside {RunnerTransaction}) to
2521
+ # register a successful run's +after_commit+ hooks once the run, and its
2522
+ # own transaction, are over.
2523
+ module RunnerAfterCommit
2524
+ def run: () -> Result
2525
+
2526
+ def schedule_after_commit: () -> void
2527
+
2528
+ # Run the hooks in order. A halt stops the remaining ones; nothing they
2529
+ # record reaches the already-returned result.
2530
+ #
2531
+ # _@param_ `hooks`
2532
+ def run_after_commit_hooks: (::Array[Steps::Base] hooks) -> void
2533
+ end
2534
+
2535
+ # Internal signal raised inside a transaction block to trigger a rollback
2536
+ # without surfacing as a genuine error. The runner wrapper swallows it; a
2537
+ # real ActiveRecord transaction rolls back and re-raises it (as it does for
2538
+ # any exception), and the wrapper catches it outside the block.
2539
+ class Rollback < StandardError
2540
+ end
2541
+
2542
+ # The +transactional+ class macro (decision D9). Extended onto
2543
+ # {Actionable::Action} by +actionable/rails+.
2544
+ module Transactions
2545
+ # Wrap this action's run in +model.transaction(**options)+.
2546
+ #
2547
+ # transactional model: :invoice
2548
+ # transactional model: Invoice, requires_new: false # join an outer one
2549
+ #
2550
+ # Run inside a transaction that is already open, the action gets its own
2551
+ # savepoint (+requires_new: true+) unless +requires_new:+ is given, so a
2552
+ # recorded +Failure+ rolls back its own writes rather than leaving them for
2553
+ # the outer transaction to commit.
2554
+ #
2555
+ # _@param_ `model` — the model whose +transaction+ is used; a Symbol is resolved to a constant (+:invoice+ → +Invoice+)
2556
+ #
2557
+ # _@param_ `options` — options forwarded to +transaction+
2558
+ def transactional: (model: (Symbol | Class), **Object options) -> void
2559
+
2560
+ # The transaction config for this action, inherited from ancestors when
2561
+ # not set locally (decision D9: inherited, overridable).
2562
+ #
2563
+ # _@return_ — +{model:, options:}+, or +nil+ when
2564
+ # neither this action nor an ancestor is +transactional+
2565
+ def transaction_config: () -> ::Hash[Symbol, Object]?
2566
+ end
2567
+
2568
+ # Prepended onto {Actionable::Runner} so a +transactional+ action runs its
2569
+ # whole pipeline inside a transaction. A raised exception or a recorded
2570
+ # +Failure+ rolls everything back; a +Success+ commits (decision D9).
2571
+ module RunnerTransaction
2572
+ def run: () -> Result
2573
+
2574
+ # _@return_ — the running action's transaction config
2575
+ def transaction_config: () -> ::Hash[Symbol, Object]?
2576
+
2577
+ # The configured options, plus +requires_new: true+ when a transaction is
2578
+ # already open and the action did not say otherwise. Joining the outer
2579
+ # transaction would swallow this run's rollback, so a recorded failure's
2580
+ # writes would commit with it (decision D9).
2581
+ #
2582
+ # _@param_ `model`
2583
+ #
2584
+ # _@param_ `options`
2585
+ def transaction_options: (Class model, ::Hash[Symbol, Object] options) -> ::Hash[Symbol, Object]
2586
+
2587
+ # _@param_ `model`
2588
+ #
2589
+ # _@return_ — the resolved model class
2590
+ def resolve_model: ((Symbol | Class) model) -> Class
2591
+ end
2592
+ end
2593
+ end