acta 0.2.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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.tool-versions +1 -0
  3. data/CHANGELOG.md +210 -0
  4. data/LICENSE +21 -0
  5. data/PLAN.md +158 -0
  6. data/README.md +559 -0
  7. data/Rakefile +12 -0
  8. data/app/controllers/acta/web/application_controller.rb +10 -0
  9. data/app/controllers/acta/web/events_controller.rb +37 -0
  10. data/app/helpers/acta/web/application_helper.rb +106 -0
  11. data/app/views/acta/web/events/index.html.erb +312 -0
  12. data/app/views/acta/web/events/show.html.erb +72 -0
  13. data/app/views/layouts/acta/web/application.html.erb +594 -0
  14. data/config/routes.rb +4 -0
  15. data/lib/acta/actor.rb +34 -0
  16. data/lib/acta/adapters/base.rb +59 -0
  17. data/lib/acta/adapters/postgres.rb +73 -0
  18. data/lib/acta/adapters/sqlite.rb +58 -0
  19. data/lib/acta/adapters.rb +19 -0
  20. data/lib/acta/array_type.rb +30 -0
  21. data/lib/acta/command.rb +48 -0
  22. data/lib/acta/current.rb +10 -0
  23. data/lib/acta/errors.rb +102 -0
  24. data/lib/acta/event.rb +80 -0
  25. data/lib/acta/events_query.rb +73 -0
  26. data/lib/acta/handler.rb +9 -0
  27. data/lib/acta/model.rb +58 -0
  28. data/lib/acta/model_type.rb +32 -0
  29. data/lib/acta/projection.rb +64 -0
  30. data/lib/acta/projection_managed.rb +108 -0
  31. data/lib/acta/railtie.rb +65 -0
  32. data/lib/acta/reactor.rb +15 -0
  33. data/lib/acta/reactor_job.rb +19 -0
  34. data/lib/acta/record.rb +10 -0
  35. data/lib/acta/schema.rb +12 -0
  36. data/lib/acta/serializable.rb +48 -0
  37. data/lib/acta/testing/dsl.rb +90 -0
  38. data/lib/acta/testing/matchers.rb +77 -0
  39. data/lib/acta/testing.rb +50 -0
  40. data/lib/acta/types/encrypted_string.rb +63 -0
  41. data/lib/acta/version.rb +5 -0
  42. data/lib/acta/web/engine.rb +13 -0
  43. data/lib/acta/web/events_query.rb +81 -0
  44. data/lib/acta/web.rb +45 -0
  45. data/lib/acta.rb +296 -0
  46. data/lib/generators/acta/install/install_generator.rb +23 -0
  47. data/lib/generators/acta/install/templates/create_acta_events.rb.tt +9 -0
  48. data/sig/acta.rbs +4 -0
  49. metadata +152 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 01a8eb1c4e0fb04d54ed2c8302c45f0d38064f07839d3b030a771ce70d03d27c
