ecs_on_rails 0.2.1 → 0.3.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +12 -0
  3. data/CHANGELOG.md +332 -0
  4. data/README.md +365 -50
  5. data/lib/ecs_rails/catalogue/address.rb +39 -0
  6. data/lib/ecs_rails/catalogue/calendar_date.rb +27 -0
  7. data/lib/ecs_rails/catalogue/counter.rb +46 -0
  8. data/lib/ecs_rails/catalogue/discard.rb +36 -0
  9. data/lib/ecs_rails/catalogue/email.rb +35 -0
  10. data/lib/ecs_rails/catalogue/geolocation.rb +53 -0
  11. data/lib/ecs_rails/catalogue/identifier.rb +23 -0
  12. data/lib/ecs_rails/catalogue/image.rb +28 -0
  13. data/lib/ecs_rails/catalogue/link.rb +27 -0
  14. data/lib/ecs_rails/catalogue/marker.rb +38 -0
  15. data/lib/ecs_rails/catalogue/money.rb +86 -0
  16. data/lib/ecs_rails/catalogue/name.rb +37 -0
  17. data/lib/ecs_rails/catalogue/password.rb +50 -0
  18. data/lib/ecs_rails/catalogue/period.rb +57 -0
  19. data/lib/ecs_rails/catalogue/phone.rb +30 -0
  20. data/lib/ecs_rails/catalogue/position.rb +18 -0
  21. data/lib/ecs_rails/catalogue/rating.rb +25 -0
  22. data/lib/ecs_rails/catalogue/relationship.rb +159 -0
  23. data/lib/ecs_rails/catalogue/role.rb +23 -0
  24. data/lib/ecs_rails/catalogue/schema_diff.rb +198 -0
  25. data/lib/ecs_rails/catalogue/search_vector.rb +38 -0
  26. data/lib/ecs_rails/catalogue/state.rb +71 -0
  27. data/lib/ecs_rails/catalogue/tags.rb +64 -0
  28. data/lib/ecs_rails/catalogue/text.rb +29 -0
  29. data/lib/ecs_rails/catalogue/timestamp.rb +34 -0
  30. data/lib/ecs_rails/catalogue/token.rb +77 -0
  31. data/lib/ecs_rails/catalogue.rb +405 -0
  32. data/lib/ecs_rails/component.rb +13 -7
  33. data/lib/ecs_rails/config.rb +45 -10
  34. data/lib/ecs_rails/dsl.rb +503 -88
  35. data/lib/ecs_rails/entity.rb +21 -5
  36. data/lib/ecs_rails/errors.rb +3 -0
  37. data/lib/ecs_rails/inverses.rb +398 -0
  38. data/lib/ecs_rails/lazy.rb +86 -23
  39. data/lib/ecs_rails/markers.rb +197 -0
  40. data/lib/ecs_rails/preloading.rb +33 -8
  41. data/lib/ecs_rails/presence.rb +124 -20
  42. data/lib/ecs_rails/querying.rb +195 -17
  43. data/lib/ecs_rails/railtie.rb +10 -0
  44. data/lib/ecs_rails/registry.rb +113 -18
  45. data/lib/ecs_rails/relationships.rb +241 -159
  46. data/lib/ecs_rails/slots.rb +183 -0
  47. data/lib/ecs_rails/validations.rb +3 -5
  48. data/lib/ecs_rails/version.rb +4 -1
  49. data/lib/ecs_rails.rb +51 -6
  50. data/lib/generators/ecs_rails/catalogue_options.rb +66 -0
  51. data/lib/generators/ecs_rails/component/component_generator.rb +5 -0
  52. data/lib/generators/ecs_rails/component/templates/migration.rb.tt +9 -5
  53. data/lib/generators/ecs_rails/component/templates/model.rb.tt +2 -1
  54. data/lib/generators/ecs_rails/entity/entity_generator.rb +93 -0
  55. data/lib/generators/ecs_rails/entity/templates/entity.rb.tt +9 -0
  56. data/lib/generators/ecs_rails/install/install_generator.rb +35 -8
  57. data/lib/generators/ecs_rails/install/templates/application_component.rb.tt +3 -2
  58. data/lib/generators/ecs_rails/install/templates/application_entity.rb.tt +2 -2
  59. data/lib/generators/ecs_rails/install/templates/initializer.rb.tt +2 -2
  60. data/lib/generators/ecs_rails/install/templates/install_migration.rb.tt +38 -0
  61. data/lib/generators/ecs_rails/templates/catalogue_class.rb.tt +10 -0
  62. data/lib/generators/ecs_rails/upgrade/templates/catalogue_migration.rb.tt +18 -0
  63. data/lib/generators/ecs_rails/upgrade/templates/markers_migration.rb.tt +31 -0
  64. data/lib/generators/ecs_rails/upgrade/templates/relationships_migration.rb.tt +33 -0
  65. data/lib/generators/ecs_rails/upgrade/templates/slots_migration.rb.tt +23 -0
  66. data/lib/generators/ecs_rails/upgrade/upgrade_generator.rb +281 -0
  67. metadata +65 -6
  68. data/lib/generators/ecs_rails/install/templates/migration.rb.tt +0 -20
  69. data/lib/generators/ecs_rails/relationship/relationship_generator.rb +0 -94
  70. data/lib/generators/ecs_rails/relationship/templates/migration.rb.tt +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bef233f6d1933696953db3058563f4e4178c67aeb6d56f6d2f241f64f23b81f3
