event_engine 0.1.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/CHANGELOG.md +53 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +517 -0
  5. data/Rakefile +12 -0
  6. data/app/assets/config/event_engine_manifest.js +1 -0
  7. data/app/assets/stylesheets/event_engine/application.css +15 -0
  8. data/app/controllers/event_engine/application_controller.rb +4 -0
  9. data/app/helpers/event_engine/application_helper.rb +4 -0
  10. data/app/jobs/event_engine/application_job.rb +4 -0
  11. data/app/mailers/event_engine/application_mailer.rb +6 -0
  12. data/app/models/event_engine/application_record.rb +5 -0
  13. data/app/views/layouts/event_engine/application.html.erb +15 -0
  14. data/config/routes.rb +2 -0
  15. data/lib/event_engine/configuration.rb +20 -0
  16. data/lib/event_engine/definition_loader.rb +26 -0
  17. data/lib/event_engine/dsl_compiler.rb +50 -0
  18. data/lib/event_engine/engine.rb +56 -0
  19. data/lib/event_engine/event.rb +32 -0
  20. data/lib/event_engine/event_builder.rb +45 -0
  21. data/lib/event_engine/event_definition/inputs.rb +43 -0
  22. data/lib/event_engine/event_definition/payloads.rb +47 -0
  23. data/lib/event_engine/event_definition/schemas.rb +158 -0
  24. data/lib/event_engine/event_definition/validation.rb +18 -0
  25. data/lib/event_engine/event_definition.rb +76 -0
  26. data/lib/event_engine/event_schema.rb +99 -0
  27. data/lib/event_engine/event_schema_dumper.rb +13 -0
  28. data/lib/event_engine/event_schema_loader.rb +37 -0
  29. data/lib/event_engine/event_schema_merger.rb +62 -0
  30. data/lib/event_engine/event_schema_writer.rb +47 -0
  31. data/lib/event_engine/handler_registry.rb +23 -0
  32. data/lib/event_engine/lifecycle_definition.rb +86 -0
  33. data/lib/event_engine/process_type.rb +26 -0
  34. data/lib/event_engine/railtie.rb +9 -0
  35. data/lib/event_engine/reference/guide.md +129 -0
  36. data/lib/event_engine/reference.rb +16 -0
  37. data/lib/event_engine/schema_catalog.rb +50 -0
  38. data/lib/event_engine/schema_compatibility.rb +50 -0
  39. data/lib/event_engine/schema_diff.rb +35 -0
  40. data/lib/event_engine/schema_drift_guard.rb +38 -0
  41. data/lib/event_engine/schema_registry.rb +122 -0
  42. data/lib/event_engine/subject_registry.rb +40 -0
  43. data/lib/event_engine/the_local/agents/event_engine-develop.md +142 -0
  44. data/lib/event_engine/the_local/agents/event_engine-info.md +140 -0
  45. data/lib/event_engine/the_local/agents/event_engine-install.md +140 -0
  46. data/lib/event_engine/the_local.rb +55 -0
  47. data/lib/event_engine/version.rb +3 -0
  48. data/lib/event_engine.rb +197 -0
  49. data/lib/generators/event_engine/install_generator.rb +31 -0
  50. data/lib/generators/event_engine/templates/event_schema.rb +10 -0
  51. data/lib/generators/event_engine/templates/initializer.rb +4 -0
  52. data/lib/tasks/event_engine_catalog.rake +13 -0
  53. data/lib/tasks/event_engine_schema.rake +82 -0
  54. data/lib/tasks/event_engine_schema_check.rake +20 -0
  55. data/lib/tasks/event_engine_tasks.rake +4 -0
  56. metadata +127 -0