4
+ data.tar.gz: b34f32a1dc253ae7402d6c7a67f17c2a650d369b9e2d6036979f94da2e4b8820
5
+ SHA512:
6
+ metadata.gz: bf7c4fb886f607687bc5a274627f4c0580dc097b4096f5fb5d36f34d91dd6f05a0a2ea69e49a132b13b896ce8b61f0f6729792577543d0132d00565beacd0528
7
+ data.tar.gz: 157f9f789ad5d6387a6a71ee0c97754ec69f73a09b638cb7c20f6b68c6836637359af024b1765ffd05236c9908d0f83df6ac450b475827fdc7da7eaed0beef1f
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.4.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,210 @@
1
+ # Changelog
2
+
3
+ All notable changes to Acta are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ Public API stability begins at v1.0.0. Versions prior to that may make
9
+ breaking changes as the API settles through real-world consumer integration.
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [0.2.0] — 2026-04-27
14
+
15
+ ### Added
16
+
17
+ - `Acta::Projection.truncates(*ar_classes)` — class macro for declaring
18
+ the AR classes a projection owns. Used both as the default `truncate!`
19
+ target list (`delete_all` on each in declared order) and as input to
20
+ `Acta.rebuild!`'s cross-projection ordering: projections whose tables
21
+ are FK-referenced by another projection's tables now run first, so
22
+ children are deleted before their parents — independent of registration
23
+ order. Cycles raise `Acta::TruncateOrderError`. Projections without
24
+ `truncates` declarations keep their existing registration-order
25
+ behavior. The truncate phase runs inside `Projection.applying!`, so
26
+ `acta_managed!` models truncate cleanly. Closes #3.
27
+
28
+ - `acta_managed!` AR class macro — opt-in safety net for projection-owned
29
+ models. Once an AR model becomes a projection, writes from anywhere
30
+ other than the projection bypass the event log and break
31
+ `Acta.rebuild!` determinism. `acta_managed!` gates every AR write path
32
+ (save / update / destroy / update_columns / update_all / delete_all /
33
+ insert_all / upsert_all) on `Acta::Projection.applying?` and raises
34
+ `Acta::ProjectionWriteError` (or warns, with `on_violation: :warn`)
35
+ when violated. `Acta::Projection.applying! { … }` is the public escape
36
+ hatch for fixtures, migrations, and intentional backfills. Closes #6.
37
+
38
+ - `Acta::Testing.default_actor!(config, **attrs)` — RSpec configuration
39
+ helper that sets `Acta::Current.actor` before every example and resets
40
+ it after, eliminating the per-spec boilerplate and the easy-to-forget
41
+ `Acta::MissingActor` errors that come with it. Defaults to a
42
+ `system / rspec / test` actor; override any attribute. Closes #8.
43
+ - `Acta::Testing::DSL#with_actor(**attrs) { … }` — block-scoped actor
44
+ override for individual examples that need to attribute emissions to
45
+ a specific user. Restores the previous actor when the block returns
46
+ (or raises).
47
+
48
+ - `Acta::Railtie` — auto-loads projection / handler / reactor classes at boot
49
+ so they self-register before the first emit, even in Rails dev mode where
50
+ Zeitwerk would otherwise lazy-load them on first reference. Without this,
51
+ a projection that nothing has touched yet stays unsubscribed: the emit
52
+ succeeds, the event row is written, and the projection silently never runs.
53
+ Configurable via `config.acta.{projection,handler,reactor}_paths`; defaults
54
+ to `app/projections`, `app/handlers`, `app/reactors`. Set a path list to
55
+ `[]` to opt out. Closes #7.
56
+
57
+ ### Changed
58
+
59
+ - **Breaking: command DSL collapses around streams, concurrency, and
60
+ emit declarations.**
61
+ - Removed `Acta::Command.stream` macro. Commands no longer declare or
62
+ inherit stream identity — events are the only thing that carries
63
+ stream config.
64
+ - Removed `Acta::Command.on_concurrent_write` macro and the
65
+ capture-at-instantiation / assert-at-emit machinery on Command
66
+ instances.
67
+ - Removed `Acta::Command.emits` macro and `emitted_event_class(es)`.
68
+ The framework no longer asks commands to declare what they emit;
69
+ `def call` is the only source of truth. The "primary event" concept
70
+ that came with single-arg `emits` was a fiction once commands could
71
+ legitimately emit zero, one, or many events.
72
+ - `Acta::Command.call` now returns the command instance (was: the
73
+ return value of the user's `#call` method). Read events back via
74
+ `cmd.emitted_events` — an array of every event emitted during the
75
+ invocation, in order. Idempotent commands return an instance with
76
+ an empty array.
77
+ - Renamed `Acta.emit(event, expected_sequence: N)` keyword to
78
+ `if_version: N`.
79
+ - Renamed `Acta::ConcurrencyConflict` → `Acta::VersionConflict`. Its
80
+ `expected_sequence` / `actual_sequence` readers are now
81
+ `expected_version` / `actual_version`.
82
+
83
+ `Acta::Command` now has four moving parts: `param`, `validates`,
84
+ `call`, `emit`. Apps that need optimistic locking write it explicitly
85
+ using the new public primitive:
86
+ ```ruby
87
+ version = Acta.version_of(stream_type: :order, stream_key: order_id)
88
+ emit OrderRenamed.new(...), if_version: version
89
+ ```
90
+ Two lines, fully visible, no macro magic. Most commands need none of
91
+ this and lose nothing.
92
+
93
+ - `Acta.register_projection` is now idempotent — registering the same
94
+ projection class twice is a no-op instead of double-dispatching events.
95
+
96
+ ### Added
97
+
98
+ - `Acta.version_of(stream_type:, stream_key:)` — public class method
99
+ returning the current high-water mark for a stream (0 for fresh
100
+ streams). Pair with `Acta.emit(..., if_version:)` for optimistic
101
+ locking.
102
+
103
+ - Per-attribute payload encryption via `attribute :token, :encrypted_string`.
104
+ Backed by `ActiveRecord::Encryption` — same primary/deterministic/derivation
105
+ keys as Rails AR-encrypted columns, same key-rotation model (append a new
106
+ primary, keep old keys for decryption). In-memory event values stay
107
+ plaintext (`event.token` returns the secret); only the serialized payload
108
+ written to `events.payload` is ciphertext. Resolves the issue where events
109
+ carrying OAuth tokens / API keys would defeat AR encryption on the
110
+ projection's columns by leaving cleartext copies in the audit log. Closes #1.
111
+ - `Acta::Event.from_acta_record(envelope:, payload:)` — internal hydration
112
+ hook that routes payload values through `type.deserialize` before
113
+ construction. Used by `EventsQuery` to decrypt `:encrypted_string`
114
+ attributes on read; existing types are unaffected.
115
+ - Acta::Web masks encrypted payload leaves as `********` in both the
116
+ row preview and the pretty-JSON detail block. Detection is
117
+ envelope-based (`ActiveRecord::Encryption.encryptor.encrypted?`), so
118
+ any AR-encrypted ciphertext in the payload is masked regardless of
119
+ whether the event class declares `:encrypted_string` — including
120
+ historical events written before the attribute was opted in.
121
+
122
+ ## [0.1.1]
123
+
124
+ ### Added
125
+
126
+ - `Acta::Command` — new `emits EventClass` class-method DSL. The command
127
+ inherits `stream_type` and `stream_key_attribute` from the declared
128
+ event class, eliminating the duplicate `stream :order, key: :order_id`
129
+ declaration in the common case where a command emits a single event
130
+ for its aggregate. Explicit `stream` on the command still works and
131
+ takes precedence when both are given (useful when the command operates
132
+ on a different aggregate than its emitted event, or doesn't emit an
133
+ Acta event at all).
134
+
135
+ ## [0.1.0]
136
+
137
+ Feature-complete per the initial implementation plan (M0–M10). Next step
138
+ is real-world consumer integration to validate the API before cutting
139
+ v1.0.0.
140
+
141
+ ### Core primitives
142
+
143
+ - `Acta::Event` — ActiveModel-backed event classes with typed payloads,
144
+ validate-on-init, uuid / occurred_at / recorded_at / actor envelope.
145
+ - `Acta::Handler` — base primitive with the `on EventClass` DSL and
146
+ auto-registration via Rails eager loading.
147
+ - `Acta::Projection < Acta::Handler` — sync + transactional + replayable.
148
+ Raises `ProjectionError` on failure, rolling back the emit. Tracks
149
+ subclasses for `Acta.rebuild!`.
150
+ - `Acta::Reactor < Acta::Handler` — after-commit + async via ActiveJob
151
+ (default) or `sync!` opt-in. Skipped during replay.
152
+ - `Acta::Command < Acta::Model` — param validation, `stream` declaration,
153
+ `on_concurrent_write :raise` / `:ignore` optimistic-concurrency DSL.
154
+ Raises `InvalidCommand` on param validation failure.
155
+ - `Acta::Actor` value object — type, id, source, metadata.
156
+ - `Acta::Current` — `ActiveSupport::CurrentAttributes` with an `actor`
157
+ attribute, propagates through ActiveJob.
158
+
159
+ ### Payload shape
160
+
161
+ - `Acta::Model` base class — `ActiveModel::Attributes` + `ActiveModel::Model`
162
+ + JSON round-trip with schema-drift tolerance.
163
+ - Class-typed attributes: `attribute :location, GeoPoint` wraps the class
164
+ in `Acta::ModelType` automatically.
165
+ - Array attributes: `attribute :tags, array_of: Tag` wraps the element
166
+ type in `Acta::ArrayType`.
167
+ - `Acta::Serializable` concern — opt-in for AR classes to participate as
168
+ payload types with `acta_serialize only:` / `except:` control.
169
+ - Nested models and AR classes compose; arrays of either work.
170
+
171
+ ### Storage
172
+
173
+ - Single events table with identity, stream, payload (JSON/jsonb), actor,
174
+ source, metadata, and dual time columns (`occurred_at` + `recorded_at`).
175
+ - Indexes: uuid unique, stream-identity partial unique, event_type,
176
+ actor, source, occurred_at.
177
+ - `rails g acta:install` generator for the migration.
178
+ - Adapter seam: `Acta::Adapters::SQLite` (default) and
179
+ `Acta::Adapters::Postgres`.
180
+ - SQLite: single-writer sequencing with unique-constraint backstop.
181
+ - Postgres: `pg_advisory_xact_lock(hashtext(...))` per stream; `uuid` and
182
+ `jsonb` native column types.
183
+
184
+ ### Testing
185
+
186
+ - `Acta::Testing.test_mode { }` — inline reactors for the block.
187
+ - RSpec matchers: `emit(EventClass).with(attrs)`, `emit_events([...])`,
188
+ `emit_any_events`.
189
+ - `Acta::Testing::DSL` — given_events / when_command / when_event /
190
+ then_emitted / then_emitted_nothing_else.
191
+ - `ensure_replay_deterministic { snapshot }` — catches Time.current,
192
+ rand, and other non-deterministic projection patterns.
193
+
194
+ ### Observability
195
+
196
+ - ActiveSupport::Notifications:
197
+ - `acta.event_emitted` — `{ event, event_type }`
198
+ - `acta.projection_applied` — `{ event, projection_class }`
199
+ - `acta.reactor_invoked` — `{ event, reactor_class, sync: true }`
200
+ - `acta.reactor_enqueued` — `{ event, reactor_class }`
201
+
202
+ ### Errors
203
+
204
+ - `Acta::Error` (StandardError)
205
+ - `InvalidEvent` (carries event)
206
+ - `InvalidCommand < CommandError` (carries command)
207
+ - `ConcurrencyConflict` (stream identity, expected/actual sequence)
208
+ - `ProjectionError` (event, projection_class, original)
209
+ - `MissingActor`, `ConfigurationError`, `AdapterError`
210
+ - `UnknownEventType`, `ReplayError`
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom Gladhill
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/PLAN.md ADDED
@@ -0,0 +1,158 @@
1
+ # Acta Implementation Plan
2
+
3
+ Companion to the design doc (private, at `~/Sites/Journal/ideas/event_source_rails.md`).
4
+ This file is version-controlled alongside the code and tracks the milestone
5
+ breakdown for reaching v1.0.
6
+
7
+ ## Conventions
8
+
9
+ - **Ruby hash shorthand** (Ruby 3.1+): `{ name:, age: }` when variables in
10
+ scope match keys. Applied everywhere.
11
+ - **RSpec** exclusively for v1. Minitest matcher support considered post-v1.
12
+ - **TDD**: every milestone is a sequence of small red → green → refactor
13
+ cycles. One behaviour per commit where practical.
14
+ - **Rubocop**: `rubocop-rails-omakase` style. Clean before each commit.
15
+ - **Commits**: atomic, imperative mood, one logical change each.
16
+ - **Semver** from v0.1. Public API stability begins at v1.0.
17
+
18
+ ## Environment
19
+
20
+ - Ruby: 3.4+
21
+ - Rails floor: 8.1+
22
+ - Local: `~/Sites/acta`
23
+ - Remote: `git@github.com:whoojemaflip/acta.git`
24
+
25
+ ## Milestone breakdown
26
+
27
+ Each milestone is independently shippable.
28
+
29
+ ### M0 — Scaffolding ✅
30
+
31
+ Gem skeleton, RSpec, rubocop-rails-omakase, CI, README, LICENSE, CHANGELOG,
32
+ `PLAN.md` in repo. Baseline green build.
33
+
34
+ ### M1 — First emit (the round-trip milestone) ✅
35
+
36
+ **Goal:** `Acta.emit(event)` persists a row; `Acta.events.last` reads it back.
37
+
38
+ 1. Adapter seam — spec `Acta::Adapters::Base` interface; SQLite adapter stub.
39
+ 2. Migration generator — `rails g acta:install` creates the events table.
40
+ 3. `Acta::Model` — AM::Attributes + AM::Model + `to_acta_hash` / `from_acta_hash`
41
+ + `validate!` in initialize raising `Acta::InvalidEvent`.
42
+ 4. `Acta::Event < Acta::Model` — adds `uuid`, `event_type`, `event_version`,
43
+ `occurred_at`, `recorded_at`, `actor`.
44
+ 5. `Acta::Actor` value object — `type`, `id`, `source`, `metadata`.
45
+ 6. `Acta::Current` — CurrentAttributes with `actor`.
46
+ 7. `Acta.configure` — connection + single-store `:default` registration
47
+ (latent store concept).
48
+ 8. `Acta.emit(event)` — strict on missing actor (`Acta::MissingActor`);
49
+ persists via adapter; returns the persisted event.
50
+ 9. `Acta.events` — query API returning `Acta::Event` instances from the log.
51
+ 10. Error leaves so far: `Error`, `InvalidEvent`, `MissingActor`,
52
+ `ConfigurationError`, `AdapterError`.
53
+
54
+ **Checkpoint:** end-to-end spec that configures, emits, queries, asserts.
55
+
56
+ ### M2 — Streams & concurrency ✅
57
+
58
+ 1. Stream DSL — `stream :order, key: :order_id` on event classes.
59
+ 2. Sequence calculation in SQLite adapter (BEGIN IMMEDIATE + SELECT MAX).
60
+ 3. `ConcurrencyConflict` on unique-index violation.
61
+ 4. Stream-scoped query — `Acta.events.for_stream(type:, key:)`.
62
+ 5. `on_concurrent_write :raise` machinery (wires into M6 commands).
63
+
64
+ ### M3 — Handlers & dispatch ✅
65
+
66
+ 1. `Acta::Handler` base class + `on EventClass do |event| ... end` DSL.
67
+ 2. Auto-registration via inheritance + Rails `eager_load_paths`.
68
+ 3. Dispatch on emit (sync base handlers).
69
+ 4. Registry isolation for specs — `Acta.reset_handlers!`.
70
+
71
+ ### M4 — Projections ✅
72
+
73
+ 1. `Acta::Projection < Acta::Handler` with sync+transactional contract.
74
+ 2. Projections run inside emit transaction.
75
+ 3. `ProjectionError` wraps underlying exception + projection class.
76
+ 4. `Acta.rebuild!` — truncate projections, replay log, re-run projections.
77
+ 5. Replay skips reactors (prep for M5).
78
+
79
+ ### M5 — Reactors ✅
80
+
81
+ 1. `Acta::Reactor < Acta::Handler` with after-commit + ActiveJob default.
82
+ 2. `Acta::ReactorJob` — loads event by uuid, dispatches to reactor class.
83
+ 3. `sync true` opt-in.
84
+ 4. Skip on replay.
85
+ 5. Actor propagation via `Acta::Current` serialized into ActiveJob.
86
+
87
+ ### M6 — Commands ✅
88
+
89
+ 1. `Acta::Command < Acta::Model` — param validation via AM::Attributes.
90
+ 2. `stream :order, key: :order_id` on command — declares aggregate identity.
91
+ 3. `on_concurrent_write :raise` — captures stream sequence at instantiation.
92
+ 4. `.call(**params)` entry; `emit event` as instance method;
93
+ `InvalidCommand` on validation failure.
94
+ 5. Auto-loading from `app/commands/`.
95
+
96
+ ### M7 — Testing DSL (`Acta::Testing`) ✅
97
+
98
+ 1. RSpec matchers: `emit(EventClass).with(...)`, `emit_events([...])`,
99
+ `not_to emit_any_events`.
100
+ 2. `given_events { ... }` — seeds the log directly without running reactors.
101
+ 3. `when_command(cmd)` — runs command, captures emitted events.
102
+ 4. `then_emitted(EventClass, **attrs)` / `then_emitted_nothing_else`.
103
+ 5. `Acta.test_mode { ... }` — inline reactors for the block.
104
+ 6. Replay determinism helper —
105
+ `expect_projections_deterministic { ... }`.
106
+
107
+ ### M8 — `Acta::Serializable` (AR piggyback) ✅
108
+
109
+ 1. Concern adding `to_acta_hash` / `self.from_acta_hash(hash)` on AR classes.
110
+ 2. `acta_serialize only: / except:` configuration.
111
+ 3. Type dispatch for AR classes in event attributes.
112
+ 4. Arrays of AR (`array_of:`) support.
113
+ 5. Nested AR-in-AR round-trip.
114
+ 6. STI support via `type` column capture.
115
+ 7. Schema-drift tolerance — filter unknown keys on deserialize.
116
+
117
+ ### M9 — Postgres adapter ✅
118
+
119
+ 1. `Acta::Adapters::Postgres` implementation.
120
+ 2. Advisory locks (`pg_advisory_xact_lock(hashtext(...))`) per stream.
121
+ 3. `jsonb` column type + `uuid` column type + `gen_random_uuid()`.
122
+ 4. Shared behaviour specs — `it_behaves_like "an Acta adapter"`.
123
+ 5. CI matrix with both SQLite and Postgres.
124
+ 6. Concurrency-specific specs exercising genuine concurrent writers.
125
+
126
+ ### M10 — v1.0 polish ✅
127
+
128
+ 1. Observability via `ActiveSupport::Notifications` —
129
+ `acta.event_emitted`, `acta.projection_applied`, `acta.reactor_enqueued`.
130
+ 2. Remaining error leaves — `UnknownEventType`, `ReplayError`, gaps.
131
+ 3. README rewrite with full worked examples.
132
+ 4. Tag v1.0.0.
133
+
134
+ ## Milestone dependencies
135
+
136
+ ```
137
+ M0 → M1 → M2 → M3 → M4 ─┐
138
+ └──→ M5 ─┐
139
+ M6 ──────┴─→ M7 → M8 → M9 → M10
140
+ ```
141
+
142
+ M4 and M5 are independent after M3. M6 can start after M2. M8 benefits from
143
+ M7. M9 can start anytime after M1 but is most valuable after M8.
144
+
145
+ ## Out of scope for v1
146
+
147
+ - Upcasters (column reserved)
148
+ - Multi-store (latent concept, not exposed)
149
+ - MySQL adapter
150
+ - `Acta::Saga` / process managers
151
+ - LISTEN/NOTIFY or other pub/sub transport
152
+ - Snapshots
153
+
154
+ ## Quality gates per commit
155
+
156
+ - `bundle exec rspec` green
157
+ - `bundle exec rubocop` clean
158
+ - Change has a spec unless it's pure refactoring