upkeep-rails 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.
Potentially problematic release.
This version of upkeep-rails might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +424 -0
- data/docs/architecture/ambient-inputs-roadmap.md +306 -0
- data/docs/architecture/herb-roadmap.md +324 -0
- data/docs/architecture/identity-and-sharing.md +187 -0
- data/docs/architecture/query-dependencies.md +230 -0
- data/docs/cost-model-roadmap.md +703 -0
- data/docs/guides/getting-started.md +282 -0
- data/docs/handoff-2026-05-15.md +230 -0
- data/docs/production_roadmap.md +372 -0
- data/docs/shared-warm-scale-roadmap.md +214 -0
- data/docs/single-subscriber-cold-roadmap.md +192 -0
- data/docs/testing.md +113 -0
- data/lib/generators/upkeep/install/install_generator.rb +90 -0
- data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +31 -0
- data/lib/generators/upkeep/install/templates/subscription.js +107 -0
- data/lib/generators/upkeep/install/templates/upkeep.rb +6 -0
- data/lib/upkeep/active_record_query.rb +294 -0
- data/lib/upkeep/capture/request.rb +150 -0
- data/lib/upkeep/dag/subscription_shape.rb +244 -0
- data/lib/upkeep/dag.rb +370 -0
- data/lib/upkeep/delivery/action_cable_adapter.rb +43 -0
- data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
- data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
- data/lib/upkeep/delivery/transport.rb +194 -0
- data/lib/upkeep/delivery/turbo_streams.rb +275 -0
- data/lib/upkeep/delivery.rb +7 -0
- data/lib/upkeep/dependencies.rb +466 -0
- data/lib/upkeep/herb/developer_report.rb +116 -0
- data/lib/upkeep/herb/manifest_cache.rb +83 -0
- data/lib/upkeep/herb/manifest_diff.rb +183 -0
- data/lib/upkeep/herb/source_instrumenter.rb +84 -0
- data/lib/upkeep/herb/template_manifest.rb +377 -0
- data/lib/upkeep/invalidation/collection_append.rb +84 -0
- data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
- data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
- data/lib/upkeep/invalidation/collection_remove.rb +57 -0
- data/lib/upkeep/invalidation/planner.rb +341 -0
- data/lib/upkeep/invalidation.rb +7 -0
- data/lib/upkeep/rails/action_view_capture.rb +765 -0
- data/lib/upkeep/rails/cable/channel.rb +108 -0
- data/lib/upkeep/rails/cable/subscriber_identity.rb +214 -0
- data/lib/upkeep/rails/cable.rb +4 -0
- data/lib/upkeep/rails/client_subscription.rb +37 -0
- data/lib/upkeep/rails/configuration.rb +57 -0
- data/lib/upkeep/rails/controller_runtime.rb +137 -0
- data/lib/upkeep/rails/install.rb +28 -0
- data/lib/upkeep/rails/railtie.rb +36 -0
- data/lib/upkeep/rails/replay.rb +176 -0
- data/lib/upkeep/rails/testing.rb +36 -0
- data/lib/upkeep/rails.rb +276 -0
- data/lib/upkeep/replay.rb +408 -0
- data/lib/upkeep/runtime.rb +1075 -0
- data/lib/upkeep/shared_streams.rb +72 -0
- data/lib/upkeep/subscriptions/active_record_store.rb +292 -0
- data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +291 -0
- data/lib/upkeep/subscriptions/active_registry.rb +93 -0
- data/lib/upkeep/subscriptions/async_durable_writer.rb +136 -0
- data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
- data/lib/upkeep/subscriptions/layered_reverse_index.rb +122 -0
- data/lib/upkeep/subscriptions/persistent_reverse_index.rb +144 -0
- data/lib/upkeep/subscriptions/registrar.rb +36 -0
- data/lib/upkeep/subscriptions/reverse_index.rb +294 -0
- data/lib/upkeep/subscriptions/shape.rb +116 -0
- data/lib/upkeep/subscriptions/store.rb +159 -0
- data/lib/upkeep/subscriptions.rb +7 -0
- data/lib/upkeep/targeting.rb +135 -0
- data/lib/upkeep/version.rb +5 -0
- data/lib/upkeep-rails.rb +3 -0
- data/lib/upkeep.rb +14 -0
- data/upkeep-rails.gemspec +53 -0
- metadata +296 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Identity And Sharing
|
|
2
|
+
|
|
3
|
+
Upkeep can share rendered work only when every value that can affect the bytes
|
|
4
|
+
is represented in the graph identity. Rails apps should not declare identity
|
|
5
|
+
dimensions to Upkeep. The runtime derives identity by observing Rails request,
|
|
6
|
+
auth, current-context, and ActionCable surfaces.
|
|
7
|
+
|
|
8
|
+
## Observed Identity Surfaces
|
|
9
|
+
|
|
10
|
+
CurrentAttributes:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
14
|
+
attribute :user, :account_id
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Reads of `Current.user`, `Current.account_id`, and other
|
|
19
|
+
`ActiveSupport::CurrentAttributes` attributes are recorded as identity
|
|
20
|
+
dependencies for the frame that performed the read.
|
|
21
|
+
|
|
22
|
+
Warden and Devise:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
request.env["warden"].user(:user)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Warden `user` and `authenticate` calls record the requested scope and canonical
|
|
29
|
+
user identity. Devise applications normally route `current_user` through Warden,
|
|
30
|
+
so the same observer covers that path when the rendered code reaches Warden.
|
|
31
|
+
|
|
32
|
+
Session and cookies:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
session[:tenant_id]
|
|
36
|
+
cookies[:theme]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Session and cookie reads record private fingerprints of the values. The values
|
|
40
|
+
are not exposed in delivery reports. Controller page replay keeps the observed
|
|
41
|
+
raw values only when replay needs them; unread session keys and cookies are not
|
|
42
|
+
copied into replay payloads.
|
|
43
|
+
|
|
44
|
+
Request values:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
request.subdomain
|
|
48
|
+
request.params
|
|
49
|
+
request.user_agent
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Request reads record the key and a private fingerprint of the value. Host,
|
|
53
|
+
subdomain, path, fullpath, request method, user agent, remote IP, and params are
|
|
54
|
+
covered. Replay copies only the observed request values that map to supported
|
|
55
|
+
Rack env keys, such as `request.user_agent`; unread headers such as
|
|
56
|
+
authorization or CSRF-style headers are not copied.
|
|
57
|
+
|
|
58
|
+
ActionCable identifiers:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
identified_by :current_user
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The subscription stream identity is derived from canonical ActionCable
|
|
65
|
+
connection identifiers and, for the request that created the subscription,
|
|
66
|
+
observed recorder identity dependencies.
|
|
67
|
+
|
|
68
|
+
## Sharing Rules
|
|
69
|
+
|
|
70
|
+
Public render output can share when:
|
|
71
|
+
|
|
72
|
+
- the selected target is the same page, fragment, or render site;
|
|
73
|
+
- the identity signature is `public`;
|
|
74
|
+
- the replay recipe and sharing inputs are equivalent.
|
|
75
|
+
|
|
76
|
+
Equivalent public targets are grouped before replay, so one render can serve
|
|
77
|
+
multiple subscribers. Delivery still merges identical payload bytes after
|
|
78
|
+
rendering when separate groups converge on the same output.
|
|
79
|
+
|
|
80
|
+
Identity-bound output is partitioned when:
|
|
81
|
+
|
|
82
|
+
- a frame reads `Current.user`, another CurrentAttributes value, Warden/Devise,
|
|
83
|
+
session, cookie, or request state;
|
|
84
|
+
- two subscribers have different observed identity values;
|
|
85
|
+
- the same DOM target renders different bytes under different identity.
|
|
86
|
+
|
|
87
|
+
The delivery layer consumes identity signatures. It does not decide whether a
|
|
88
|
+
subscriber is eligible for a payload.
|
|
89
|
+
|
|
90
|
+
Identity dependencies remain attached to the graph for replay and sharing. They
|
|
91
|
+
do not create lifecycle reverse-index rows, because Active Record commits cannot
|
|
92
|
+
select request, session, cookie, Warden, CurrentAttributes, or ActionCable
|
|
93
|
+
identity reads directly.
|
|
94
|
+
|
|
95
|
+
## Authorization Example
|
|
96
|
+
|
|
97
|
+
If a card value is visible only when it is under the current user's limit:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
class CardPresenter
|
|
101
|
+
def initialize(card)
|
|
102
|
+
@card = card
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def value_content
|
|
106
|
+
return "Hidden" unless @card.value <= Current.user.value_limit
|
|
107
|
+
|
|
108
|
+
"$#{@card.value}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The render reads:
|
|
114
|
+
|
|
115
|
+
- `Current.user`, which partitions the frame by current user;
|
|
116
|
+
- `Current.user.value_limit`, which records an Active Record attribute
|
|
117
|
+
dependency for the user row;
|
|
118
|
+
- `card.value`, which records an Active Record attribute dependency for the
|
|
119
|
+
card row.
|
|
120
|
+
|
|
121
|
+
When the card changes, each subscriber gets the payload rendered under their
|
|
122
|
+
own identity. When a user's `value_limit` changes, only the subscriber identity
|
|
123
|
+
bound to that user is selected.
|
|
124
|
+
|
|
125
|
+
## Ambiguous Identity
|
|
126
|
+
|
|
127
|
+
Identity is ambiguous when render output depends on values outside the observed
|
|
128
|
+
Rails surfaces, such as:
|
|
129
|
+
|
|
130
|
+
- a plain global singleton;
|
|
131
|
+
- an unobserved thread-local;
|
|
132
|
+
- a closure that captured a user or tenant but does not read Current, request,
|
|
133
|
+
Warden, session, or cookies during render;
|
|
134
|
+
- external process state with no observed request or data dependency.
|
|
135
|
+
|
|
136
|
+
The correct shape is to make identity visible through Rails-owned surfaces that
|
|
137
|
+
Upkeep observes: CurrentAttributes, Warden/Devise, session, cookies, request
|
|
138
|
+
values, ActionCable identifiers, or Active Record reads.
|
|
139
|
+
|
|
140
|
+
## Refused Boundaries
|
|
141
|
+
|
|
142
|
+
Development and test raise for reactive boundaries that would otherwise become
|
|
143
|
+
unsafe or broad. Production-style warning mode records a refused boundary and
|
|
144
|
+
skips subscription registration.
|
|
145
|
+
|
|
146
|
+
Opaque collection predicates:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
Card.where("status = ?", "open")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Refactor to structural Active Record or Arel:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
Card.where(status: "open")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Opaque pluck expressions:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
Tag.pluck("LOWER(name)")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Refactor to a structural column read or render outside Upkeep reactivity:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
Tag.pluck(:name).map(&:downcase)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Hidden controller state is not a dependency by itself:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
@cards = Card.where(status: "open").to_a
|
|
174
|
+
@cards.count
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
If the materialized relation is rendered as a collection, Upkeep attaches the
|
|
178
|
+
relation dependency to that render site. If scalar query output affects the
|
|
179
|
+
page, use structural `pluck`. If record attributes affect output, read the
|
|
180
|
+
attributes so Active Record attribute dependencies can select the page.
|
|
181
|
+
|
|
182
|
+
## No Scope Registry
|
|
183
|
+
|
|
184
|
+
There is no `identity_attributes`, `tracks :tenant_id`, `declared_inputs`, or
|
|
185
|
+
session-key allow-list. A host-maintained registry would either duplicate what
|
|
186
|
+
the runtime can observe or become a silent data-leak risk when the host forgets
|
|
187
|
+
an input. Upkeep's identity claim depends on structural observation.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Query Dependencies
|
|
2
|
+
|
|
3
|
+
Active Record collection dependencies are derived from relation shape. Upkeep
|
|
4
|
+
does not ask the app to register queries, predicates, params, or invalidation
|
|
5
|
+
keys.
|
|
6
|
+
|
|
7
|
+
## Capture Point
|
|
8
|
+
|
|
9
|
+
When Rails renders an Active Record relation as a collection, Upkeep analyzes
|
|
10
|
+
the relation before creating the collection dependency:
|
|
11
|
+
|
|
12
|
+
```erb
|
|
13
|
+
<%= render partial: "cards/card", collection: @board.cards.order(:position), as: :card %>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The dependency stores:
|
|
17
|
+
|
|
18
|
+
- the primary model table;
|
|
19
|
+
- the relation SQL digest;
|
|
20
|
+
- table and column coverage derived from Arel;
|
|
21
|
+
- proven column coverage;
|
|
22
|
+
- replay inputs for the render-site recipe.
|
|
23
|
+
|
|
24
|
+
If a controller materializes the relation first, the materialized collection
|
|
25
|
+
retains relation provenance:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
def index
|
|
29
|
+
@cards = Card.where(status: params.fetch(:status)).order(:position).to_a
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```erb
|
|
34
|
+
<%= render partial: "cards/card", collection: @cards, as: :card %>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The render-site consumes that provenance and records the collection dependency
|
|
38
|
+
at the rendered collection boundary. `Relation#exec_queries` does not register
|
|
39
|
+
an Active Record collection dependency by itself.
|
|
40
|
+
|
|
41
|
+
If a controller materializes a relation and never renders that relation-backed
|
|
42
|
+
collection, the materialization is not a lifecycle dependency. Upkeep relies on
|
|
43
|
+
the surfaces that actually affect output: rendered relation collections,
|
|
44
|
+
scalar query dependencies, and record attribute reads.
|
|
45
|
+
|
|
46
|
+
Scalar relation queries are page-level query dependencies, not collection
|
|
47
|
+
dependencies:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
def index
|
|
51
|
+
@tag_names = Tag.where(active: true).pluck(:name)
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This records an `active_record_query` dependency. A matching commit can replay
|
|
56
|
+
the page because scalar query output may affect the page, but the dependency is
|
|
57
|
+
not eligible for collection append/remove/prepend/member-replace planning.
|
|
58
|
+
Simple plucked columns are included in the query dependency. Opaque pluck
|
|
59
|
+
expressions, such as raw SQL expressions, are refused instead of becoming broad
|
|
60
|
+
collection dependencies.
|
|
61
|
+
|
|
62
|
+
## Collection Coverage
|
|
63
|
+
|
|
64
|
+
Upkeep records a collection dependency only when it can see the involved tables
|
|
65
|
+
and columns:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
Card.where(board_id: board.id, status: "open").order(:position)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This records the `cards` table with the primary key plus the predicate and
|
|
72
|
+
ordering columns. Updates to unrelated columns do not select the collection
|
|
73
|
+
dependency.
|
|
74
|
+
|
|
75
|
+
Opaque predicates are not registered as broad collection dependencies:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
Card.where("status = ?", "open").order(:position)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
In development/test this raises `Upkeep::ActiveRecordQuery::OpaqueRelationError`
|
|
82
|
+
while capturing the reactive render. In production-style warning mode, Upkeep
|
|
83
|
+
warns, marks the boundary refused, and skips subscription registration instead
|
|
84
|
+
of broadening to the whole `cards` table.
|
|
85
|
+
|
|
86
|
+
Preferred refactor:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
Card.where(status: "open").order(:position)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Opaque table sources are not reactive:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
Card
|
|
96
|
+
.joins("INNER JOIN authors ON authors.id = cards.author_id")
|
|
97
|
+
.where("authors.name = ?", "Ada")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This raises `Upkeep::ActiveRecordQuery::OpaqueRelationError` while capturing
|
|
101
|
+
the reactive render. Upkeep does not create a dependency for a relation whose
|
|
102
|
+
table sources cannot be structurally proven.
|
|
103
|
+
|
|
104
|
+
Preferred refactor:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
Card.joins(:author).where(authors: { name: "Ada" })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Reverse Index Shape
|
|
111
|
+
|
|
112
|
+
Collection dependencies enter the invalidation reverse index through concrete
|
|
113
|
+
table and column lookup keys:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
Card.where(status: "open").order(:position)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This registers collection lookup keys for the proven `cards` columns, such as
|
|
120
|
+
`status`, `id`, and `position`. A lifecycle event for `cards.title` does not
|
|
121
|
+
select this collection unless `title` was part of the proven relation shape.
|
|
122
|
+
|
|
123
|
+
`active_record_query` dependencies use the same table and column lookup shape,
|
|
124
|
+
but target the nearest page frame. They intentionally do not unlock collection
|
|
125
|
+
operation planning, because scalar query output has no rendered collection
|
|
126
|
+
membership boundary.
|
|
127
|
+
|
|
128
|
+
Record-specific attribute reads index the exact table, id, and attribute:
|
|
129
|
+
|
|
130
|
+
```erb
|
|
131
|
+
<%= card.title %>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Bulk or otherwise id-less attribute changes use an any-id attribute lookup key
|
|
135
|
+
for the changed column. Record-specific reads no longer also register an any-id
|
|
136
|
+
lookup row.
|
|
137
|
+
|
|
138
|
+
Request, session, cookie, CurrentAttributes, Warden, and ActionCable identity
|
|
139
|
+
dependencies are graph inputs, not lifecycle invalidation keys. They partition
|
|
140
|
+
replay and sharing, but an Active Record commit cannot select them directly, so
|
|
141
|
+
they do not occupy reverse-index lookup rows.
|
|
142
|
+
|
|
143
|
+
## Joins And Aliases
|
|
144
|
+
|
|
145
|
+
Association joins are structural:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
Card.joins(:author).where(authors: { name: "Ada" }).order(:position)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Upkeep records columns from both the `cards` table and the `authors` table.
|
|
152
|
+
Aliased self-joins map alias columns back to the underlying table so committed
|
|
153
|
+
changes use real table names from Active Record callbacks.
|
|
154
|
+
|
|
155
|
+
## Bulk Writes
|
|
156
|
+
|
|
157
|
+
Bulk writes are observed through Active Record relation methods:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
Card.where(board_id: board.id).update_all(status: "done")
|
|
161
|
+
Card.where(status: "archived").delete_all
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
For hash updates, Upkeep records the changed keys. For string updates, Upkeep
|
|
165
|
+
records all columns on the model table because parsing assignment SQL would not
|
|
166
|
+
be a structural proof. Relation predicate coverage is derived through the same
|
|
167
|
+
Arel analyzer used for collection reads. Bulk write events may carry table
|
|
168
|
+
coverage for opaque write predicates, but that coverage describes the write
|
|
169
|
+
event; it is not a collection subscription fallback.
|
|
170
|
+
|
|
171
|
+
## Provable Collection Operations
|
|
172
|
+
|
|
173
|
+
Collection changes can sometimes be delivered as narrow Turbo Stream operations
|
|
174
|
+
instead of replacing the whole render site. These operations are optimizations,
|
|
175
|
+
not the correctness path.
|
|
176
|
+
|
|
177
|
+
Operation proofs currently cover:
|
|
178
|
+
|
|
179
|
+
- `append` for creates when replay proves the new record appears after all
|
|
180
|
+
previously rendered member ids;
|
|
181
|
+
- `prepend` for creates when replay proves the new record appears before all
|
|
182
|
+
previously rendered member ids;
|
|
183
|
+
- `remove` for destroys when the prior rendered member target is known;
|
|
184
|
+
- stable member `replace` for updates when the row remains in the relation and
|
|
185
|
+
replay proves the rendered member order did not change.
|
|
186
|
+
|
|
187
|
+
Each operation requires a collection render-site recipe, an Active Record
|
|
188
|
+
relation snapshot, a primary key, proven column-level relation coverage, and a
|
|
189
|
+
relation shape without limit, offset, distinct, group, or having. If any
|
|
190
|
+
requirement fails, delivery deoptimizes to the canonical render-site
|
|
191
|
+
replacement recipe and reports the reason.
|
|
192
|
+
|
|
193
|
+
## Debugging Surface
|
|
194
|
+
|
|
195
|
+
Collection dependency metadata reports:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
{
|
|
199
|
+
primary_table: "cards",
|
|
200
|
+
table_columns: { "cards" => ["board_id", "id", "position"] },
|
|
201
|
+
coverage: "columns",
|
|
202
|
+
sql: "SELECT ..."
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The important debugging question is the coverage level. `columns` is narrow and
|
|
207
|
+
accepted for collection dependencies. Opaque predicates or table sources raise
|
|
208
|
+
a guided error in development/test or become refused boundaries in
|
|
209
|
+
production-style warning mode instead of registering a broad invalidation
|
|
210
|
+
dependency.
|
|
211
|
+
|
|
212
|
+
Operation planning also reports whether a narrow collection operation was
|
|
213
|
+
proved or deoptimized to replacement. These diagnostics explain implementation
|
|
214
|
+
choices; benchmark summaries remain responsible for comparing runtime cost
|
|
215
|
+
against Turbo refresh and manually written Turbo Stream operations.
|
|
216
|
+
|
|
217
|
+
## No Query Catalog
|
|
218
|
+
|
|
219
|
+
There is no Rails-side equivalent of:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
tracks_query :open_cards
|
|
223
|
+
declared_inputs :board_id, :status
|
|
224
|
+
config.identity_attributes = [...]
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The runtime derives what it can from Active Record and Arel. When it cannot
|
|
228
|
+
prove the table sources for a relation, it rejects the reactive boundary with
|
|
229
|
+
guidance to rewrite the query through structural Active Record/Arel joins or
|
|
230
|
+
move the render outside Upkeep reactivity.
|