@@ -0,0 +1,122 @@
1
+ module EventEngine
2
+ # In-memory registry holding all event schemas by name and version.
3
+ # Loaded once at boot from the compiled schema file.
4
+ #
5
+ # @example Look up a schema
6
+ # schema = registry.schema(:cow_fed)
7
+ # schema.event_version #=> 2
8
+ #
9
+ # @example Look up a specific version
10
+ # schema = registry.schema(:cow_fed, version: 1)
11
+ class SchemaRegistry
12
+ # Raised when looking up an event name or version that doesn't exist.
13
+ class UnknownEventError < StandardError; end
14
+
15
+ # Raised when the registry is accessed before loading or loaded twice.
16
+ class RegistryFrozenError < StandardError; end
17
+
18
+ # @param event_schema [EventSchema] initial schema (empty by default)
19
+ def initialize(event_schema = EventSchema.new)
20
+ @event_schema = event_schema
21
+ @loaded = false
22
+ end
23
+
24
+ # Registers a schema into the underlying EventSchema store.
25
+ #
26
+ # @param schema [EventDefinition::Schema] the schema to register
27
+ def register(schema)
28
+ @event_schema.register(schema)
29
+ end
30
+
31
+ # Returns all registered event names.
32
+ #
33
+ # @return [Array<Symbol>]
34
+ def events
35
+ @event_schema.events
36
+ end
37
+
38
+ # Returns all version numbers for a given event.
39
+ #
40
+ # @param event_name [Symbol]
41
+ # @return [Array<Integer>] sorted version numbers
42
+ def versions_for(event_name)
43
+ @event_schema.versions_for(event_name)
44
+ end
45
+
46
+ # Populates the registry from a loaded EventSchema. Can only be called once.
47
+ #
48
+ # @param schema [EventSchema] the compiled schema
49
+ # @return [self]
50
+ # @raise [RegistryFrozenError] if already loaded
51
+ def load_from_schema!(schema)
52
+ raise RegistryFrozenError, "EventRegistry already loaded" if loaded?
53
+ @event_schema = schema
54
+
55
+ @loaded = true
56
+ self
57
+ end
58
+
59
+ # Clears the registry, allowing it to be reloaded.
60
+ #
61
+ # @return [void]
62
+ def reset!
63
+ @event_schema = {}
64
+ @loaded = false
65
+ end
66
+
67
+ # Looks up a schema by event name and optional version.
68
+ # Returns the latest version when no version is specified.
69
+ #
70
+ # @param event_name [Symbol]
71
+ # @param version [Integer, nil] specific version, or nil for latest
72
+ # @return [EventDefinition::Schema]
73
+ # @raise [RegistryFrozenError] if registry is not loaded
74
+ # @raise [UnknownEventError] if event or version is not found
75
+ def schema(event_name, version: nil)
76
+ raise RegistryFrozenError, "EventRegistry not loaded" unless loaded?
77
+
78
+ schema =
79
+ if version
80
+ @event_schema.schema_for(event_name, version)
81
+ else
82
+ @event_schema.latest_for(event_name)
83
+ end
84
+
85
+ unless schema
86
+ raise UnknownEventError,
87
+ "Unknown #{version ? "version #{version} for " : ""}event: #{event_name}"
88
+ end
89
+
90
+ schema
91
+ end
92
+
93
+ # Returns the latest schema version for an event.
94
+ #
95
+ # @param event_name [Symbol]
96
+ # @return [EventDefinition::Schema, nil]
97
+ def latest_for(event_name)
98
+ @event_schema.latest_for(event_name)
99
+ end
100
+
101
+ # Returns the underlying EventSchema store.
102
+ #
103
+ # @return [EventSchema]
104
+ def event_schema
105
+ @event_schema
106
+ end
107
+
108
+ # Freezes the underlying schema, preventing further modifications.
109
+ #
110
+ # @return [void]
111
+ def finalize!
112
+ @event_schema.finalize!
113
+ end
114
+
115
+ # Whether the registry has been loaded with schemas.
116
+ #
117
+ # @return [Boolean]
118
+ def loaded?
119
+ @loaded == true
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,40 @@
1
+ module EventEngine
2
+ class SubjectRegistry
3
+ class UnknownSubjectError < StandardError; end
4
+
5
+ class Subject
6
+ attr_reader :name, :metadata
7
+
8
+ def initialize(name, **metadata)
9
+ @name = name
10
+ @metadata = metadata
11
+ end
12
+ end
13
+
14
+ def self.define(&block)
15
+ registry = new
16
+ registry.instance_eval(&block) if block
17
+ registry
18
+ end
19
+
20
+ def initialize
21
+ @subjects = {}
22
+ end
23
+
24
+ def subject(name, **metadata)
25
+ @subjects[name] = Subject.new(name, **metadata)
26
+ end
27
+
28
+ def [](name)
29
+ @subjects[name]
30
+ end
31
+
32
+ def registered?(name)
33
+ @subjects.key?(name)
34
+ end
35
+
36
+ def names
37
+ @subjects.keys
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: event_engine-develop
3
+ description: Use PROACTIVELY for any EventEngine work — defining events, choosing process_type, emitting, and keeping the committed schema in sync. MUST BE USED instead of hand-writing event plumbing.
4
+ tools: Read, Write, Edit, Grep
5
+ ---
6
+
7
+ You build EventEngine events following the reference's conventions: one
8
+ EventDefinition class per event in app/event_definitions/, payloads composed from
9
+ inputs, process_type set explicitly, emitted through the generated
10
+ EventEngine.<event_name> helpers. After any definition change you run
11
+ `bin/rails event_engine:schema:dump` and commit db/event_schema.rb, keeping
12
+ event_engine:schema_check green. You keep handlers idempotent.
13
+
14
+ ## EventEngine
15
+
16
+ > **DO NOT** explore the event_engine gem source code. This reference is the
17
+ > complete user-facing API, embedded verbatim into every event_engine local so
18
+ > their guidance never drifts. Keep it the single source of truth.
19
+
20
+ EventEngine is a Rails engine for defining domain events as declarative classes,
21
+ compiling them to a committed schema, emitting them through generated helpers, and
22
+ dispatching them to registered handlers. Core builds and routes events; it ships no
23
+ handlers of its own. Durable delivery, an event store, and ready-made subscriber
24
+ classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
25
+ `event_engine-subscribers`) — this reference covers core only.
26
+
27
+ ### What it offers
28
+
29
+ **Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
30
+
31
+ ```ruby
32
+ class CowFed < EventEngine::EventDefinition
33
+ event_name :cow_fed # the event's identity (required)
34
+ event_type :domain # classification, e.g. :domain (required)
35
+ process_type :durable # routing type (optional; set it explicitly)
36
+
37
+ input :cow # a required input
38
+ optional_input :farmer # an optional input
39
+
40
+ required_payload :weight, from: :cow, attr: :weight
41
+ optional_payload :farmer_name, from: :farmer, attr: :name
42
+ end
43
+ ```
44
+
45
+ | DSL method | Purpose |
46
+ |---|---|
47
+ | `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
48
+ | `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
49
+ | `process_type(:symbol)` | Routing type (optional). One of the six values below. |
50
+ | `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
51
+ | `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
52
+ | `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
53
+
54
+ Duplicate input names raise `ArgumentError`; payload `from:` must reference a
55
+ declared input.
56
+
57
+ **process_type** — core stamps this symbol onto every emitted event but does not act
58
+ on it. Which handlers receive an event is decided by each handler's `levels:`. The
59
+ values:
60
+
61
+ | value | intent |
62
+ |---|---|
63
+ | `:inline` | handled in-process, synchronously |
64
+ | `:background` | handled in-process, via a background job |
65
+ | `:durable` | handled when a durable outbox drains |
66
+ | `:broker` | published to an external transport |
67
+ | `:telemetry` | metrics / observability handlers |
68
+ | `:sourced` | an append-only event store |
69
+
70
+ The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
71
+ etc. their behavior; core just routes to whatever is registered. If `process_type`
72
+ is omitted it is `nil` — set it explicitly so routing intent is clear.
73
+
74
+ **Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
75
+
76
+ ```ruby
77
+ EventEngine.cow_fed(
78
+ cow: cow, farmer: farmer, # declared inputs, by name
79
+ occurred_at: Time.current, # optional, defaults to now
80
+ metadata: { request_id: "abc" }, # optional
81
+ idempotency_key: "…", # optional, defaults to a UUID
82
+ aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
83
+ event_version: 1 # optional, defaults to the latest schema version
84
+ )
85
+ ```
86
+
87
+ Missing a required input, or passing an unknown one, raises `ArgumentError`. The
88
+ event's `payload` is symbol-keyed.
89
+
90
+ **Register handlers** — a handler is any object responding to `call(event)`:
91
+
92
+ ```ruby
93
+ EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
94
+ EventEngine.dispatch(event) # fan an event out (emit helpers call this)
95
+ EventEngine.reset_handlers! # clear all handlers
96
+ ```
97
+
98
+ Handlers run synchronously in registration order; if one raises, the rest don't run.
99
+ Keep handlers idempotent.
100
+
101
+ **Configure** — `config/initializers/event_engine.rb`, logger only:
102
+
103
+ ```ruby
104
+ EventEngine.configure { |config| config.logger = Rails.logger }
105
+ ```
106
+
107
+ **Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
108
+ is authoritative at boot:
109
+
110
+ ```bash
111
+ bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
112
+ bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
113
+ ```
114
+
115
+ A new event is version 1; changing an event's identity or payload bumps its version.
116
+ Changing only `process_type` does not bump the version.
117
+
118
+ ### Install
119
+
120
+ 1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
121
+ 2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
122
+ `config/initializers/event_engine.rb`.
123
+ 3. Define events as classes in `app/event_definitions/`.
124
+ 4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
125
+ 5. Set `config.logger` in the initializer if you want something other than the default.
126
+
127
+ Durable delivery, an event store, and prebuilt subscriber classes are separate gems
128
+ (`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
129
+ when you need them and follow their own setup.
130
+
131
+ ### EventEngine conventions
132
+
133
+ - Define one `EventDefinition` class per event in `app/event_definitions/`; never
134
+ hand-build event hashes.
135
+ - Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
136
+ raw payload hashes to the emit helper.
137
+ - Always set `process_type` explicitly so routing intent is clear.
138
+ - Emit only through the generated `EventEngine.<event_name>` helpers, passing the
139
+ declared inputs.
140
+ - Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
141
+ definition change; keep `event_engine:schema_check` green in CI.
142
+ - Keep handlers and subscribers idempotent.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: event_engine-info
3
+ description: Use to learn what EventEngine offers — the event-definition DSL, process_type, emitting, handlers, and the schema workflow.
4
+ tools: Read
5
+ ---
6
+
7
+ You explain how EventEngine works and how to use it, answering only from the
8
+ reference: defining events, process_type routing, emitting through the generated
9
+ helpers, registering handlers, and the schema dump/check workflow. You make no
10
+ changes.
11
+
12
+ ## EventEngine
13
+
14
+ > **DO NOT** explore the event_engine gem source code. This reference is the
15
+ > complete user-facing API, embedded verbatim into every event_engine local so
16
+ > their guidance never drifts. Keep it the single source of truth.
17
+
18
+ EventEngine is a Rails engine for defining domain events as declarative classes,
19
+ compiling them to a committed schema, emitting them through generated helpers, and
20
+ dispatching them to registered handlers. Core builds and routes events; it ships no
21
+ handlers of its own. Durable delivery, an event store, and ready-made subscriber
22
+ classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
23
+ `event_engine-subscribers`) — this reference covers core only.
24
+
25
+ ### What it offers
26
+
27
+ **Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
28
+
29
+ ```ruby
30
+ class CowFed < EventEngine::EventDefinition
31
+ event_name :cow_fed # the event's identity (required)
32
+ event_type :domain # classification, e.g. :domain (required)
33
+ process_type :durable # routing type (optional; set it explicitly)
34
+
35
+ input :cow # a required input
36
+ optional_input :farmer # an optional input
37
+
38
+ required_payload :weight, from: :cow, attr: :weight
39
+ optional_payload :farmer_name, from: :farmer, attr: :name
40
+ end
41
+ ```
42
+
43
+ | DSL method | Purpose |
44
+ |---|---|
45
+ | `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
46
+ | `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
47
+ | `process_type(:symbol)` | Routing type (optional). One of the six values below. |
48
+ | `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
49
+ | `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
50
+ | `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
51
+
52
+ Duplicate input names raise `ArgumentError`; payload `from:` must reference a
53
+ declared input.
54
+
55
+ **process_type** — core stamps this symbol onto every emitted event but does not act
56
+ on it. Which handlers receive an event is decided by each handler's `levels:`. The
57
+ values:
58
+
59
+ | value | intent |
60
+ |---|---|
61
+ | `:inline` | handled in-process, synchronously |
62
+ | `:background` | handled in-process, via a background job |
63
+ | `:durable` | handled when a durable outbox drains |
64
+ | `:broker` | published to an external transport |
65
+ | `:telemetry` | metrics / observability handlers |
66
+ | `:sourced` | an append-only event store |
67
+
68
+ The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
69
+ etc. their behavior; core just routes to whatever is registered. If `process_type`
70
+ is omitted it is `nil` — set it explicitly so routing intent is clear.
71
+
72
+ **Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
73
+
74
+ ```ruby
75
+ EventEngine.cow_fed(
76
+ cow: cow, farmer: farmer, # declared inputs, by name
77
+ occurred_at: Time.current, # optional, defaults to now
78
+ metadata: { request_id: "abc" }, # optional
79
+ idempotency_key: "…", # optional, defaults to a UUID
80
+ aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
81
+ event_version: 1 # optional, defaults to the latest schema version
82
+ )
83
+ ```
84
+
85
+ Missing a required input, or passing an unknown one, raises `ArgumentError`. The
86
+ event's `payload` is symbol-keyed.
87
+
88
+ **Register handlers** — a handler is any object responding to `call(event)`:
89
+
90
+ ```ruby
91
+ EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
92
+ EventEngine.dispatch(event) # fan an event out (emit helpers call this)
93
+ EventEngine.reset_handlers! # clear all handlers
94
+ ```
95
+
96
+ Handlers run synchronously in registration order; if one raises, the rest don't run.
97
+ Keep handlers idempotent.
98
+
99
+ **Configure** — `config/initializers/event_engine.rb`, logger only:
100
+
101
+ ```ruby
102
+ EventEngine.configure { |config| config.logger = Rails.logger }
103
+ ```
104
+
105
+ **Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
106
+ is authoritative at boot:
107
+
108
+ ```bash
109
+ bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
110
+ bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
111
+ ```
112
+
113
+ A new event is version 1; changing an event's identity or payload bumps its version.
114
+ Changing only `process_type` does not bump the version.
115
+
116
+ ### Install
117
+
118
+ 1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
119
+ 2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
120
+ `config/initializers/event_engine.rb`.
121
+ 3. Define events as classes in `app/event_definitions/`.
122
+ 4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
123
+ 5. Set `config.logger` in the initializer if you want something other than the default.
124
+
125
+ Durable delivery, an event store, and prebuilt subscriber classes are separate gems
126
+ (`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
127
+ when you need them and follow their own setup.
128
+
129
+ ### EventEngine conventions
130
+
131
+ - Define one `EventDefinition` class per event in `app/event_definitions/`; never
132
+ hand-build event hashes.
133
+ - Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
134
+ raw payload hashes to the emit helper.
135
+ - Always set `process_type` explicitly so routing intent is clear.
136
+ - Emit only through the generated `EventEngine.<event_name>` helpers, passing the
137
+ declared inputs.
138
+ - Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
139
+ definition change; keep `event_engine:schema_check` green in CI.
140
+ - Keep handlers and subscribers idempotent.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: event_engine-install
3
+ description: Use to add EventEngine to a Rails app and set it up correctly.
4
+ tools: Bash, Read, Edit
5
+ ---
6
+
7
+ You install EventEngine following the reference's install section exactly: add the
8
+ gem, bundle, run `bin/rails g event_engine:install`, set the logger in the
9
+ initializer, then dump and commit db/event_schema.rb. You do not invent steps, and
10
+ you do not set up the separate delivery/store/subscribers gems unless asked.
11
+
12
+ ## EventEngine
13
+
14
+ > **DO NOT** explore the event_engine gem source code. This reference is the
15
+ > complete user-facing API, embedded verbatim into every event_engine local so
16
+ > their guidance never drifts. Keep it the single source of truth.
17
+
18
+ EventEngine is a Rails engine for defining domain events as declarative classes,
19
+ compiling them to a committed schema, emitting them through generated helpers, and
20
+ dispatching them to registered handlers. Core builds and routes events; it ships no
21
+ handlers of its own. Durable delivery, an event store, and ready-made subscriber
22
+ classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
23
+ `event_engine-subscribers`) — this reference covers core only.
24
+
25
+ ### What it offers
26
+
27
+ **Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
28
+
29
+ ```ruby
30
+ class CowFed < EventEngine::EventDefinition
31
+ event_name :cow_fed # the event's identity (required)
32
+ event_type :domain # classification, e.g. :domain (required)
33
+ process_type :durable # routing type (optional; set it explicitly)
34
+
35
+ input :cow # a required input
36
+ optional_input :farmer # an optional input
37
+
38
+ required_payload :weight, from: :cow, attr: :weight
39
+ optional_payload :farmer_name, from: :farmer, attr: :name
40
+ end
41
+ ```
42
+
43
+ | DSL method | Purpose |
44
+ |---|---|
45
+ | `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
46
+ | `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
47
+ | `process_type(:symbol)` | Routing type (optional). One of the six values below. |
48
+ | `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
49
+ | `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
50
+ | `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
51
+
52
+ Duplicate input names raise `ArgumentError`; payload `from:` must reference a
53
+ declared input.
54
+
55
+ **process_type** — core stamps this symbol onto every emitted event but does not act
56
+ on it. Which handlers receive an event is decided by each handler's `levels:`. The
57
+ values:
58
+
59
+ | value | intent |
60
+ |---|---|
61
+ | `:inline` | handled in-process, synchronously |
62
+ | `:background` | handled in-process, via a background job |
63
+ | `:durable` | handled when a durable outbox drains |
64
+ | `:broker` | published to an external transport |
65
+ | `:telemetry` | metrics / observability handlers |
66
+ | `:sourced` | an append-only event store |
67
+
68
+ The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
69
+ etc. their behavior; core just routes to whatever is registered. If `process_type`
70
+ is omitted it is `nil` — set it explicitly so routing intent is clear.
71
+
72
+ **Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
73
+
74
+ ```ruby
75
+ EventEngine.cow_fed(
76
+ cow: cow, farmer: farmer, # declared inputs, by name
77
+ occurred_at: Time.current, # optional, defaults to now
78
+ metadata: { request_id: "abc" }, # optional
79
+ idempotency_key: "…", # optional, defaults to a UUID
80
+ aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
81
+ event_version: 1 # optional, defaults to the latest schema version
82
+ )
83
+ ```
84
+
85
+ Missing a required input, or passing an unknown one, raises `ArgumentError`. The
86
+ event's `payload` is symbol-keyed.
87
+
88
+ **Register handlers** — a handler is any object responding to `call(event)`:
89
+
90
+ ```ruby
91
+ EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
92
+ EventEngine.dispatch(event) # fan an event out (emit helpers call this)
93
+ EventEngine.reset_handlers! # clear all handlers
94
+ ```
95
+
96
+ Handlers run synchronously in registration order; if one raises, the rest don't run.
97
+ Keep handlers idempotent.
98
+
99
+ **Configure** — `config/initializers/event_engine.rb`, logger only:
100
+
101
+ ```ruby
102
+ EventEngine.configure { |config| config.logger = Rails.logger }
103
+ ```
104
+
105
+ **Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
106
+ is authoritative at boot:
107
+
108
+ ```bash
109
+ bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
110
+ bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
111
+ ```
112
+
113
+ A new event is version 1; changing an event's identity or payload bumps its version.
114
+ Changing only `process_type` does not bump the version.
115
+
116
+ ### Install
117
+
118
+ 1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
119
+ 2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
120
+ `config/initializers/event_engine.rb`.
121
+ 3. Define events as classes in `app/event_definitions/`.
122
+ 4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
123
+ 5. Set `config.logger` in the initializer if you want something other than the default.
124
+
125
+ Durable delivery, an event store, and prebuilt subscriber classes are separate gems
126
+ (`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
127
+ when you need them and follow their own setup.
128
+
129
+ ### EventEngine conventions
130
+
131
+ - Define one `EventDefinition` class per event in `app/event_definitions/`; never
132
+ hand-build event hashes.
133
+ - Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
134
+ raw payload hashes to the emit helper.
135
+ - Always set `process_type` explicitly so routing intent is clear.
136
+ - Emit only through the generated `EventEngine.<event_name>` helpers, passing the
137
+ declared inputs.
138
+ - Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
139
+ definition change; keep `event_engine:schema_check` green in CI.
140
+ - Keep handlers and subscribers idempotent.
@@ -0,0 +1,55 @@
1
+ require "event_engine/reference"
2
+
3
+ module EventEngine
4
+ module Companion
5
+ def self.register!
6
+ TheLocal.register("event_engine",
7
+ scope: "events — defining EventEngine events, emitting, and the schema workflow",
8
+ agents_dir: File.expand_path("the_local/agents", __dir__)) do |c|
9
+ c.agent "info",
10
+ description: "Use to learn what EventEngine offers — the event-definition DSL, " \
11
+ "process_type, emitting, handlers, and the schema workflow.",
12
+ tools: "Read",
13
+ knowledge: Reference.content,
14
+ body: <<~BODY.chomp
15
+ You explain how EventEngine works and how to use it, answering only from the
16
+ reference: defining events, process_type routing, emitting through the generated
17
+ helpers, registering handlers, and the schema dump/check workflow. You make no
18
+ changes.
19
+ BODY
20
+
21
+ c.agent "install",
22
+ description: "Use to add EventEngine to a Rails app and set it up correctly.",
23
+ tools: "Bash, Read, Edit",
24
+ knowledge: Reference.content,
25
+ body: <<~BODY.chomp
26
+ You install EventEngine following the reference's install section exactly: add the
27
+ gem, bundle, run `bin/rails g event_engine:install`, set the logger in the
28
+ initializer, then dump and commit db/event_schema.rb. You do not invent steps, and
29
+ you do not set up the separate delivery/store/subscribers gems unless asked.
30
+ BODY
31
+
32
+ c.agent "develop",
33
+ description: "Use PROACTIVELY for any EventEngine work — defining events, choosing " \
34
+ "process_type, emitting, and keeping the committed schema in sync. MUST BE USED " \
35
+ "instead of hand-writing event plumbing.",
36
+ tools: "Read, Write, Edit, Grep",
37
+ knowledge: Reference.content,
38
+ body: <<~BODY.chomp
39
+ You build EventEngine events following the reference's conventions: one
40
+ EventDefinition class per event in app/event_definitions/, payloads composed from
41
+ inputs, process_type set explicitly, emitted through the generated
42
+ EventEngine.<event_name> helpers. After any definition change you run
43
+ `bin/rails event_engine:schema:dump` and commit db/event_schema.rb, keeping
44
+ event_engine:schema_check green. You keep handlers idempotent.
45
+ BODY
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ begin
52
+ require "the_local"
53
+ EventEngine::Companion.register!
54
+ rescue LoadError
55
+ end
@@ -0,0 +1,3 @@
1
+ module EventEngine
2
+ VERSION = "0.1.0"
3
+ end