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.
- checksums.yaml +7 -0
- data/.claude-plugin/marketplace.json +15 -0
- data/.claude-plugin/plugin.json +9 -0
- data/.rspec +3 -0
- data/AGENTS.md +150 -0
- data/CHANGELOG.md +766 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +555 -0
- data/UPGRADING.md +957 -0
- data/USAGE.md +1291 -0
- data/docs/getting_started.md +611 -0
- data/lib/actionable/action.rb +1220 -0
- data/lib/actionable/batch_result.rb +92 -0
- data/lib/actionable/errors.rb +36 -0
- data/lib/actionable/history.rb +91 -0
- data/lib/actionable/input_dispatch.rb +94 -0
- data/lib/actionable/job.rb +157 -0
- data/lib/actionable/measurement.rb +64 -0
- data/lib/actionable/rails/after_commit.rb +142 -0
- data/lib/actionable/rails/proxy_validator.rb +59 -0
- data/lib/actionable/rails/transactions.rb +95 -0
- data/lib/actionable/rails.rb +24 -0
- data/lib/actionable/railtie.rb +10 -0
- data/lib/actionable/rbs.rb +502 -0
- data/lib/actionable/registry.rb +64 -0
- data/lib/actionable/result.rb +166 -0
- data/lib/actionable/results/failure.rb +43 -0
- data/lib/actionable/results/skipped.rb +30 -0
- data/lib/actionable/results/success.rb +14 -0
- data/lib/actionable/rspec/coverage/report.rb +264 -0
- data/lib/actionable/rspec/coverage/tracker.rb +330 -0
- data/lib/actionable/rspec/coverage.rb +45 -0
- data/lib/actionable/rspec/matchers.rb +266 -0
- data/lib/actionable/rspec/stubs.rb +250 -0
- data/lib/actionable/rspec.rb +40 -0
- data/lib/actionable/rubocop/config.yml +8 -0
- data/lib/actionable/rubocop/cop/actionable/verb_not_last.rb +62 -0
- data/lib/actionable/rubocop/plugin.rb +31 -0
- data/lib/actionable/rubocop.rb +25 -0
- data/lib/actionable/runner.rb +300 -0
- data/lib/actionable/steps/action.rb +76 -0
- data/lib/actionable/steps/base.rb +107 -0
- data/lib/actionable/steps/case.rb +124 -0
- data/lib/actionable/steps/method.rb +39 -0
- data/lib/actionable/steps.rb +27 -0
- data/lib/actionable/value_match.rb +22 -0
- data/lib/actionable/verb_position.rb +574 -0
- data/lib/actionable/version.rb +6 -0
- data/lib/actionable-rb.rb +6 -0
- data/lib/actionable.rb +59 -0
- data/sig/actionable/dsl.rbs +42 -0
- data/sig/actionable/rails.rbs +16 -0
- data/sig/actionable.rbs +2593 -0
- data/skills/actionable/SKILL.md +214 -0
- metadata +132 -0
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [1.9.0] - 2026-10-24
|
|
6
|
+
|
|
7
|
+
The gem is renamed **`actionable-rb`** so it can be published to rubygems.org,
|
|
8
|
+
where `actionable` belongs to an unrelated 2014 gem. Nothing in the library
|
|
9
|
+
changed: `require 'actionable'`, the `Actionable` module and every API stay the
|
|
10
|
+
same. Two lines in an app change: the Gemfile's `gem 'actionable'` becomes
|
|
11
|
+
`gem 'actionable-rb'`, and a `.rubocop.yml` that loads the cop with `plugins:`
|
|
12
|
+
renames its key to `actionable-rb`. Read `UPGRADING.md` "1.8.2 → 1.9.0".
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **The gem is renamed `actionable-rb`**, so it can be published to rubygems.org,
|
|
17
|
+
where `actionable` belongs to an unrelated 2014 gem. `require 'actionable'`,
|
|
18
|
+
the `Actionable` module and every API are unchanged. A new
|
|
19
|
+
`lib/actionable-rb.rb` lets `Bundler.require` load it by the gem name, and the
|
|
20
|
+
gemspec now pushes to rubygems.org instead of GitHub Packages.
|
|
21
|
+
**Upgrade note:** the Gemfile line becomes `gem 'actionable-rb'`, and a
|
|
22
|
+
`.rubocop.yml` that loads the cop with `plugins:` must use the `actionable-rb`
|
|
23
|
+
key. <!-- upgrading: gem-renamed-actionable-rb -->
|
|
24
|
+
|
|
25
|
+
## [1.8.2] - 2026-10-23
|
|
26
|
+
|
|
27
|
+
Three fixes found by a mutation-driven review, plus upgrade-guide answers from
|
|
28
|
+
a dry run against a large app. All three change what an app can see, so read
|
|
29
|
+
`UPGRADING.md` "1.8.1 → 1.8.2" before bumping. `case_step` branches now honour
|
|
30
|
+
`if:` / `unless:`: a guarded-out branch does nothing, silently, where the guard
|
|
31
|
+
used to be ignored. `Actionable::RBS` generates valid class names for `?` / `!`
|
|
32
|
+
codes and raises on colliding ones. The first-run verb check sees through
|
|
33
|
+
`alias_method`, so it can raise on an aliased step it used to skip.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **`case_step` branches honour `if:` / `unless:`.** `on 'x', :handle, if:
|
|
38
|
+
:ready?` (and `default :y, unless: …`) accepted the guard and then ignored
|
|
39
|
+
it, so the branch always ran. The branch is still picked by value alone; its
|
|
40
|
+
guard now decides whether the target runs, as for a plain step. A matched
|
|
41
|
+
branch whose guard opts out does nothing: no fall-through to the next branch
|
|
42
|
+
or the `default`. **Upgrade note:** a branch you guarded now really skips.
|
|
43
|
+
<!-- upgrading: case-branch-guards -->
|
|
44
|
+
- **`Actionable::RBS` names output classes for `?` / `!` codes validly.**
|
|
45
|
+
Under `typed_outcomes`, `fails(:expired?) { … }` emitted `class
|
|
46
|
+
Expired?FailureOutput`, which RBS can't parse. A trailing `?` / `!` now
|
|
47
|
+
becomes `Predicate` / `Bang` (`ExpiredPredicateFailureOutput`,
|
|
48
|
+
`DoneBangSkipOutput`), so `:x` and `:x?` get different classes. `generate`
|
|
49
|
+
now raises `ArgumentError` when two codes of one kind would share a class
|
|
50
|
+
name (`:ready` / `:ready_`), which silently emitted one class for two
|
|
51
|
+
schemas, or when a code can't start a class name. **Upgrade note:** rename
|
|
52
|
+
one of two clashing codes. <!-- upgrading: rbs-outcome-class-names -->
|
|
53
|
+
- **The first-run verb check sees through `alias_method`.** A step naming an
|
|
54
|
+
alias (`step :charge` with `alias_method :charge, :do_charge`) looked up
|
|
55
|
+
the def by the alias name, found none, and skipped the check; it now reads
|
|
56
|
+
the original `def`, so a misplaced verb there raises `DefinitionError` like
|
|
57
|
+
any other step. Documented limit: a module `prepend`ed over a step method
|
|
58
|
+
hides the class's own `def` from the check. **Upgrade note:** fix the
|
|
59
|
+
reported verb, as for any verb-position offense.
|
|
60
|
+
<!-- upgrading: verb-check-alias -->
|
|
61
|
+
|
|
62
|
+
### Documentation
|
|
63
|
+
|
|
64
|
+
- **UPGRADING.md, from a dry run against a large app (133 actions).** The guide
|
|
65
|
+
now says which six Finds must run on a green suite (silent behaviour changes)
|
|
66
|
+
and that the rest cover code the suite doesn't reach. The `describe` Find skips
|
|
67
|
+
`RSpec.describe` (1,552 hits before). The code Finds skip receivers and
|
|
68
|
+
non-Ruby files. The `@result` entry says a `Result` you build yourself is still
|
|
69
|
+
allowed, with `halt!`. A new section lists the class-level API unchanged since
|
|
70
|
+
1.5.1. The cop entry says to list every base class defined in its own file. The
|
|
71
|
+
opt-out goes in `spec_helper`, not an initializer. The effort estimate counts
|
|
72
|
+
suite runs, and there is a PCRE2 check. AGENTS.md and the skill follow.
|
|
73
|
+
|
|
74
|
+
## [1.8.1] - 2026-10-23
|
|
75
|
+
|
|
76
|
+
Housekeeping. The `Actionable/VerbNotLast` cop resolves superclass names the way
|
|
77
|
+
Ruby does, so same-named classes in one file no longer confuse it; the
|
|
78
|
+
UPGRADING.md drift guard is fixture-tested and now asks for an explicit
|
|
79
|
+
decision on every visible CHANGELOG entry. No runtime behaviour changes.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- **`Actionable/VerbNotLast` resolves a superclass by its lexical full name.**
|
|
84
|
+
It matched by the last name segment, so in one file `module B; class Base;
|
|
85
|
+
end; class X < Base` counted as an action when `module A` held a `Base <
|
|
86
|
+
Actionable::Action`, and a file's own `Legacy::ApplicationAction` counted as
|
|
87
|
+
the listed base. It now looks the name up as Ruby does (enclosing `module` /
|
|
88
|
+
`class` outwards, a compact `class A::B` opens only `A::B`, `::X` is the
|
|
89
|
+
top level), so those classes are left alone, and a listed full name is
|
|
90
|
+
found from inside its namespace (`module Ops; class X < BaseAction` with
|
|
91
|
+
`Ops::BaseAction` listed). An offense it newly reports is a call the
|
|
92
|
+
first-run check already raises on. <!-- upgrading: none -->
|
|
93
|
+
|
|
94
|
+
### Tests / CI
|
|
95
|
+
|
|
96
|
+
- **The UPGRADING.md drift guard asks for a decision on every visible
|
|
97
|
+
change.** Each `[Unreleased]` bullet under `### Changed`, `### Removed` or
|
|
98
|
+
`### Breaking…` must carry an `upgrading: <slug>` marker comment (with its
|
|
99
|
+
guide entry, as before) or `upgrading: none`, so a change filed there
|
|
100
|
+
without an upgrade note no longer slips past it. Released sections are not
|
|
101
|
+
re-checked. The guard's own examples now run on fixtures, so they hold in
|
|
102
|
+
any release state; one example still checks the live files.
|
|
103
|
+
|
|
104
|
+
## [1.8.0] - 2026-10-22
|
|
105
|
+
|
|
106
|
+
A guard against silently continuing runs, plus upgrade guidance. A plain
|
|
107
|
+
`fail` / `fail_with` / `skip` / `succeed` must now be the last thing its step
|
|
108
|
+
or hook does — checked once per action class on first run (Prism; turn it off
|
|
109
|
+
with `Actionable.verb_position_check = false`) and by a shipped RuboCop cop,
|
|
110
|
+
`Actionable/VerbNotLast`. `fail` / `skip` codes must be identifiers, so the
|
|
111
|
+
`fail 'card expired'` Kernel habit raises instead of recording a code. Both can
|
|
112
|
+
raise on code that used to run: read `UPGRADING.md`, which walks 1.5 → 1.8
|
|
113
|
+
change by change. CI now also runs real ActiveJob/Sidekiq and ActiveRecord 7.1
|
|
114
|
+
lanes. Still requires field_struct >= 0.12.
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
|
|
118
|
+
- **A plain recording verb must end its step or hook method (owner rulings
|
|
119
|
+
R5, R10).** `fail :card_expired if card.expired?` followed by more code keeps
|
|
120
|
+
running — almost always a `fail!` missing its bang. On an action's first
|
|
121
|
+
run, every step method (case-step branch targets included) and lifecycle
|
|
122
|
+
hook method (`on_success` / `on_failure` / `on_skip` / `always`, and
|
|
123
|
+
`after_commit` with the Rails adapter) is parsed with Prism, and a plain
|
|
124
|
+
`fail` / `fail_with` / `skip` / `succeed` (or `self.fail` …) that isn't the
|
|
125
|
+
method's last expression raises `DefinitionError` naming the action, method, `file:line`,
|
|
126
|
+
verb and fix. Branches, `return fail(...)`, a verb followed by `return`,
|
|
127
|
+
and blocks whose call is last are allowed; USAGE (Control flow, "Where a
|
|
128
|
+
plain verb may go") has the full table. Checked once per class: later runs
|
|
129
|
+
pay one instance-variable read. Skipped without Prism >= 1.0 (Ruby 3.2
|
|
130
|
+
without the `prism` gem; no new dependency) and for methods with no
|
|
131
|
+
parseable source (`eval`, `define_method`).
|
|
132
|
+
- **`Actionable.verb_position_check = false`** turns that first-run check off
|
|
133
|
+
for the process (on by default; `Actionable.verb_position_check?` reads it).
|
|
134
|
+
Off, `run` never parses source or loads Prism. It is an escape hatch while
|
|
135
|
+
upgrading, and leaves the cop as the only guard. The identifier check on
|
|
136
|
+
`fail` / `skip` codes stays on: it's a per-call regex, not a parse.
|
|
137
|
+
- **`Actionable/VerbNotLast`, an opt-in RuboCop cop** with the same rules
|
|
138
|
+
(one shared analyzer). Not loaded by `require 'actionable'`; enable it with
|
|
139
|
+
`plugins: [{actionable: {require_path: actionable/rubocop}}]` (RuboCop >=
|
|
140
|
+
1.72, via the gem's lint_roller plugin) or `require: [actionable/rubocop]`.
|
|
141
|
+
It checks a class whose superclass is an action base and that declares
|
|
142
|
+
`step` / `case_step`, and the methods that class names in its declarations.
|
|
143
|
+
An action base is one of the cop's `ActionBaseClasses` (default
|
|
144
|
+
`Actionable::Action`, `ApplicationAction`) or a class defined in the same
|
|
145
|
+
file whose superclass chain reaches one, so a dry-transaction class with
|
|
146
|
+
its own `step` gets no false positives (owner ruling R10).
|
|
147
|
+
- `rake bench` gains `fail! action` and `skip! action` scenarios.
|
|
148
|
+
|
|
149
|
+
### Changed
|
|
150
|
+
|
|
151
|
+
- **`fail` / `skip` codes must be identifiers.** A code that isn't a Symbol
|
|
152
|
+
or String matching `/\A[[:alpha:]_][[:alnum:]_]*[?!]?\z/` (`'card
|
|
153
|
+
expired'`, `:"not-found"`, `'payment.declined'`, a class) raises
|
|
154
|
+
`ArgumentError` at the call, suggesting `raise` for `Kernel#fail` habit.
|
|
155
|
+
Applies to `fail` / `fail!` / `fail_with` / `fail_with!` / `skip` / `skip!`.
|
|
156
|
+
|
|
157
|
+
**Upgrading:** both checks raise on code that used to run. <!-- upgrading: verb-position-guard --> <!-- upgrading: identifier-codes -->
|
|
158
|
+
- `DefinitionError` "… calls `fail` with more code after it …" on an action's
|
|
159
|
+
first run: if the code after the verb should not run, use the bang verb
|
|
160
|
+
(`fail!`); if it should, move the verb to the end of the method (or
|
|
161
|
+
`return fail(...)`). Run the cop over your app to find them all up front.
|
|
162
|
+
If you can't fix them all before deploying, set
|
|
163
|
+
`Actionable.verb_position_check = false` in an initializer until you have.
|
|
164
|
+
- `ArgumentError` "… code "…" is not an identifier": use a Symbol code and
|
|
165
|
+
pass the sentence as the message (`fail(:card_expired, 'card expired')`),
|
|
166
|
+
or `raise` if you meant an exception.
|
|
167
|
+
|
|
168
|
+
### Tests / CI
|
|
169
|
+
|
|
170
|
+
- **Real-backend lanes.** Two new CI jobs feed the required `ci` check:
|
|
171
|
+
`jobs (ActiveJob + Sidekiq)` runs the suite with `gemfiles/jobs.gemfile`, so
|
|
172
|
+
`spec/actionable/job_backends/` proves `Job` + `honor_retry_metadata` on a
|
|
173
|
+
real ActiveJob (`retry_on Actionable::Job::RetryableFailure`: `retry: true`
|
|
174
|
+
re-enqueues until attempts run out, `retry: false` discards or acks, the
|
|
175
|
+
default is unchanged) and that `RetryableFailure` propagates out of a Sidekiq
|
|
176
|
+
job in inline testing mode (no Redis). `activerecord 7.1` runs the suite on
|
|
177
|
+
ActiveRecord 7.1 (`gemfiles/activerecord_7_1.gemfile`, Ruby 3.3):
|
|
178
|
+
`transactional` and savepoints work there, and declaring `after_commit`
|
|
179
|
+
raises the `DefinitionError` naming 7.2. These specs skip in the default
|
|
180
|
+
suite, where the gems or versions they need are absent.
|
|
181
|
+
|
|
182
|
+
### Documentation
|
|
183
|
+
|
|
184
|
+
- **`UPGRADING.md`: how to move an app from 1.5.x to 1.8.0** (owner ruling
|
|
185
|
+
R9). A TL;DR with the order of work (field_struct first, then actionable,
|
|
186
|
+
the cop, RBS), a straight 1.5-to-1.8 path, a symptom index, and a Symptom /
|
|
187
|
+
Find / Fix / Why entry for every change a caller can see, each Find an `rg`
|
|
188
|
+
pattern checked against its "before" code or the tool that finds it. AGENTS.md
|
|
189
|
+
and the skill gain a numbered upgrade procedure for agents; README and
|
|
190
|
+
getting_started link to it, and the gem ships it. `spec/upgrading_spec.rb`
|
|
191
|
+
keeps it honest: every CHANGELOG upgrade note since 1.5.2 carries an
|
|
192
|
+
invisible `upgrading:` slug comment that the guide must carry in that
|
|
193
|
+
version's section, and the guide's declared range must reach the next
|
|
194
|
+
release. Its runnable examples are doctested.
|
|
195
|
+
- README "Types (RBS)" says the adapter signatures (`transactional`,
|
|
196
|
+
`after_commit`, the RSpec helpers, `Job`) are declared on `Actionable::Action`
|
|
197
|
+
even when `actionable/rails` isn't required: RBS can't make a signature
|
|
198
|
+
conditional and `rbs` loads a gem's whole `sig/`.
|
|
199
|
+
|
|
200
|
+
## [1.7.0] - 2026-10-22
|
|
201
|
+
|
|
202
|
+
Lifecycle and adapter hardening. `always` hooks now run even after an outcome
|
|
203
|
+
hook halts; redeclaring a step or hook overrides it in place; the Rails adapter
|
|
204
|
+
gains an opt-in `after_commit` hook (ActiveRecord >= 7.2) that runs only after
|
|
205
|
+
the outermost transaction commits; `Job` can opt in to honouring a failure's or
|
|
206
|
+
skip's declared `retry:`; the Rails, Job and RSpec surfaces now ship RBS. Two
|
|
207
|
+
behaviour changes have upgrade notes under Changed. Still requires
|
|
208
|
+
field_struct >= 0.12.
|
|
209
|
+
|
|
210
|
+
### Added
|
|
211
|
+
|
|
212
|
+
- **`benchmarks/` and `rake bench`.** A dependency-free script (monotonic
|
|
213
|
+
clock, no `benchmark` / benchmark-ips) times a trivial run, a 5-step run with
|
|
214
|
+
`measure :none` and `:all`, 5-field input coercion, a nested action with and
|
|
215
|
+
without measurement, and `history.to_json`. `BENCH_ITERATIONS` sets the loop
|
|
216
|
+
count. Not in the default task and not packaged in the gem; a smoke spec
|
|
217
|
+
runs it with 5 iterations so it can't rot. README has a "Performance"
|
|
218
|
+
section with indicative numbers.
|
|
219
|
+
|
|
220
|
+
### Documentation
|
|
221
|
+
|
|
222
|
+
- **`fail` is not `raise` inside an action.** USAGE (Control flow), README,
|
|
223
|
+
getting_started, AGENTS.md and the skill now warn that the gem's `fail` verb
|
|
224
|
+
replaces `Kernel#fail` in an action's instance methods: `fail 'boom'`
|
|
225
|
+
records a `Failure` with code `:"boom"` and the run continues, and
|
|
226
|
+
`fail SomeError, 'msg'` raises `NoMethodError`. Use `raise` for exceptions
|
|
227
|
+
and `fail!(:code)` to stop with a failure. Behaviour is unchanged.
|
|
228
|
+
- README, getting_started, AGENTS.md and the skill now mention the 1.7.0
|
|
229
|
+
topics where they list their neighbours: step/hook redeclaration (last
|
|
230
|
+
wins), `always` hooks running after a hook halt, `after_commit`, and
|
|
231
|
+
`honor_retry_metadata`, each linking to USAGE.
|
|
232
|
+
|
|
233
|
+
- **RBS for the adapters' public surface (owner ruling P11).** `sig/` now <!-- upgrading: adapter-rbs -->
|
|
234
|
+
declares what clients call from the adapters: `transactional` /
|
|
235
|
+
`transaction_config` and `after_commit` / `after_commit_hooks` on
|
|
236
|
+
`Actionable::Action` (Rails adapter), and the RSpec matchers
|
|
237
|
+
(`perform_actionable` and its chain) and stubs (`allow_actionable_*`,
|
|
238
|
+
`stub_actionable_*`, `actionable_*_result`). They are generated from YARD by
|
|
239
|
+
sord like the core's (`.yardopts` now excludes only `ProxyValidator`, the
|
|
240
|
+
Railtie and `actionable/rspec/coverage`); the one runtime mix-in YARD cannot
|
|
241
|
+
see, `after_commit`'s singleton-class prepend, is declared by hand in
|
|
242
|
+
`sig/actionable/rails.rbs`. A spec compares each adapter type's public
|
|
243
|
+
methods, by reflection, with its RBS so neither can drift. `Actionable::Job`
|
|
244
|
+
was already generated. The report-only Steep lane now checks the adapters
|
|
245
|
+
too.
|
|
246
|
+
- **`describe` / `describe_text` list `after_commit` hooks.** With the Rails <!-- upgrading: describe-after-commit -->
|
|
247
|
+
adapter loaded, `describe[:hooks][:after_commit]` names them, and
|
|
248
|
+
`describe_text` adds an `after_commit:` hooks line when there are any.
|
|
249
|
+
|
|
250
|
+
- **`after_commit` hook kind (Rails adapter).** After `require 'actionable/rails'`,
|
|
251
|
+
`after_commit :method` (or a nested action, with `:if` / `:unless` /
|
|
252
|
+
`:input` like any hook) runs a side effect only after the database work
|
|
253
|
+
really committed. It is registered with
|
|
254
|
+
`ActiveRecord.after_all_transactions_commit` when a run ends in a `Success`,
|
|
255
|
+
so it waits for the **outermost** open transaction (not the action's own
|
|
256
|
+
savepoint), runs immediately when none is open, and never runs if an
|
|
257
|
+
enclosing transaction rolls back. A `Failure` or `Skipped` never runs it.
|
|
258
|
+
The run is over by then: the hook can't change the result, isn't recorded in
|
|
259
|
+
the history, and an exception it raises propagates after the commit (from
|
|
260
|
+
the outer `transaction` call, or `.run` when no transaction was open).
|
|
261
|
+
Needs ActiveRecord >= 7.2; declaring it on an older one (or without
|
|
262
|
+
ActiveRecord loaded) raises a `DefinitionError`. Existing hooks and the
|
|
263
|
+
core are unchanged.
|
|
264
|
+
|
|
265
|
+
- **`honor_retry_metadata`: let `Actionable::Job` act on declared `retry:`
|
|
266
|
+
metadata.** An action that calls it (the macro comes with
|
|
267
|
+
`require 'actionable/job'`) has the `retry:` key of its declared failures and
|
|
268
|
+
skips decide the job's disposition: `retry: true` retries (a skip or a
|
|
269
|
+
permanent-code failure included) and `retry: false` does not (a failure is
|
|
270
|
+
discarded, a skip acks). Codes without the key map as before. Retried skips
|
|
271
|
+
raise the same `RetryableFailure`, carrying the `Skipped`. Inherited; a
|
|
272
|
+
subclass backs out with `honor_retry_metadata(enabled: false)`.
|
|
273
|
+
`Actionable::Job.disposition` takes the matching `honor_retry:` keyword.
|
|
274
|
+
Off by default, so metadata stays neutral (D20/D21) and existing jobs behave
|
|
275
|
+
exactly as before.
|
|
276
|
+
|
|
277
|
+
### Changed
|
|
278
|
+
|
|
279
|
+
- **Redeclaring a step or hook now overrides it.** A second `step :x`, <!-- upgrading: step-redeclaration -->
|
|
280
|
+
`case_step :x`, nested `step SomeAction`, or `on_success` / `on_failure` /
|
|
281
|
+
`on_skip` / `always :x` with the same type and name, in the same class or a
|
|
282
|
+
subclass, replaces the earlier declaration's options (guards, `:input` /
|
|
283
|
+
`:expose`, case branches) and keeps the earlier position. Before, the
|
|
284
|
+
redeclaration was silently dropped and the first declaration's options won,
|
|
285
|
+
so a subclass could not override an inherited step as invariant 5 promises.
|
|
286
|
+
A subclass's override never changes the parent. **Upgrade note:** an action
|
|
287
|
+
that redeclares a step with different options now runs the last options.
|
|
288
|
+
|
|
289
|
+
- **`always` hooks now run even after an outcome hook halts.** A bang verb <!-- upgrading: always-after-halt -->
|
|
290
|
+
(`fail!` / `skip!` / `succeed!`), `halt!`, or a failing nested action inside
|
|
291
|
+
an `on_success` / `on_failure` / `on_skip` hook still has its result adopted
|
|
292
|
+
and still stops the remaining hooks of that category, but it no longer skips
|
|
293
|
+
the `always` hooks: they run next and see the adopted result. Before, one
|
|
294
|
+
halt stopped every remaining hook, so cleanup and audit hooks could be
|
|
295
|
+
silently skipped. A halt inside an `always` hook is adopted the same way and
|
|
296
|
+
stops only the remaining `always` hooks. Under `transactional`, `always`
|
|
297
|
+
hooks still run inside the transaction and the adopted result still decides
|
|
298
|
+
commit or rollback, so an `always` hook's writes roll back with a failed run.
|
|
299
|
+
When measuring, those `always` hooks appear in the history as usual.
|
|
300
|
+
**Upgrade note:** an `always` hook that assumed it would not run after a hook
|
|
301
|
+
failure must now check `result` itself.
|
|
302
|
+
|
|
303
|
+
## [1.6.0] - 2026-10-22
|
|
304
|
+
|
|
305
|
+
Valid, guarded RBS. The gem's own signatures can only get more typed (CI
|
|
306
|
+
ratchet, `untyped` 46 → 27); generated RBS for your actions declares the real
|
|
307
|
+
superclass chain, a typed `InputSchema`, typed `.run` keywords and output
|
|
308
|
+
ivars, built on field_struct 0.12's `FieldStruct::RBS.type_for`. Requires
|
|
309
|
+
field_struct >= 0.12. No runtime behaviour changes.
|
|
310
|
+
|
|
311
|
+
### Added
|
|
312
|
+
|
|
313
|
+
- **`Actionable::RBS.generate` types an action's input and output instance <!-- upgrading: rbs-output-ivars --> <!-- upgrading: rbs-input-schema -->
|
|
314
|
+
variables** (generated RBS only; no runtime change). For an action with a
|
|
315
|
+
fixed `input` schema it emits a nested `class InputSchema < ::FieldStruct::Base`
|
|
316
|
+
with a typed reader and writer per input field, `def input: () -> InputSchema`,
|
|
317
|
+
and a `.run` overload `| (InputSchema input) -> ...` beside the keyword form.
|
|
318
|
+
`InputSchema` exists only in RBS (the runtime schema is anonymous), so
|
|
319
|
+
generation raises `ArgumentError` when Ruby already resolves
|
|
320
|
+
`YourAction::InputSchema` (a constant on the action, an ancestor or an
|
|
321
|
+
included module). It also declares each output field as a nullable
|
|
322
|
+
instance variable (`@total: ::Integer?`), so a step that assigns
|
|
323
|
+
`@total = ...` type-checks. **Upgrade note:** the generated RBS now owns
|
|
324
|
+
those instance variables; delete any declaration of them from your own sig
|
|
325
|
+
files, since RBS rejects an instance variable declared twice.
|
|
326
|
+
|
|
327
|
+
### Fixed
|
|
328
|
+
|
|
329
|
+
- **`Actionable::RBS.generate` declares the real superclass chain.** An action <!-- upgrading: rbs-superclass-chain -->
|
|
330
|
+
under an intermediate base (`class Codes::DecideMapping < ApplicationAction`)
|
|
331
|
+
was declared `< ::Actionable::Action`, hiding its bases from the type
|
|
332
|
+
checker, and a host that declared the real superclass got
|
|
333
|
+
`NoSuperclassFoundError` / a failing `rbs validate` when the base had no
|
|
334
|
+
signature. The action now names its nearest named superclass, and every
|
|
335
|
+
named ancestor up to `Actionable::Action` is declared first, member-less
|
|
336
|
+
(a typed one with its empty result classes), so the RBS resolves on its own.
|
|
337
|
+
The declarations repeat byte-identically across files and merge. A namespace
|
|
338
|
+
that is a class now opens as `class`, not `module`, which RBS rejected as a
|
|
339
|
+
duplicate declaration.
|
|
340
|
+
**Upgrade note:** if your own sig files declare one of those bases with a
|
|
341
|
+
different superclass (e.g. `class ApplicationAction < ::Actionable::Action`
|
|
342
|
+
while Ruby says otherwise), RBS now reports a superclass mismatch; make your
|
|
343
|
+
declaration match Ruby's ancestry or drop it.
|
|
344
|
+
|
|
345
|
+
### Changed
|
|
346
|
+
|
|
347
|
+
- **Optional `.run` keywords are nullable in generated RBS** (`?note: ::String?`): an optional field may be passed `nil` explicitly, and the run succeeds.
|
|
348
|
+
- **A required input field with a default is an optional `.run` keyword** (`?amount: ::Integer`), since `run` succeeds without it. An action whose input is chosen by an `input_for` dispatch (including one inherited under a fixed `input`) gets `.run: (*untyped)`, since the dispatch decides the keywords at run time.
|
|
349
|
+
- **Requires field_struct >= 0.12.** The generator now takes every field type <!-- upgrading: field-struct-0-12 --> <!-- upgrading: rbs-stricter-types -->
|
|
350
|
+
from `FieldStruct::RBS.type_for` instead of its own copy of the mapping, so
|
|
351
|
+
generated action RBS matches `FieldStruct::RBS.generate`. Consumer-visible
|
|
352
|
+
changes to generated RBS: writers on `Output` and the per-code output
|
|
353
|
+
classes are typed `(T? value) -> T?` instead of `(untyped value) ->
|
|
354
|
+
untyped`; scalar array elements are nullable (`::Array[::String?]`); a
|
|
355
|
+
`:value` field is `untyped`, never `untyped?`; a bare generic `ruby_type`
|
|
356
|
+
gets type arguments (`::Hash[untyped, untyped]`); and a field's `rbs_type:`
|
|
357
|
+
override is honoured. A field typed by an anonymous nested struct class now
|
|
358
|
+
raises `ArgumentError` (it previously emitted an invalid `::` name).
|
|
359
|
+
- **The committed signatures can only get more typed.** Contributor tooling
|
|
360
|
+
only; the shipped `sig/actionable.rbs` is unchanged. `rake sigs:untyped`
|
|
361
|
+
(part of the default task) fails when sig/ holds more or fewer than the
|
|
362
|
+
committed ceiling of 46 non-comment `untyped`, so a typing change lowers
|
|
363
|
+
the ceiling with it. `sigs:generate` / `sigs:check` fail on a sord
|
|
364
|
+
`[OMIT]`/`[DUCK]` fallback that isn't in the committed allowlist (24
|
|
365
|
+
today) and on allowlist entries sord no longer emits. sord runs without
|
|
366
|
+
`--replace-errors-with-untyped` (the output is identical) and fails on any
|
|
367
|
+
`SORD_ERROR_` type. `sigs:validate` validates the whole sig/ directory.
|
|
368
|
+
- **Optional, report-only Steep 2.x** via `gemfiles/types.gemfile` and a
|
|
369
|
+
`types` CI job on Ruby 3.4 that never fails the build. The Steepfile now
|
|
370
|
+
checks the core only (the RSpec and Rails adapters are ignored); Steep is
|
|
371
|
+
no longer in the main Gemfile.
|
|
372
|
+
- **More precise signatures** (RBS only; no runtime change). Keyword splats
|
|
373
|
+
are typed per value (`**Object options`, not `**::Hash[Symbol, Object]
|
|
374
|
+
options`, which RBS reads as "every value is a Hash"); `.run`, `#initialize`
|
|
375
|
+
and `Job::Mixin#run_actionable` take `*Object, **Object`; the gem's
|
|
376
|
+
constants (`Actionable::HALT`, `VERSION`, `Action::GEM_FAILURE_CODES`, …)
|
|
377
|
+
are declared; and the halt/measurement helpers declare their block. The
|
|
378
|
+
DSL methods whose block runs with another `self` (`output`, `input`,
|
|
379
|
+
`fails`, `skips`, `case_step`, `input_for`, and the `define` builders)
|
|
380
|
+
gain a block overload with its self-type in the new
|
|
381
|
+
`sig/actionable/dsl.rbs`. `untyped` in sig/ falls from 46 to 27, and the
|
|
382
|
+
sord fallback allowlist from 24 entries to 4.
|
|
383
|
+
|
|
384
|
+
## [1.5.2] - 2026-10-22
|
|
385
|
+
|
|
386
|
+
Runner and nested-action fixes: hooks that halt, kwargs that survive hooks,
|
|
387
|
+
nested actions that neither wipe parent ivars nor commit failed writes.
|
|
388
|
+
Requires field_struct 0.11.0 or later.
|
|
389
|
+
|
|
390
|
+
### Changed
|
|
391
|
+
|
|
392
|
+
- **Requires field_struct >= 0.11.** It is the release whose numeric and input <!-- upgrading: field-struct-0-11 -->
|
|
393
|
+
fixes this one is tested against; CI now pins it by tag.
|
|
394
|
+
- **CI runs Ruby 3.2, 3.3, 3.4 and 4.0** (was 3.3.6 only), plus the docs guard,
|
|
395
|
+
behind one aggregate `ci` check that `main` requires.
|
|
396
|
+
- **The packaged gem leaves out contributor-only files** (`.claude/`,
|
|
397
|
+
`CLAUDE.md`, `docs/origin/`, Rakefile/Steepfile/lint config). It still ships
|
|
398
|
+
`lib/`, `sig/`, the user docs, `AGENTS.md` and the Claude skill.
|
|
399
|
+
|
|
400
|
+
### Fixed
|
|
401
|
+
|
|
402
|
+
- **A failed `transactional` action inside an outer transaction no longer <!-- upgrading: transactional-savepoint -->
|
|
403
|
+
commits its writes.** It joined the outer transaction, whose commit ignored
|
|
404
|
+
the action's rollback, so a recorded `Failure` still committed its rows
|
|
405
|
+
(measured on ActiveRecord 8.1 + SQLite). Inside an open transaction the action
|
|
406
|
+
now takes its own savepoint (`requires_new: true`) unless `requires_new:` is
|
|
407
|
+
given; `requires_new: false` keeps the old joining behaviour.
|
|
408
|
+
- **A nested action no longer wipes parent ivars it never set.** Absorbing a <!-- upgrading: nested-absorb-set-fields --> <!-- upgrading: stub-set-fields -->
|
|
409
|
+
successful or skipped child copied every declared output field, so a field
|
|
410
|
+
the child left nil (or at its default) overwrote the parent's value. Only
|
|
411
|
+
fields the child actually set are absorbed now; `expose:` still narrows them.
|
|
412
|
+
- **Private step, hook and `case_step` methods work.** The definition check <!-- upgrading: core-method-steps -->
|
|
413
|
+
already accepted them, but the step then raised `NoMethodError`; they are
|
|
414
|
+
now called like guards are. A step or hook named after a method the action
|
|
415
|
+
only inherits from Ruby's core (`step :puts`, `always :exit`) raises
|
|
416
|
+
`DefinitionError` up front instead of calling `Kernel`.
|
|
417
|
+
- **`Action.input` without a block reads the schema** instead of replacing it
|
|
418
|
+
with an empty one, matching `output`.
|
|
419
|
+
- **A step that assigns `@result`** gets a clear `DefinitionError` instead of a <!-- upgrading: result-ivar -->
|
|
420
|
+
`NoMethodError` deep in the runner.
|
|
421
|
+
- **`History#to_json` requires Oj itself** rather than relying on another file
|
|
422
|
+
having loaded it.
|
|
423
|
+
|
|
424
|
+
- **A bang verb inside a lifecycle hook now sets the result.** `fail!`, <!-- upgrading: hook-bang-verb -->
|
|
425
|
+
`skip!` or `succeed!` in a hook used to be dropped: `fail!` in `on_success`
|
|
426
|
+
returned `Success`, committed a `transactional` action and acked the job.
|
|
427
|
+
The hook's result is now adopted before the halt stops the remaining hooks,
|
|
428
|
+
as the docs always said. This includes a nested action used as a hook
|
|
429
|
+
(`on_success ChildAction`): a failing child now fails the run and stops the
|
|
430
|
+
remaining hooks.
|
|
431
|
+
- **`succeed`/`skip` kwargs survive lifecycle hooks.** After any hook <!-- upgrading: hook-kwargs -->
|
|
432
|
+
refreshed the output, the kwargs were lost: a required field set only by
|
|
433
|
+
kwarg turned the run into `Failure(:invalid_output)`, and a kwarg that
|
|
434
|
+
overrode an ivar silently reverted to the ivar's value. Present since 1.0.0.
|
|
435
|
+
A `Failure(:invalid_output)` also keeps the output that failed validation
|
|
436
|
+
across hooks instead of having it rebuilt from ivars.
|
|
437
|
+
- **No spurious `errors[:output]` on a failure or skip.** The best-effort, <!-- upgrading: no-spurious-output-error -->
|
|
438
|
+
unvalidated output of a non-typed failure or skip added
|
|
439
|
+
`output: ["is invalid"]` to the result's errors. It no longer does; errors
|
|
440
|
+
the failure recorded itself are kept.
|
|
441
|
+
|
|
442
|
+
## [1.5.1] - 2026-10-21
|
|
443
|
+
|
|
444
|
+
### Added
|
|
445
|
+
|
|
446
|
+
- **`Coverage.new_gap_hint`** sets the advice the baseline-mismatch message
|
|
447
|
+
gives for a new gap. The default (`Coverage::DEFAULT_NEW_GAP_HINT`) is
|
|
448
|
+
unchanged; a host whose baseline only shrinks can drop "or add it to the
|
|
449
|
+
baseline".
|
|
450
|
+
|
|
451
|
+
### Fixed
|
|
452
|
+
|
|
453
|
+
- **RBS for a subclass of a typed action now resolves on its own.** Its result
|
|
454
|
+
classes subclass the parent's (`< ::ApiAuth::BaseAction::Success`), which
|
|
455
|
+
nothing declared when the parent is an abstract base that is never
|
|
456
|
+
generated. `RBS.generate` now declares those parent classes, empty, up the
|
|
457
|
+
chain of typed ancestors.
|
|
458
|
+
- **RBS readers on `Failure` are nullable.** An `invalid_output` failure
|
|
459
|
+
carries the output that broke its schema, so a required field can be
|
|
460
|
+
missing; the readers were typed non-nil.
|
|
461
|
+
|
|
462
|
+
## [1.5.0] - 2026-10-21
|
|
463
|
+
|
|
464
|
+
Typed outcomes (decision D23). Opt-in with `typed_outcomes`: an action that
|
|
465
|
+
doesn't turn it on runs as before, except that a code whose `fails` or `skips`
|
|
466
|
+
declaration carries a block (new syntax) has its output validated.
|
|
467
|
+
|
|
468
|
+
### Added
|
|
469
|
+
|
|
470
|
+
- **`typed_outcomes`** turns typed outcomes on for an action and its
|
|
471
|
+
descendants (`typed_outcomes enabled: false` turns them off for a subclass);
|
|
472
|
+
`typed_outcomes?` reads it.
|
|
473
|
+
- **A block on `fails` / `skips`** declares the output a result with that code
|
|
474
|
+
carries, in the `output` DSL. **`failure_output_schema(code)`** and
|
|
475
|
+
**`skip_output_schema(code)`** read it back, and `describe` shows its fields.
|
|
476
|
+
Such output is built from the call's keywords alone, never from instance
|
|
477
|
+
variables, and validated: output that breaks the schema ends the run as
|
|
478
|
+
`invalid_output`. The block also ends instance-variable capture for that
|
|
479
|
+
code, typed outcomes or not.
|
|
480
|
+
- **`output:` on `fail` / `fail!` / `fail_with` / `fail_with!`**, under typed
|
|
481
|
+
outcomes only: the failure's output, a Hash or a FieldStruct
|
|
482
|
+
(`ArgumentError` otherwise). Without them `output:` on `fail` is still an
|
|
483
|
+
error, and `fail_with` refuses it. Under typed outcomes, output for a code
|
|
484
|
+
with no schema raises `DefinitionError`, naming the code.
|
|
485
|
+
- **Per-action result classes.** Under typed outcomes `.run` returns the
|
|
486
|
+
action's own `Success`, `Failure` or `Skipped`, each a subclass of the gem
|
|
487
|
+
class (or of the parent action's own). A nested failure becomes the parent's
|
|
488
|
+
own `Failure`, judged by the parent's schema for its code; a parent without
|
|
489
|
+
typed outcomes gets a plain `Actionable::Failure`. The RSpec stub helpers
|
|
490
|
+
return them too, with output shaped through the code's schema (or empty
|
|
491
|
+
without one) as a real run would.
|
|
492
|
+
- **RBS:** `Actionable::RBS.generate` types a typed action's `.run` as
|
|
493
|
+
`(Success | Skipped | Failure)`, with each `code` and `output` narrowed and
|
|
494
|
+
the output's readers declared on each class. A host's own result subclass,
|
|
495
|
+
which `.run` still returns as it is, is not described: a known gap.
|
|
496
|
+
|
|
497
|
+
### Changed
|
|
498
|
+
|
|
499
|
+
- Under typed outcomes, a skip or failure whose code has no schema carries an
|
|
500
|
+
empty output, where it used to carry a best-effort capture into the success
|
|
501
|
+
schema. The gem's own `invalid_input` / `invalid_output` keep theirs.
|
|
502
|
+
- Under typed outcomes, results compare by class: `Act::Success` is not `==`
|
|
503
|
+
to an `Actionable::Success` with the same output, and the stub helpers
|
|
504
|
+
return a new object of the action's own class. Specs that compare results
|
|
505
|
+
with `==` need to compare against the action's class.
|
|
506
|
+
|
|
507
|
+
## [1.4.0] - 2026-10-21
|
|
508
|
+
|
|
509
|
+
Declared skips (decision D21), the skip counterpart of 1.3.0's declared
|
|
510
|
+
failures. Additive: an action that declares no skips runs as before, and a
|
|
511
|
+
child skip still continues its parent. One edge can reach existing code, listed
|
|
512
|
+
under Changed: a skip's `to_h`, `inspect` and `attributes` have a `metadata`
|
|
513
|
+
key.
|
|
514
|
+
|
|
515
|
+
Outcome coverage for test suites (decision D22). Opt-in with
|
|
516
|
+
`require 'actionable/rspec/coverage'`; nothing changes for a suite that
|
|
517
|
+
doesn't require it.
|
|
518
|
+
|
|
519
|
+
### Added
|
|
520
|
+
|
|
521
|
+
- **`skips(code, message = nil, **metadata)`** declares a skip code the action
|
|
522
|
+
can end with, plus neutral per-code metadata, the skip counterpart of `fails`
|
|
523
|
+
(decision D21). Declarations are inherited and a subclass can add to them.
|
|
524
|
+
**`declared_skips`** reads them back as `{code => metadata}`, and
|
|
525
|
+
**`skip_declarations`** as `{code => {message:, metadata:}}`.
|
|
526
|
+
- **`Skipped#metadata`** (default `{}`): a `skip` or `skip!` with a declared
|
|
527
|
+
code carries its metadata, and its declared message when the call gives none.
|
|
528
|
+
`Skipped#to_h` includes `metadata`.
|
|
529
|
+
- **Strict once declared.** An action that declares at least one skip code may
|
|
530
|
+
only skip with declared codes; any other `skip` / `skip!` raises
|
|
531
|
+
`Actionable::DefinitionError` at the call, including the implicit default
|
|
532
|
+
`:skipped`. An action that declares no skips is unchecked, and declared
|
|
533
|
+
failures don't make skips strict. **`allows_skip_code?(code)`** answers
|
|
534
|
+
whether an action may skip with a code.
|
|
535
|
+
- **`describe` shows declared skips** as `skips: {code => {message:,
|
|
536
|
+
metadata:}}`, and `describe_text` lists them under `Skips:`, after
|
|
537
|
+
`Failures:`.
|
|
538
|
+
- **RSpec support follows the skip declarations.** On an action that declares
|
|
539
|
+
skips, `allow_actionable_skip` / `stub_actionable_skip` build a skip carrying
|
|
540
|
+
the code's declared metadata (and message, when none is given), and they and
|
|
541
|
+
`and_skip` raise `ArgumentError` for a code the action doesn't declare,
|
|
542
|
+
`:skipped` included. `actionable_skip_result` takes `metadata:`.
|
|
543
|
+
- **`Actionable::RBS` emits a `skip_code` type alias** for an action that
|
|
544
|
+
declares skips, listing the declared codes, after `failure_code` when both
|
|
545
|
+
are declared.
|
|
546
|
+
- **`Actionable::Coverage`** records every real `.run` of a named action as
|
|
547
|
+
`[class name, outcome, code]`, nested runs and `run_each` included. It hooks
|
|
548
|
+
`Action.run` by prepending onto its singleton class, so an RSpec stub of
|
|
549
|
+
`.run` replaces it and a stubbed result never counts as the action tested.
|
|
550
|
+
- **The RSpec stub helpers note their runs** in `Coverage.stubbed` when
|
|
551
|
+
coverage is loaded, so a report can show an outcome that was only ever
|
|
552
|
+
stubbed. They record when the stub answers a call, not when it is set up.
|
|
553
|
+
- **`Coverage.expected_outcomes(action)`** lists what an action can end with,
|
|
554
|
+
read from its declarations at run time: `success`, every `declared_failures`
|
|
555
|
+
code, every `declared_skips` code, and any
|
|
556
|
+
`Coverage.gem_codes` that apply (`:invalid_input` when an input field is
|
|
557
|
+
required, `:invalid_output` when an output field is). `gem_codes` defaults
|
|
558
|
+
to `[]`.
|
|
559
|
+
- **`Coverage.actions`** lists the actions to report: named descendants of
|
|
560
|
+
`Coverage.root` (default `Actionable::Action`) that have steps and whose
|
|
561
|
+
name still resolves to them, filtered by `include_prefixes` and
|
|
562
|
+
`exclude_prefixes`. `Coverage.loader` runs first so a host can eager-load.
|
|
563
|
+
- **`Coverage::Report`** compares the two: `rows` and `totals` (actions,
|
|
564
|
+
expected outcomes, reached, missing), a text table (`to_s`) of each action's
|
|
565
|
+
never-reached outcomes with a "Stubbed only" column, and `to_json`.
|
|
566
|
+
`Coverage.only_observed = true` reports only actions with a real run, for a
|
|
567
|
+
partial or targeted run.
|
|
568
|
+
- **A named baseline of known gaps.** `Coverage.baseline = path` points at a
|
|
569
|
+
committed JSON or YAML file of `{"Action::Name" => [outcome keys]}`. Missing
|
|
570
|
+
outcomes alone never fail; with a baseline, the report fails on a gap the
|
|
571
|
+
baseline doesn't list and on an entry that is covered, no longer declared,
|
|
572
|
+
or names an action that is gone, and its message lists each entry to add
|
|
573
|
+
or remove. `Report#to_baseline` and `#write_baseline` produce the file.
|
|
574
|
+
- **Sharded suites.** `Coverage.write(path)` dumps a shard's runs as JSON and
|
|
575
|
+
`Report.new(files:)` merges any number of them (a glob that matches no file
|
|
576
|
+
raises rather than reporting everything missing). `Coverage.report!` prints
|
|
577
|
+
the report, writes JSON to `ACTIONABLE_COVERAGE_JSON` when set, rewrites
|
|
578
|
+
the baseline under `ACTIONABLE_COVERAGE_UPDATE_BASELINE=true`, and otherwise
|
|
579
|
+
raises `Coverage::BaselineMismatch` when the gaps differ from it.
|
|
580
|
+
- **`require 'actionable/rspec/coverage'`** turns it all on: it starts
|
|
581
|
+
recording and adds an RSpec `after(:suite)` hook that writes the shard file
|
|
582
|
+
named by `ACTIONABLE_COVERAGE_FILE` when that is set, and otherwise prints
|
|
583
|
+
the report and fails the suite on a baseline mismatch.
|
|
584
|
+
|
|
585
|
+
### Changed
|
|
586
|
+
|
|
587
|
+
- **`Skipped#to_h` includes `metadata`** (`{}` unless the code is declared),
|
|
588
|
+
so code comparing a skip's `to_h` to an exact Hash sees one more key.
|
|
589
|
+
`Skipped#inspect` and `#attributes` show it too.
|
|
590
|
+
|
|
591
|
+
## [1.3.0] - 2026-10-21
|
|
592
|
+
|
|
593
|
+
Declared failures (decision D20). Additive: an action that declares no
|
|
594
|
+
failures runs as before. Three edges can reach existing code, listed under
|
|
595
|
+
Changed: `metadata` is now a reserved output name, `Failure#to_h` has a
|
|
596
|
+
`metadata` key, and a nested failure no longer passes through an overridden
|
|
597
|
+
`fail`.
|
|
598
|
+
|
|
599
|
+
### Added
|
|
600
|
+
|
|
601
|
+
- **`fails(code, message = nil, **metadata)`** declares a failure code the
|
|
602
|
+
action can end with, plus neutral per-code metadata (arbitrary keywords the
|
|
603
|
+
gem stores and passes through but never interprets). Declarations are
|
|
604
|
+
inherited and a subclass can add to them. **`declared_failures`** reads them
|
|
605
|
+
back as `{code => metadata}`.
|
|
606
|
+
- **`Failure#metadata`** (default `{}`): a `fail`, `fail!`, `fail_with` or
|
|
607
|
+
`fail_with!` with a declared code carries its metadata, and its declared
|
|
608
|
+
message when the call gives none. Declaring `:invalid_input` or
|
|
609
|
+
`:invalid_output` attaches metadata to the failures the gem builds itself.
|
|
610
|
+
`Failure#to_h` includes `metadata`.
|
|
611
|
+
|
|
612
|
+
- **`describe` shows declared failures** as `failures: {code => {message:,
|
|
613
|
+
metadata:}}`, and `describe_text` lists them under `Failures:`.
|
|
614
|
+
- **Strict once declared.** An action that declares at least one failure code
|
|
615
|
+
may only fail with declared codes; any other `fail` / `fail_with` raises
|
|
616
|
+
`Actionable::DefinitionError` at the call. Exempt: actions that declare none,
|
|
617
|
+
and the codes the gem emits itself, `Actionable::Action::GEM_FAILURE_CODES`
|
|
618
|
+
(`:invalid_input`, `:invalid_output`).
|
|
619
|
+
|
|
620
|
+
- **`allows_failure_code?(code)`** answers whether an action may fail with a
|
|
621
|
+
code under those rules.
|
|
622
|
+
- **RSpec support follows the declarations.** On an action that declares
|
|
623
|
+
failures, `allow_actionable_failure` / `stub_actionable_failure` build a
|
|
624
|
+
failure carrying the code's declared metadata (and message, when none is
|
|
625
|
+
given), and they and `and_fail` raise `ArgumentError` for a code the action
|
|
626
|
+
doesn't declare. `actionable_failure_result` takes `metadata:`.
|
|
627
|
+
|
|
628
|
+
- **`Actionable::RBS.generate`** emits a `failure_code` type alias for an
|
|
629
|
+
action that declares failures: the declared codes, then the codes the gem
|
|
630
|
+
emits itself.
|
|
631
|
+
|
|
632
|
+
### Changed
|
|
633
|
+
|
|
634
|
+
- **A nested action's failure crosses intact.** `Steps::Action` used to re-fail
|
|
635
|
+
the parent with `fail!(code, message, **errors)`, which dropped anything else
|
|
636
|
+
on the child's failure and let a child error keyed like a keyword of an
|
|
637
|
+
overridden `fail` (pbx's `kind:`) bind to that keyword. The parent now adopts
|
|
638
|
+
the child's code, message, every error, its metadata and its result class
|
|
639
|
+
directly, with the parent's declared metadata for the code filling in keys
|
|
640
|
+
the child didn't set. A host override of `fail` no longer runs for a
|
|
641
|
+
propagated failure.
|
|
642
|
+
- `metadata` joins the reserved output field names, since an output field of
|
|
643
|
+
that name would be shadowed on a failure.
|
|
644
|
+
- `Failure#to_h` gains a `metadata` key (`{}` when the code isn't declared).
|
|
645
|
+
|
|
646
|
+
## [1.2.2] - 2026-06-03
|
|
647
|
+
|
|
648
|
+
### Tooling
|
|
649
|
+
|
|
650
|
+
- The repo is now a single-plugin Claude Code marketplace
|
|
651
|
+
(`.claude-plugin/{plugin.json,marketplace.json}`), so the bundled skill installs
|
|
652
|
+
via `/plugin marketplace add Paymentbox-com/actionable` +
|
|
653
|
+
`/plugin install actionable@actionable` (the skill still ships in the gem for a
|
|
654
|
+
no-marketplace copy). Mirrors the field_struct agent-harness setup.
|
|
655
|
+
|
|
656
|
+
## [1.2.1] - 2026-06-03
|
|
657
|
+
|
|
658
|
+
### Tooling
|
|
659
|
+
|
|
660
|
+
- The `rake docs:check` commit guard (decision D19) now detects a `git commit`
|
|
661
|
+
invocation from the command itself rather than a prefix gate, so a compound
|
|
662
|
+
`git add -A && git commit …` can no longer slip past the pre-commit hook. No
|
|
663
|
+
library changes.
|
|
664
|
+
|
|
665
|
+
## [1.2.0] - 2026-06-03
|
|
666
|
+
|
|
667
|
+
Phase-2 ergonomics and adapters (decision D19), from real-world use fitting
|
|
668
|
+
Actionable into a polymorphic ingestion endpoint. All additive and
|
|
669
|
+
backward-compatible.
|
|
670
|
+
|
|
671
|
+
### Added
|
|
672
|
+
|
|
673
|
+
- **`fail_with(source, code: :invalid, message: nil)` / `fail_with!`** — record a
|
|
674
|
+
`Failure` that absorbs an arbitrary FieldStruct's validation errors mid-step
|
|
675
|
+
(the mechanism `:invalid_input` / `:invalid_output` use internally, now exposed
|
|
676
|
+
as a verb via the shared `Failure#absorb_errors_from`).
|
|
677
|
+
- **`Result#to_h`, `#status`, `#to_api_h(index:, id_field:)`** — plain-Hash and
|
|
678
|
+
HTTP/batch-element views of a result, so controllers don't hand-map the shape.
|
|
679
|
+
- **`Action.run_each(enumerable) { |item| run_args }`** — run the action once per
|
|
680
|
+
item, collecting a `BatchResult` (Enumerable) with `all_ok?` / `any_failure?` /
|
|
681
|
+
`partial?`, `successes` / `failures` / `skips`, and `to_api_h`. Items run
|
|
682
|
+
independently.
|
|
683
|
+
- **`input_for(:discriminator) { on value, Shape; default Shape }`** — pick the
|
|
684
|
+
input schema at run time from a discriminator value (typed polymorphic input);
|
|
685
|
+
an unmatched value with no `default` is `Failure(:invalid_input)`. Backed by the
|
|
686
|
+
shared `Actionable::ValueMatch` (also used by `case_step`).
|
|
687
|
+
- **`require 'actionable/job'`** — a framework-agnostic background-job helper:
|
|
688
|
+
`Actionable::Job.disposition(result)` maps a result to `:ack` / `:retry` /
|
|
689
|
+
`:discard`, and `Actionable::Job::Mixin` (included into a Sidekiq worker or
|
|
690
|
+
ActiveJob job) runs an action in `perform` and raises `RetryableFailure` for a
|
|
691
|
+
retryable failure.
|
|
692
|
+
|
|
693
|
+
### Changed
|
|
694
|
+
|
|
695
|
+
- **`skip` / `skip!` accept `**output` kwargs** (symmetric with `succeed`),
|
|
696
|
+
captured best-effort and never validated — so an idempotent hit can return the
|
|
697
|
+
existing record's data.
|
|
698
|
+
- **`measure :sampled, rate:`** — a third measurement mode recording history on a
|
|
699
|
+
fraction of runs (rate between 0 and 1) for low-overhead production
|
|
700
|
+
observability, alongside `:all` and `:none`.
|
|
701
|
+
|
|
702
|
+
## [1.1.0] - 2026-06-03
|
|
703
|
+
|
|
704
|
+
Developer-experience improvements (decision D18): fail loudly on misconfiguration
|
|
705
|
+
and make actions self-describing.
|
|
706
|
+
|
|
707
|
+
### Added
|
|
708
|
+
|
|
709
|
+
- `Actionable::Action.describe` — a structured summary (name, input/output field
|
|
710
|
+
metadata, ordered steps with type, hooks, measure mode, transaction config) so
|
|
711
|
+
humans and agents can understand an action without reading its source.
|
|
712
|
+
- `Actionable::DefinitionError` for misconfigured actions.
|
|
713
|
+
|
|
714
|
+
### Changed
|
|
715
|
+
|
|
716
|
+
- A reserved `output` field name — one that shadows a result attribute
|
|
717
|
+
(`code`/`message`/`output`/`history`/`errors`) or a reserved instance variable
|
|
718
|
+
(`result`/`input`) — now raises `DefinitionError` when declared, instead of
|
|
719
|
+
silently returning `nil` or failing deep in the runner.
|
|
720
|
+
- A declared step whose method isn't implemented now raises a clear
|
|
721
|
+
`DefinitionError` (naming the action and method) at run start, instead of a
|
|
722
|
+
cryptic `NoMethodError`.
|
|
723
|
+
|
|
724
|
+
## [1.0.0] - 2026-06-03
|
|
725
|
+
|
|
726
|
+
First feature-complete cut of the clean-slate rebuild.
|
|
727
|
+
|
|
728
|
+
### Added
|
|
729
|
+
|
|
730
|
+
- **Core action model** — `Actionable::Action` with an ordered, inherited `step`
|
|
731
|
+
DSL and an `Actionable::Runner` that runs steps inside `catch(:actionable_halt)`,
|
|
732
|
+
auto-succeeding when no result was recorded.
|
|
733
|
+
- **Result value objects** — FieldStruct-backed `Success`, `Failure`, and
|
|
734
|
+
`Skipped`, with `success?`/`failure?`/`skipped?`/`ok?` predicates, structured
|
|
735
|
+
`errors`, and a compact, deterministic `inspect`.
|
|
736
|
+
- **Control flow** — `succeed` / `fail` / `skip` (record, continue) and
|
|
737
|
+
`succeed!` / `fail!` / `skip!` / `halt!` (record + halt) via `throw`/`catch`;
|
|
738
|
+
genuine exceptions propagate (no `rescue`).
|
|
739
|
+
- **The `:skip` outcome** (decision D17) — a third, first-class result for "nothing
|
|
740
|
+
to do": not a success, not a failure; `on_skip` hook; commits under
|
|
741
|
+
`transactional`; best-effort, unvalidated output.
|
|
742
|
+
- **Typed output** — `output do … end` schema; matching ivars captured and coerced,
|
|
743
|
+
`succeed` kwargs overlaid; only a strict success is validated (else
|
|
744
|
+
`Failure(:invalid_output)`); declared fields delegate off the result.
|
|
745
|
+
- **Typed input** — `input do … end` schema; `.run` coerces and validates kwargs,
|
|
746
|
+
exposes `input`, and short-circuits to `Failure(:invalid_input)` on bad input.
|
|
747
|
+
- **Step types** — method steps, nested action steps (`:input` / `:expose`,
|
|
748
|
+
failure propagation), and `case_step` branching (`==`, `Regexp`, Array
|
|
749
|
+
membership) with `:if` / `:unless` guards.
|
|
750
|
+
- **Lifecycle hooks** — `on_success` / `on_failure` / `on_skip` / `always`.
|
|
751
|
+
- **History & measurement** — `measure :all` records per-step section/name/timing/
|
|
752
|
+
code and nested history, cascading into nested actions; Oj serialization.
|
|
753
|
+
- **Registry** — `Actionable.registry` of all named actions.
|
|
754
|
+
- **Optional Rails adapter** (`require 'actionable/rails'`) — `transactional`
|
|
755
|
+
macro (rollback on failure or exception, commit on success/skip) and
|
|
756
|
+
`Actionable::ProxyValidator`.
|
|
757
|
+
- **Optional RSpec integration** (`require 'actionable/rspec'`) — the
|
|
758
|
+
`perform_actionable` matcher (`and_succeed` / `and_fail` / `and_skip` /
|
|
759
|
+
`and_raise` + block) and `allow_/stub_actionable_*` helpers.
|
|
760
|
+
- **Types & docs** — Sord/RBS/YARD toolchain (`sig/actionable.rbs`,
|
|
761
|
+
`rake sigs:* / docs:* / release:check`), `Actionable::RBS.generate` for user
|
|
762
|
+
actions, and doctested README/USAGE examples.
|
|
763
|
+
|
|
764
|
+
## [0.1.0] - 2026-06-02
|
|
765
|
+
|
|
766
|
+
- Initial release
|