4
- data.tar.gz: a7806e71e299991ae122dd30ce78a009bf181ed55c11d8b43444be2a2f6ef1d8
3
+ metadata.gz: f5ee13ec49dd2b98779e03c1fe5f29e4e9dd26c2fc7efdcc4be69dbb548fc65d
4
+ data.tar.gz: 49a77b4f33c89abd4ffbc1a21f6e4554ebc8540f74f6f562ca94ff73e6ec3b5b
5
5
  SHA512:
6
- metadata.gz: 7fd3624f55f3264bbaf3c84cb355a63cc5e0e5e66eabd43d04ad01c1d87bff202dd8d4e851d6006c66c383c742702da548115161ab33dd2ddc7d1f37b0e86295
7
- data.tar.gz: 926baa1d0cfca36f1453d0cc4abb02d463a04d49b499b2a6eb72f54534a4486ccfe27e1452a6b0a2206f2589062cd4f53aa1b05ab07fed7b4479e19515b92ecd
6
+ metadata.gz: a48ce6bd28e4db48233251a28e5d7991c14a830a04c51e83f321762a544ab74fef9fb321722bac48600a0acd028f67de23343c91104d154bb4f8aa0cf0447451
7
+ data.tar.gz: a8eaa377b244e0a694251aef678ebe6114a67062052824da9d35694095d3a3e340654871f2fb0a025b85643bf809e59bef1bf31156fd1fa2cd2a168b25646198
data/.yardopts ADDED
@@ -0,0 +1,12 @@
1
+ --markup markdown
2
+ --markup-provider kramdown
3
+ --readme README.md
4
+ --title "ECS Rails - an Entity-Component-System reimagining of ActiveRecord"
5
+ --charset utf-8
6
+ --no-private
7
+ --hide-void-return
8
+ --embed-mixins
9
+ lib/**/*.rb
10
+ -
11
+ CHANGELOG.md
12
+ LICENSE.txt
data/CHANGELOG.md CHANGED
@@ -6,6 +6,338 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] — 2026-09-27
10
+
11
+ **The zero-migrations release.** After `rails g ecs_rails:install` and one
12
+ `db:migrate`, entities, labelled slots, relationships, markers and systems are
13
+ plain Ruby: the catalogue ships the components, one shared table holds every
14
+ relationship, another every marker. Breaking, pre-1.0.
15
+
16
+ ### Upgrading from 0.2
17
+
18
+ Back up the database and try this on a copy first. The gem README's
19
+ "Upgrading an existing installation" has the full steps.
20
+
21
+ 1. Update the Gemfile to `gem "ecs_on_rails", "~> 0.3.0"` and run
22
+ `bundle update ecs_on_rails`.
23
+ 2. In development, run `bin/rails generate ecs_rails:upgrade` (add
24
+ `--sets core commerce` for `Money`), review the migrations, then
25
+ `bin/rails db:migrate`. It adds `slot` to every component table and moves
26
+ the unique index to `(entity_id, slot)`, creates `relationships`, `markers`
27
+ and the catalogue tables, and moves rows from per-relationship and marker
28
+ tables into the shared ones, keeping their IDs. Running it again generates
29
+ nothing.
30
+ 3. **Delegated names are prefixed with the reader**: `user.address` becomes
31
+ `user.email_address`. Add `prefix: false` to a declaration to keep bare names.
32
+ 4. **Markers are declared by name**: `component Moderator` becomes
33
+ `marker :moderator`, and `add(Moderator)` becomes `add(:moderator)`. Delete
34
+ the old marker class after migrating.
35
+ 5. **`rails g ecs_rails:relationship` is gone**: `relates_to` needs no
36
+ migration. Nested preloads name `target`:
37
+ `preload(author_relationship: { target: :name })`.
38
+ 6. Run `bin/rails zeitwerk:check` and your tests.
39
+
40
+ ### Added
41
+
42
+ - **Demo journey, identity and accessibility** (ECS-35): the demo opens on a
43
+ four-step tour quoting each step's declaration; a session-backed "acting
44
+ as" bar replaces per-form user pickers; a scripted rendered and axe-core
45
+ review (`demo/script/qa`) brought 21 pages and 7 states to zero WCAG 2.1 AA
46
+ violations in light and dark schemes and fixed phone layouts. See
47
+ [the review](../docs/design/demo-qa.md). No gem change.
48
+
49
+ - **Geocoder map in the demo** (ECS-44): `/geocoder` draws located
50
+ Geolocations on a server-rendered SVG world map (Natural Earth 1:110m, public
51
+ domain; no JavaScript or tiles), with non-overlapping labels, merged markers
52
+ for nearby points, a text list of places, and filter links by entity type that
53
+ narrow map and table with `Geolocation.where(entity: User.all)`. No gem change.
54
+
55
+ - **Geocoder system in the demo** (ECS-8): `Demo::Geocoder` pairs every
56
+ `Address` with a `Geolocation` in the same slot across users (shipping and
57
+ billing) and companies without naming either, skips owners that declare no
58
+ pair (order/invoice snapshots), redoes only new or edited addresses, records
59
+ unknown or blank places without coordinates, and resolves overlapping runs on
60
+ the `(entity_id, slot)` unique index. Lookups are simulated. The demo's
61
+ `/geocoder` page shows each address's state, runs it and lists the one
62
+ migration. `Geolocation#locate` now takes `at:` (default `Time.current`) so a
63
+ system can stamp the address version the coordinates describe, and documents
64
+ `locate(nil, nil)` as a lookup that found nothing.
65
+
66
+ - **Entity generator** (ECS-11): `rails generate ecs_rails:entity Person name
67
+ email mobile:phone work:phone home:address` writes a plain entity class from
68
+ existing components, including labelled slots and namespaces. Validates names,
69
+ concrete references and duplicates before writing; respects configured paths
70
+ and normal file collision handling. Emits no migration. Quickstart/package
71
+ checks now exercise generated classes and independent persisted slots.
72
+
73
+ - **Executable onboarding** (ECS-34): the gem README starts with a contacts
74
+ tutorial covering composition, querying, a cross-entity system and rendering.
75
+ Package CI executes its actual snippets, upgrade commands and bespoke storage
76
+ example. The demo guide documents setup, resets, tests and a guided journey;
77
+ lifecycle guidance covers virtual reads, replacements, targets and deletion.
78
+
79
+ - **Source walkthrough and readability review** (ECS-36): follow installation,
80
+ composition, checkout and entity-independent indexing through the gem and demo.
81
+ Simplified checkout orchestration and Period branching; corrected catalogue
82
+ examples, generated comments and lifecycle/query/token documentation to match
83
+ existing behavior. No API, schema or dependency changes.
84
+
85
+ - **Reproducible performance evidence** (ECS-33): the companion demo includes
86
+ equivalent ECS/conventional Rails catalogue, detail, checkout, indexing and
87
+ slot-preload workloads, behavioral verification and a CI smoke run. Published
88
+ local measurements include latency distributions, allocations, SQL, storage
89
+ and query plans at 100/1,000/5,000 products. See
90
+ [the comparison](../docs/design/performance-comparison.md) for costs and
91
+ limitations; no gem API or production schema change.
92
+
93
+ - **Component queries beyond equality, and ordering** (RFC-0018).
94
+ `with_component` takes a block run on the component's relation
95
+ (`{ where("amount_cents < ?", 5000) }`, `{ tagged("ruby") }`,
96
+ `{ matching(q) }`) and `where`-style positional conditions
97
+ (`with_component(Rating, "stars >= ?", 4)`), all inside the same correlated
98
+ `EXISTS`. New `order_by_component(Money, :amount_cents, :desc, prefix:
99
+ :price)` orders by a component column through a correlated scalar subquery —
100
+ no join, no duplicate rows — with the column defaulting to the component's
101
+ primary attribute and entities lacking the row sorting last.
102
+
103
+ - **Inverse relationships** (RFC-0015): `has_many :comments, via: :post` and
104
+ `has_one :invoice, via: :order` on the parent expand to native `has_many
105
+ :through` over the shared `relationships` table — a real `CollectionProxy` of
106
+ the child class, `<<`, `create!`, scopes, the component query DSL chained on,
107
+ `includes(:comments)`. The child is inferred from the reader, or named
108
+ (`"Comment"`, or the class), and the pair is validated on first use and at
109
+ boot under `eager_load` — never at class-load time, which autoloading makes
110
+ unsafe. `has_one` requires the child's `unique: true`. `dependent: :destroy`
111
+ / `:delete_all` removes the link rows with the parent. `entity.referrers`
112
+ lists every relationship row pointing at an entity. Without `via:` both
113
+ macros are ActiveRecord's own.
114
+
115
+ - **Primary attributes** (RFC-0014 amendment). A component declares
116
+ `primary :value`; a labelled slot of it then also delegates the bare slot
117
+ name, so `component Text, prefix: :title` gives `post.title` / `post.title=`
118
+ (→ `title_text.value`) beside `post.title_text` and `post.title_text_value`.
119
+ The catalogue declares them on `Text`, `Identifier`, `Counter`, `Timestamp`,
120
+ `CalendarDate`, `Rating`, `Position` and `Role`. Decided by the forum rebuild.
121
+
122
+ - **The catalogue** (ADR-0018, RFC-0017): 25 standard components shipped as
123
+ concerns under `EcsRails::Catalogue` — `Name`, `Email`, `Password`, `Phone`,
124
+ `Address`, `Geolocation`, `Link`, `Text`, `Identifier`, `Counter`, `Rating`,
125
+ `Timestamp`, `CalendarDate`, `Period`, `Position`, `State`, `Tags`,
126
+ `SearchVector`, `Discard`, `Image`, `Role`, `Token`, `Marker`, `Relationship`
127
+ (`core`) and `Money` (`commerce`). Each carries its behaviour, validations and
128
+ a schema declaration; `rails g ecs_rails:install` writes a one-line class per
129
+ component and ONE migration creating every table in the chosen `--sets`, so
130
+ an application builds from the catalogue with no further migration.
131
+ `--rename money:Price` is the name-collision remedy. `rails g
132
+ ecs_rails:upgrade` diffs the declarations against the database and writes
133
+ what is missing.
134
+
135
+ - **Markers on one shared table** (ADR-0018 §4, RFC-0016). `marker :moderator`
136
+ declares a marker as slot "moderator" of the app's `Marker` component
137
+ (`EcsRails::Catalogue::Marker`, one-line class written by install), with the
138
+ bare `user.moderator?` predicate, a Boolean `user.moderator = true` writer,
139
+ `add(:moderator)` / `has?` / `remove` by name, and `User.with_marker(:moderator)`
140
+ / `without_marker`. No table per marker; a zero-attribute component on its own
141
+ table still works. `EcsRails.config.marker_class_name` names the class.
142
+ `ecs_rails:upgrade` gains a third job that creates `markers` and moves every
143
+ empty per-marker table into it.
144
+
145
+ - **One shared `relationships` table** (ADR-0017). Every `relates_to` is now a
146
+ row in a table created at install, under a slot named for the relationship;
147
+ declaring a relationship needs no migration. The public API is unchanged
148
+ (`relates_to`, `post.author`, `post.author=`, `with_related`,
149
+ `without_related`, `includes_related`), plus `post.author_id` /
150
+ `post.author_id=`.
151
+ - `relates_to :order, Order, unique: true` — at most one owner of this type per
152
+ target, enforced by a partial unique index created once at install.
153
+ - `EcsRails::Catalogue::Relationship`, the first catalogue concern (ADR-0018);
154
+ `rails g ecs_rails:install` writes the one-line `Relationship` class that
155
+ includes it. `EcsRails.config.relationship_class_name` names it (default
156
+ `"Relationship"`).
157
+ - The target type is checked on assignment: `post.author = team` raises
158
+ `EcsRails::InvalidRelationship`.
159
+ - `rails g ecs_rails:upgrade` gains a second job: create `relationships` and
160
+ move every pre-0.3 per-relationship table into it (recognised by shape and
161
+ name), then drop them. Irreversible; review the generated file.
162
+
163
+ - **Labelled (plural) components — slots** (RFC-0014 / ADR-0015). A component
164
+ may be declared more than once on an entity under distinct labels, each a
165
+ singleton with its own reader: `component Address` and `component Address,
166
+ prefix: :business` give `user.address` and `user.business_address`, backed by
167
+ one `addresses` table and told apart by a new `slot` column. Everything
168
+ singular works per slot with no new API — the lazy reader (a virtual is
169
+ built with its slot preset), delegation (`business_address_line1`), presence
170
+ (`add(Address, prefix: :business)`, `user.business_address?`), validation
171
+ keys (`errors[:"business_address.postcode"]`), querying
172
+ (`with_component(Address, prefix: :business, region: "WA")`) and preloading
173
+ (`includes_components(Address)` batches every slot).
174
+ - `component Foo, delegate: false` — reader and predicate only, no delegated
175
+ methods, for when even prefixed names get long.
176
+ - **Per-slot options.** A component declares what it accepts with
177
+ `slot_option :states, default: []`; the declaration site passes them as extra
178
+ keywords (`component State, prefix: :order, states: %w[pending paid]`); the
179
+ instance reads them back (`order_state.states`, `slot_options`). Unknown
180
+ options raise at declaration time.
181
+ - `Entity.declaration_for(Component, prefix:)`; `Registry::Declaration#slot`,
182
+ `#reader_name`, `#prefix`, `#slot_options`.
183
+ - **`rails g ecs_rails:upgrade`** — one migration that adds the `slot` column
184
+ and the `(entity_id, slot)` unique index to every existing component table
185
+ that lacks them, found by inspecting the database. Safe on shipped data.
186
+ This is the only migration a 0.2.x app needs to run.
187
+
188
+ - `component Foo, prefix: false` — bare delegation for one declaration, where
189
+ the prefix would be redundant (`post.state`, not `post.publish_state_state`).
190
+ `prefix: true` is the explicit default. A Symbol (RFC-0014's slot label)
191
+ raises `ArgumentError` until labelled slots are implemented.
192
+ - **Flat mass assignment.** Because the prefixed writers exist,
193
+ `User.create!(name_first: "Ada", email_address: "a@b.com")` routes each key
194
+ to its component, dirties it and persists it through the save cascade;
195
+ `update!` too. Rails multiparameter form fields (`date_select` →
196
+ `group_founded_on(1i)`…) route the same way. Unknown keys still raise
197
+ `ActiveModel::UnknownAttributeError`.
198
+
199
+ ### Fixed
200
+
201
+ - **Bounded demo indexing** (ECS-32): the component indexer batches 100 owners,
202
+ reads every eligible owner's text slots together, and batches owner discovery
203
+ and existing vector reads. Preserves slot ordering, declaration eligibility
204
+ and vector identity without loading the full Text table.
205
+
206
+ - **Bounded demo lists** (ECS-31): market, bulletin, people, sellers, groups,
207
+ order history and growing detail lists use 24-row Kaminari pages. Stable
208
+ UUID tie-breakers prevent ambiguous ordering; navigation retains filters,
209
+ normalizes invalid pages and preserves total counts. Component and
210
+ relationship preloads apply only to the selected page.
211
+
212
+ - The Rails-compatible JSON `< 3` constraint is now a runtime dependency,
213
+ protecting installed gem consumers as well as development bundles (ECS-30).
214
+ CI covers each Rails 7.1/7.2/8.0/8.1 series, Ruby 3.2 through 4.0 representative
215
+ combinations, the demo suite, documentation and real packaged install/upgrade
216
+ flows against isolated PostgreSQL databases.
217
+
218
+ - Catalogue upgrades verify live column definitions, UUID primary keys,
219
+ unique/partial indexes and validated foreign keys with deletion behavior
220
+ before writing files (ECS-29). Missing compatible structures generate additive
221
+ migrations; incompatible definitions report actual/expected properties and
222
+ require explicit repair. Known catalogue tables with missing attributes are
223
+ no longer mistaken for legacy marker tables. No data conversion is inferred.
224
+
225
+ - Demo checkout and basket edits now coordinate on the basket; stock updates
226
+ lock the actual Counter rows in product order (ECS-28). Submitted revisions
227
+ identify completed orders so successful retries return the original result,
228
+ stale forms are rejected, and declines can retry without partial changes.
229
+ Order/invoice allocation uses transaction advisory locks and numeric suffixes.
230
+ No new demo migration is required. Demo specs refuse non-test environments.
231
+
232
+ - Demo price filters and ordering now treat an absent Money row as the displayed
233
+ zero price, matching persisted free products (ECS-27). The product-specific
234
+ queries use a slot-scoped left join and `COALESCE`, with stable price ties;
235
+ the gem's persisted component-presence semantics are unchanged.
236
+
237
+ - Component assignment now replaces a singleton atomically and updates both
238
+ the lazy memo and association cache (ECS-26), including labelled slots.
239
+ Invalid replacements preserve the old row; new owners defer persistence,
240
+ default-only replacements remain virtual, and nil removes the row. Cross-owner
241
+ and persisted cross-slot moves are rejected. Component reload/reset helpers
242
+ clear both caches; generated build/create helpers now raise an intentional
243
+ error directing callers to reader-based attribute updates.
244
+
245
+ - Explicitly assigned new relationship targets now save with their owner in
246
+ one transaction instead of being silently dropped (ECS-25). The lazy
247
+ cascade recognizes pending targets, and Rails autosave validates and saves
248
+ them and their touched components. Invalid targets fail through `save` /
249
+ `save!`; simply reading an unset relationship still creates no row.
250
+
251
+ - Relationship target IDs now validate against the declared target class,
252
+ including flat mass assignment and direct Relationship saves (ECS-24).
253
+ Wrong-type and nonexistent IDs return validation errors through `save` /
254
+ `save!`; subclasses and nil targets remain valid, and validation reuses
255
+ loaded targets. Wrong-type object assignments still raise immediately.
256
+
257
+ - `EcsRails::Catalogue::Password` no longer raises at class-load time when
258
+ bcrypt is absent — which, under `eager_load`, stopped an application that
259
+ installed `core` from booting. `has_secure_password` is set up when bcrypt is
260
+ available; otherwise `password=` and `authenticate` raise a LoadError saying
261
+ what to add. Surfaced by eager-loading the forum.
262
+ - The catalogue's format validators (`Email#address`, `Phone#e164` and
263
+ `#extension`, `Link#url`, `Address#country`, `Image#url`) use `allow_blank`,
264
+ not `allow_nil`: a blank form field no longer reads "is invalid" on top of
265
+ the application's own presence rule. Surfaced by the forum rebuild (ECS-17).
266
+
267
+ ### Removed
268
+
269
+ - `rails g ecs_rails:relationship` and the per-relationship backing tables
270
+ (`post_authors`, ...). The dynamic `Post::AuthorRelationship` classes are gone;
271
+ `Post.components` lists `Relationship` instead.
272
+
273
+ ### Changed
274
+
275
+ - The install migration is now `ecs_rails_install` (`EcsRailsInstall`) and
276
+ creates both `entities` and `relationships`.
277
+ - `RelationshipMeta` is `(name, target_class_name, unique)` with `slot` and
278
+ `reader_name` derived; `backing_class_name` and `foreign_key` are gone.
279
+ - The nested-preload key for a relationship's target is `target`:
280
+ `preload(author_relationship: { target: :name })`.
281
+ - **Every component table carries `slot string NOT NULL DEFAULT ''`, and the
282
+ unique index moves from `entity_id` to `(entity_id, slot)`.** The
283
+ `ecs_rails:component` and `ecs_rails:relationship` generators emit the new
284
+ shape; existing tables need `rails g ecs_rails:upgrade` (above). **Breaking
285
+ for existing databases** until upgraded: the gem's has_ones are slot-scoped
286
+ and will not find rows in a table with no slot column.
287
+ - `Entity.components` lists each component type once however many slots it is
288
+ declared under; `component_declarations` lists each slot.
289
+ - A new reader colliding with a name the entity already answers (a sibling's
290
+ reader or delegated method) now raises `DelegationConflict` at declaration.
291
+ - `EcsRails::Registry#register` takes `slot:` and `slot_options:`; the same
292
+ component in a different slot is a distinct declaration, the same slot twice
293
+ is still `DuplicateComponent`.
294
+
295
+ - **Delegation is component-prefixed by default** (ADR-0016). A component's
296
+ delegated methods are named `#{reader}_#{method}`: `component Email` now gives
297
+ `user.email_address`, `user.email_verified` and
298
+ `user.email_send_welcome_email`, all routed through `user.email`. The rule is
299
+ uniform — attributes and behaviour alike — so two components can never
300
+ collide on a shared attribute name, and a component reader can never be
301
+ shadowed by a delegated `belongs_to`. The reader (`user.email`) and the
302
+ presence predicate (`user.email?`) are unchanged.
303
+ **Breaking:** every bare delegated call (`user.address`) becomes
304
+ `user.email_address` unless the declaration opts out. `only:`/`except:`
305
+ still name the component's own methods (`except: [:title]`).
306
+ - `EcsRails::DelegationConflict` is now raised on the *entity-level* names, so
307
+ `Name#title` and `Group#title` coexist as `name_title` / `group_title` with no
308
+ `except:`. A conflict, and the reader-collision raise, now need two bare
309
+ declarations; both messages name dropping `prefix: false` as a way out.
310
+ - `relates_to` declares its backing component bare, so `post.author` /
311
+ `post.author=` keep their shape.
312
+
313
+ ## [0.2.2] — 2026-07-23
314
+
315
+ ### Added
316
+
317
+ - **YARD API documentation across the whole public API** — `@param`, `@return`,
318
+ `@raise`, `@example` and `@see` tags on every public module, class, attribute
319
+ and method (100% documented, verified by `yard stats`). The existing prose
320
+ rationale is kept; the tags are additive, so
321
+ [rubydoc.info/gems/ecs_on_rails](https://rubydoc.info/gems/ecs_on_rails) now
322
+ renders a real API reference rather than bare comments.
323
+ - A `.yardopts`, shipped in the gem so rubydoc.info honours it. Notably
324
+ `--embed-mixins`, without which the DSL that `EcsRails::Entity` gains by
325
+ `extend`ing {EcsRails::DSL}, `Querying`, `Preloading` and `Relationships`
326
+ would not appear on `Entity` at all — which is where users look for it.
327
+ - `documentation_uri` in the gemspec, pointing at the rubydoc.info API
328
+ reference. The narrative documentation (architecture, ADRs, RFCs) is reached
329
+ via the homepage and `source_code_uri`.
330
+
331
+ ### Fixed
332
+
333
+ - `changelog_uri` in the gemspec pointed at `<repo>/blob/main/CHANGELOG.md`,
334
+ which 404s: the gem lives in `gem/` of a monorepo and there is no changelog at
335
+ the repo root, so the Changelog link on the 0.2.1 RubyGems page was broken.
336
+ Gem metadata is immutable once published, so this needed a release to correct.
337
+ - `source_code_uri` now points at the `gem/` subtree rather than the repo root.
338
+
339
+ No code change — 0.2.2 is identical to 0.2.1 apart from gemspec metadata.
340
+
9
341
  ## [0.2.1] — 2026-07-23
10
342
 
11
343
  First RubyGems release.