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,306 @@
|
|
|
1
|
+
# Ambient Inputs Roadmap
|
|
2
|
+
|
|
3
|
+
Rails apps commonly derive render state through controller and request
|
|
4
|
+
surfaces before the view renders. Upkeep should support that idiom without
|
|
5
|
+
requiring app-declared identity dimensions and without overfitting to any
|
|
6
|
+
single benchmark app.
|
|
7
|
+
|
|
8
|
+
The design rule is: observe the Rails-owned surface, scope the value to the
|
|
9
|
+
target that needs it, replay only the observed inputs, and never turn ambient
|
|
10
|
+
inputs into lifecycle invalidation lookup rows.
|
|
11
|
+
|
|
12
|
+
## Rails Idioms To Support
|
|
13
|
+
|
|
14
|
+
- `before_action` authentication that derives `@user` from session, Warden,
|
|
15
|
+
Devise, cookies, or another Rails request surface.
|
|
16
|
+
- Controller-built Active Record relations whose shape depends on the current
|
|
17
|
+
user, params, request values, or cookie-backed preferences.
|
|
18
|
+
- Views and helpers that read controller instance state such as `@user`,
|
|
19
|
+
`@page`, `@domain`, and filter state prepared by the controller.
|
|
20
|
+
- Cookie-backed preferences and filters such as theme, compact mode, and tag
|
|
21
|
+
filters.
|
|
22
|
+
- Request-shaped pages that depend on params, path, subdomain, request method,
|
|
23
|
+
user agent, or remote IP.
|
|
24
|
+
- Mixed public/private pages where public render sites can still share while
|
|
25
|
+
identity-bound fragments or render sites remain partitioned.
|
|
26
|
+
|
|
27
|
+
## Non-Goals
|
|
28
|
+
|
|
29
|
+
- No whole-session snapshot as the steady-state replay model.
|
|
30
|
+
- No Lobsters-specific keys such as `session[:u]` or `cookies[:tag_filters]`
|
|
31
|
+
in the runtime.
|
|
32
|
+
- No host-maintained allow-list of identity dimensions.
|
|
33
|
+
- No session, cookie, request, Warden, CurrentAttributes, or ActionCable
|
|
34
|
+
dependency rows in lifecycle invalidation indexes.
|
|
35
|
+
- No silent broad fallback for opaque Active Record relation shape.
|
|
36
|
+
|
|
37
|
+
## Current Checkpoint
|
|
38
|
+
|
|
39
|
+
The Lobsters benchmark app is present as a broad Rails workload. Controller
|
|
40
|
+
page replay now rebuilds Rack session state and plain cookie headers from
|
|
41
|
+
observed reads instead of snapshotting the whole Rack session or original
|
|
42
|
+
`HTTP_COOKIE`. Request reads are recorded in the same ambient replay-input
|
|
43
|
+
ledger, but target-level request scoping remains a later phase.
|
|
44
|
+
|
|
45
|
+
The reverse-index boundary is already correct: lifecycle lookup keys are still
|
|
46
|
+
limited to Active Record attribute and collection dependencies.
|
|
47
|
+
|
|
48
|
+
## Implementation Loop
|
|
49
|
+
|
|
50
|
+
For each step:
|
|
51
|
+
|
|
52
|
+
1. Add or update focused tests first.
|
|
53
|
+
2. Implement the narrow runtime behavior.
|
|
54
|
+
3. Remove the old broader path in the same slice.
|
|
55
|
+
4. Run the focused tests and `git diff --check`.
|
|
56
|
+
5. Update this roadmap with status and evidence.
|
|
57
|
+
6. Commit before moving to the next step.
|
|
58
|
+
|
|
59
|
+
## Phase 1: Observed Ambient Replay Inputs
|
|
60
|
+
|
|
61
|
+
Goal: replay only ambient values that were actually observed.
|
|
62
|
+
|
|
63
|
+
Work:
|
|
64
|
+
|
|
65
|
+
- Add a recorder-owned ambient replay input ledger for session, cookie, and
|
|
66
|
+
request reads.
|
|
67
|
+
- Have session, cookie, and request observers record both the identity
|
|
68
|
+
dependency and the replay input.
|
|
69
|
+
- Replace whole `rack.session` snapshots with a replay session built from
|
|
70
|
+
observed session keys plus a stable session id when Rails exposes one.
|
|
71
|
+
- Keep raw replay values out of dependency identity keys; dependency identity
|
|
72
|
+
continues to use private fingerprints.
|
|
73
|
+
- Add tests proving unrelated session keys do not enter replay recipes or
|
|
74
|
+
shared stream signatures.
|
|
75
|
+
|
|
76
|
+
Exit criteria:
|
|
77
|
+
|
|
78
|
+
- A controller page that reads `session[:account_token]` can replay.
|
|
79
|
+
- An unread session key is absent from the recipe.
|
|
80
|
+
- Changing an unread session key does not change a public render-site sharing
|
|
81
|
+
signature.
|
|
82
|
+
- No whole-session snapshot path remains.
|
|
83
|
+
|
|
84
|
+
Status: landed for observed session replay inputs. Evidence:
|
|
85
|
+
|
|
86
|
+
- Controller page replay survives `session[:account_token]`-style reads.
|
|
87
|
+
- Unread Rack session keys are absent from the serialized recipe.
|
|
88
|
+
- Controller page replay survives plain `cookies[:theme]`-style reads.
|
|
89
|
+
- Unread cookie keys are absent from the replay `HTTP_COOKIE` header.
|
|
90
|
+
- Changing an unread session key does not change the public render-site shared
|
|
91
|
+
stream names for the page.
|
|
92
|
+
- The old whole-session `to_hash` / `to_h` snapshot path has been removed from
|
|
93
|
+
controller page replay.
|
|
94
|
+
|
|
95
|
+
Remaining follow-up: none for this phase.
|
|
96
|
+
|
|
97
|
+
## Phase 2: Scoped Identity And Replay Inputs
|
|
98
|
+
|
|
99
|
+
Goal: identity and replay inputs should affect only the targets that depend on
|
|
100
|
+
them.
|
|
101
|
+
|
|
102
|
+
Work:
|
|
103
|
+
|
|
104
|
+
- Make page targets inherit request-level ambient inputs from controller and
|
|
105
|
+
before-action reads.
|
|
106
|
+
- Keep render-site and fragment targets public only when their render subtree
|
|
107
|
+
has no ambient identity dependency and their replay recipe does not depend on
|
|
108
|
+
unproven controller ambient state.
|
|
109
|
+
- Partition render sites that directly read session, cookie, request, Warden,
|
|
110
|
+
CurrentAttributes, or ActionCable identity.
|
|
111
|
+
- Deopt to a known enclosing page target, or refuse the narrower boundary, when
|
|
112
|
+
a render-site recipe depends on hidden controller state that cannot be scoped.
|
|
113
|
+
|
|
114
|
+
Exit criteria:
|
|
115
|
+
|
|
116
|
+
- A public render site inside a session-authenticated page can still use a
|
|
117
|
+
shared stream when its bytes do not depend on the user.
|
|
118
|
+
- A render site that reads `cookies[:theme]` is identity-bound by that cookie.
|
|
119
|
+
- A helper that depends on hidden controller state either gets a safe enclosing
|
|
120
|
+
target or a refused-boundary reason.
|
|
121
|
+
|
|
122
|
+
Status: page/request identity inheritance and render-subtree identity landed.
|
|
123
|
+
Evidence:
|
|
124
|
+
|
|
125
|
+
- Page frame identity profiles include request-level ambient reads from
|
|
126
|
+
controller and before-action work.
|
|
127
|
+
- Render-site frames inside the same page remain public when their render
|
|
128
|
+
subtree has no direct ambient identity dependency.
|
|
129
|
+
- Public render-site shared stream names remain available inside a
|
|
130
|
+
session-backed controller page.
|
|
131
|
+
- Render-site identity profiles include ambient reads from descendant member
|
|
132
|
+
partials.
|
|
133
|
+
- Render sites with identity-bound member subtrees do not register public
|
|
134
|
+
shared stream names.
|
|
135
|
+
- Hidden controller-state cases now stay explicit in reports instead of
|
|
136
|
+
silently widening. Herb fallback analysis reports named page fallback reasons
|
|
137
|
+
such as `preloaded_plain_data`, `helper_hidden_collection`,
|
|
138
|
+
`multi_root_partial`, and `page_dependency_without_narrower_frame`, and the
|
|
139
|
+
developer report attaches refactor guidance.
|
|
140
|
+
|
|
141
|
+
Remaining follow-up: none for this phase.
|
|
142
|
+
|
|
143
|
+
## Phase 3: Controller Relation Provenance
|
|
144
|
+
|
|
145
|
+
Goal: support controller-built relations without treating every query
|
|
146
|
+
materialization as a render dependency.
|
|
147
|
+
|
|
148
|
+
Work:
|
|
149
|
+
|
|
150
|
+
- Track relation provenance when a controller materializes an Active Record
|
|
151
|
+
relation under observation.
|
|
152
|
+
- Attach collection dependencies when a relation or its materialized result is
|
|
153
|
+
rendered, not simply because any controller query ran.
|
|
154
|
+
- For page replay, allow page-level dependencies from controller queries whose
|
|
155
|
+
relation shape is structural.
|
|
156
|
+
- Raise/refuse opaque controller relation shape in strict mode instead of
|
|
157
|
+
silently ignoring it or widening to a broad table dependency.
|
|
158
|
+
- Remove broad `Relation#exec_queries` dependency capture once rendered
|
|
159
|
+
relation provenance covers the needed cases.
|
|
160
|
+
|
|
161
|
+
Exit criteria:
|
|
162
|
+
|
|
163
|
+
- A controller-built `@stories = Story.where(status: "active")` collection
|
|
164
|
+
invalidates through the rendered collection boundary.
|
|
165
|
+
- An auth lookup such as `User.find_by(session_token: session[:u])` does not
|
|
166
|
+
become an unrelated rendered collection dependency.
|
|
167
|
+
- Opaque controller SQL gets a guided refused-boundary reason.
|
|
168
|
+
|
|
169
|
+
Status:
|
|
170
|
+
|
|
171
|
+
- Opaque relation materialization under observation no longer disappears
|
|
172
|
+
silently. Strict mode raises before the query executes; warning mode records
|
|
173
|
+
a refused boundary with `opaque_active_record_relation` and skips the
|
|
174
|
+
collection dependency instead of widening to a broad table dependency.
|
|
175
|
+
- Controller materialized relations now retain relation provenance on the
|
|
176
|
+
returned collection. When that materialized result is rendered as a Rails
|
|
177
|
+
collection, the render-site receives the Active Record collection dependency,
|
|
178
|
+
the replay snapshot stays relation-shaped, and `Relation#exec_queries` no
|
|
179
|
+
longer registers a broad collection dependency on its own.
|
|
180
|
+
- Structural `Relation#pluck` calls now register `active_record_query`
|
|
181
|
+
dependencies instead of `active_record_collection` dependencies. They can
|
|
182
|
+
select a page replay when scalar query output changes, but they are not
|
|
183
|
+
eligible for collection append/remove/prepend planning. Opaque pluck
|
|
184
|
+
expressions raise in strict mode and refuse the boundary in warning mode.
|
|
185
|
+
- Hidden controller relation materialization is not live by itself. If the
|
|
186
|
+
materialized result is rendered as a collection, relation provenance attaches
|
|
187
|
+
the collection dependency to that render-site. If scalar query output is
|
|
188
|
+
rendered, use `pluck`/query dependencies. If record attributes affect output,
|
|
189
|
+
those attribute reads are the lifecycle dependency. Otherwise the hidden
|
|
190
|
+
query is intentionally not a lifecycle invalidation surface.
|
|
191
|
+
|
|
192
|
+
Remaining follow-up: none for this phase.
|
|
193
|
+
|
|
194
|
+
## Phase 4: Replay Privacy And Size Gates
|
|
195
|
+
|
|
196
|
+
Goal: make replay inputs inspectable without retaining unrelated secrets.
|
|
197
|
+
|
|
198
|
+
Work:
|
|
199
|
+
|
|
200
|
+
- Keep raw replay values only where replay requires them.
|
|
201
|
+
- Redact or fingerprint values in debug/report surfaces.
|
|
202
|
+
- Add recipe-size assertions for controller page capture.
|
|
203
|
+
- Add a no-secret regression test using session keys that look like OAuth,
|
|
204
|
+
TOTP, CSRF, and redirect state but are never read by the rendered request.
|
|
205
|
+
|
|
206
|
+
Exit criteria:
|
|
207
|
+
|
|
208
|
+
- Controller recipes stay bounded when the Rack session contains unrelated
|
|
209
|
+
keys.
|
|
210
|
+
- Reports show source/key/class evidence without exposing raw private values.
|
|
211
|
+
- Security-sensitive unread session keys are absent from serialized recipes.
|
|
212
|
+
|
|
213
|
+
Status:
|
|
214
|
+
|
|
215
|
+
- Graph reports no longer embed full replay recipes. They report recipe kind,
|
|
216
|
+
target, replay keys, replay byte size, and replay digest so diagnostics can
|
|
217
|
+
reason about cost without exposing raw replay values.
|
|
218
|
+
- Controller page recipe tests now assert that unread security-shaped session
|
|
219
|
+
keys such as OAuth state, TOTP, CSRF, and redirect state are absent from
|
|
220
|
+
serialized replay payloads.
|
|
221
|
+
- Controller page replay payload size is asserted against observed inputs so
|
|
222
|
+
unrelated large session values cannot silently bloat recipes.
|
|
223
|
+
- Unread cookie values and unrelated request headers now have explicit
|
|
224
|
+
no-secret/size tests. Observed request headers needed for replay, such as
|
|
225
|
+
`request.user_agent`, are copied through a small allowlist; unread
|
|
226
|
+
authorization or CSRF-style headers stay out of replay payloads.
|
|
227
|
+
|
|
228
|
+
Remaining follow-up: none for this phase.
|
|
229
|
+
|
|
230
|
+
## Phase 5: Lobsters Acceptance Workload
|
|
231
|
+
|
|
232
|
+
Goal: use Lobsters as a generic Rails stress test, not as a runtime contract.
|
|
233
|
+
|
|
234
|
+
Work:
|
|
235
|
+
|
|
236
|
+
- Capture and replay representative anonymous and logged-in Lobsters pages.
|
|
237
|
+
- Cover session-backed user identity, cookie-backed tag filters, controller
|
|
238
|
+
ivars, and request-shaped pages.
|
|
239
|
+
- Prove public regions remain shareable where the render subtree is public.
|
|
240
|
+
- Prove identity-bound regions partition when user or cookie state changes.
|
|
241
|
+
- Add benchmark counters for ambient replay input count, recipe bytes, refused
|
|
242
|
+
boundaries, live deopts, render groups, and render counts.
|
|
243
|
+
|
|
244
|
+
Exit criteria:
|
|
245
|
+
|
|
246
|
+
- Lobsters acceptance tests pass without runtime special cases.
|
|
247
|
+
- Recipe sizes and render grouping evidence are reported.
|
|
248
|
+
- Refused boundaries are actionable and not hidden as benchmark failures.
|
|
249
|
+
|
|
250
|
+
Status:
|
|
251
|
+
|
|
252
|
+
- Lobsters surface tests pass for anonymous index, logged-in story discussion,
|
|
253
|
+
comment creation, and story voting without runtime special cases.
|
|
254
|
+
- Current Upkeep subscription acceptance tests replaced the stale
|
|
255
|
+
context-token/relay-region probes. They now assert current subscription
|
|
256
|
+
markers, refused-boundary absence, graph/report counters, replay byte bounds,
|
|
257
|
+
cookie-backed tag-filter replay, and logged-in stream partitioning.
|
|
258
|
+
- Hot-path Lobsters queries used by those pages were refactored from opaque
|
|
259
|
+
SQL strings to structural Active Record/Arel predicates or orders.
|
|
260
|
+
- Non-GET controller actions no longer run subscription observation. Mutation
|
|
261
|
+
requests still capture lifecycle change events and deliver, but opaque
|
|
262
|
+
mutation-only queries do not participate in render dependency capture.
|
|
263
|
+
- Benchmark metrics now expose an `upkeep_reactivity` summary outside the
|
|
264
|
+
integration suite. It reports stored subscription graph counts, frame and
|
|
265
|
+
dependency counts, replay recipe byte totals, ambient replay input counts,
|
|
266
|
+
refused-boundary reasons, live deoptimization reasons, render groups, and
|
|
267
|
+
render counts. The benchmark markdown reports surface the same counters.
|
|
268
|
+
|
|
269
|
+
Evidence:
|
|
270
|
+
|
|
271
|
+
- `bin/rails test test/integration` in `benchmark/lobsters-upkeep`: 6 runs, 74
|
|
272
|
+
assertions, 0 failures, 0 errors.
|
|
273
|
+
- `ruby -Itest test/benchmark/layout_test.rb` covers the benchmark metrics
|
|
274
|
+
summary shape and redaction.
|
|
275
|
+
|
|
276
|
+
Remaining follow-up: none for this phase.
|
|
277
|
+
|
|
278
|
+
## Phase 6: Documentation And Cleanup
|
|
279
|
+
|
|
280
|
+
Goal: make the supported Rails idioms clear to app authors.
|
|
281
|
+
|
|
282
|
+
Work:
|
|
283
|
+
|
|
284
|
+
- Update getting-started and identity docs with observed ambient input examples.
|
|
285
|
+
- Document refused-boundary examples for hidden controller state and opaque
|
|
286
|
+
controller relation shape.
|
|
287
|
+
- Remove transitional wording and code paths once phases land.
|
|
288
|
+
|
|
289
|
+
Exit criteria:
|
|
290
|
+
|
|
291
|
+
- Docs explain what stays live, what deopts, and what refuses.
|
|
292
|
+
- No roadmap item depends on maintaining both whole-session and observed-input
|
|
293
|
+
replay paths.
|
|
294
|
+
|
|
295
|
+
Status:
|
|
296
|
+
|
|
297
|
+
- Getting started now documents observed session/cookie/request inputs,
|
|
298
|
+
materialized relation provenance, scalar `pluck` query dependencies, and the
|
|
299
|
+
GET-only subscription capture boundary.
|
|
300
|
+
- Identity and sharing docs now explain replay privacy, request-header
|
|
301
|
+
allowlisting, opaque relation/pluck refused boundaries, and hidden controller
|
|
302
|
+
state refactor options.
|
|
303
|
+
- The roadmap no longer depends on a whole-session replay path or maintaining
|
|
304
|
+
both broad relation materialization and rendered relation provenance paths.
|
|
305
|
+
|
|
306
|
+
Remaining follow-up: none.
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Herb Roadmap
|
|
2
|
+
|
|
3
|
+
Upkeep should use Herb as the compile-time structural layer for Rails views, not
|
|
4
|
+
as the first source of runtime truth. Herb answers where updates can land. Rails
|
|
5
|
+
runtime observation answers which data and identity surfaces a subscriber
|
|
6
|
+
actually read.
|
|
7
|
+
|
|
8
|
+
This roadmap is deliberately phased. Each phase lands only after its gate
|
|
9
|
+
passes, and each new path replaces the previous implementation instead of
|
|
10
|
+
leaving duplicate code paths behind.
|
|
11
|
+
|
|
12
|
+
## Current Status
|
|
13
|
+
|
|
14
|
+
- Phase 1 landed: `TemplateManifest` is the single Herb coverage map, and the
|
|
15
|
+
Herb surface probe reports from it.
|
|
16
|
+
- Runtime alignment landed: graph frames and selected targets are checked
|
|
17
|
+
against manifest entries, with concrete deopt reasons.
|
|
18
|
+
- Fallback taxonomy landed: page fallbacks are explained as manifest mismatch,
|
|
19
|
+
missing render site, helper-hidden collection, multi-root partial, preloaded
|
|
20
|
+
plain data, or page dependency without a narrower frame.
|
|
21
|
+
- Phase 2 landed in the proof renderer: `SourceInstrumenter` owns render-site
|
|
22
|
+
wrapping and fragment-root marker insertion, and post-render partial root
|
|
23
|
+
tagging has been retired.
|
|
24
|
+
- Phase 3 landed in the proof renderer: page, fragment, and render-site frames
|
|
25
|
+
carry manifest path/fingerprint provenance, and alignment reports stale or
|
|
26
|
+
missing provenance as explicit deopt reasons.
|
|
27
|
+
- Phase 4 landed as a classifier: `ManifestDiff` uses `Herb.diff` to separate
|
|
28
|
+
no-op edits, content-only topology refreshes, topology rebuilds, and unsafe
|
|
29
|
+
full rebuilds.
|
|
30
|
+
- Phase 5 landed in the proof renderer: replay recipes carry the manifest
|
|
31
|
+
path/fingerprint for the frame or render site they replay.
|
|
32
|
+
- Phase 6 landed as a report: `DeveloperReport` turns manifest coverage and
|
|
33
|
+
proof fallback reasons into actionable template guidance.
|
|
34
|
+
- Production rollout is next: move Herb from proof renderer to real ActionView,
|
|
35
|
+
use diff classification for manifest cache updates, use manifest addresses in
|
|
36
|
+
replay, add performance gates, then make Herb the default and only
|
|
37
|
+
instrumentation path if the gates pass.
|
|
38
|
+
- Production Phase A landed: real ActionView ERB templates are instrumented
|
|
39
|
+
through `SourceInstrumenter`, Rails frame payloads carry manifest provenance,
|
|
40
|
+
and collection render-site IDs come from Herb source offsets.
|
|
41
|
+
- Production Phase B landed: `ManifestCache` stores manifests by template path
|
|
42
|
+
and source digest, classifying changed source with `ManifestDiff`.
|
|
43
|
+
- Production Phase C landed: replay recipes render matching manifest-backed
|
|
44
|
+
targets directly, with DOM extraction kept only as a mismatch fallback.
|
|
45
|
+
- Production Phase D landed as an internal gate: `PerformanceGate` records
|
|
46
|
+
capture time, allocations, replay time, replay SQL count, graph size, payload
|
|
47
|
+
bytes, and direct manifest replay coverage.
|
|
48
|
+
- Default Herb cutover landed: Herb is a runtime dependency, runtime Herb files
|
|
49
|
+
ship with the gem, and the proof-only local `herb_loader` path was removed.
|
|
50
|
+
|
|
51
|
+
## Baseline
|
|
52
|
+
|
|
53
|
+
Before making Herb critical, keep measuring the current runtime:
|
|
54
|
+
|
|
55
|
+
- selected target distribution: fragment, render site, page
|
|
56
|
+
- page fallback reasons
|
|
57
|
+
- subscription capture time and allocations
|
|
58
|
+
- replay time and SQL count
|
|
59
|
+
- Turbo Stream payload bytes
|
|
60
|
+
- graph size: nodes, edges, dependencies
|
|
61
|
+
- DOM patch correctness against a fresh full render
|
|
62
|
+
|
|
63
|
+
These numbers are the comparison point for every phase.
|
|
64
|
+
|
|
65
|
+
## Phase 1: Template Manifest
|
|
66
|
+
|
|
67
|
+
Hypothesis: Herb can statically describe the useful update addresses in Rails
|
|
68
|
+
templates.
|
|
69
|
+
|
|
70
|
+
Implementation:
|
|
71
|
+
|
|
72
|
+
- Parse each supported template with Herb.
|
|
73
|
+
- Emit a reusable template manifest with parse status, root shape, render sites,
|
|
74
|
+
helper-lowered elements, and frontend tag targets.
|
|
75
|
+
- Keep the existing Herb surface probe as a report over the manifest, not a
|
|
76
|
+
second visitor implementation.
|
|
77
|
+
|
|
78
|
+
Gate:
|
|
79
|
+
|
|
80
|
+
- Strict parse failures stay at zero for maintained benchmark templates.
|
|
81
|
+
- Herb discovers every render site that the current proof depends on.
|
|
82
|
+
- Single-root partial detection stays deterministic.
|
|
83
|
+
- The probe report remains compatible enough to compare against existing
|
|
84
|
+
`results/herb_surface.json`.
|
|
85
|
+
|
|
86
|
+
Worth proven when the manifest gives a reliable coverage map and clear reasons
|
|
87
|
+
for narrow-update eligibility or fallback.
|
|
88
|
+
|
|
89
|
+
## Phase 2: Compile-Time Instrumentation
|
|
90
|
+
|
|
91
|
+
Hypothesis: Herb can insert stable frame and render-site markers without
|
|
92
|
+
changing Rails rendering semantics.
|
|
93
|
+
|
|
94
|
+
Implementation:
|
|
95
|
+
|
|
96
|
+
- Use the manifest to add `data-upkeep-frame` to fragment roots.
|
|
97
|
+
- Wrap render sites with `upkeep-render-site` markers.
|
|
98
|
+
- Run only in a test or shadow path until output equivalence is proven.
|
|
99
|
+
|
|
100
|
+
Gate:
|
|
101
|
+
|
|
102
|
+
- Rendered HTML differs only by expected Upkeep markers.
|
|
103
|
+
- Frame IDs are stable across repeated renders.
|
|
104
|
+
- There are no missing or duplicate frame IDs.
|
|
105
|
+
- ActionView helpers, locals, layouts, and partial rendering continue to behave
|
|
106
|
+
normally.
|
|
107
|
+
|
|
108
|
+
Worth proven when DOM tagging can be deterministic and source-derived instead
|
|
109
|
+
of inferred after rendering.
|
|
110
|
+
|
|
111
|
+
## Phase 3: Runtime Attachment
|
|
112
|
+
|
|
113
|
+
Hypothesis: runtime observation can attach reads to manifest-defined frame slots
|
|
114
|
+
more cheaply and precisely than rediscovering structure at render time.
|
|
115
|
+
|
|
116
|
+
Implementation:
|
|
117
|
+
|
|
118
|
+
- Attach manifest path and fingerprint provenance to runtime frames.
|
|
119
|
+
- Run the current recorder and manifest-backed recorder side by side.
|
|
120
|
+
- Deliver using the current recorder until divergence is understood.
|
|
121
|
+
- Deopt to page frame when runtime shape does not match the manifest.
|
|
122
|
+
|
|
123
|
+
Gate:
|
|
124
|
+
|
|
125
|
+
- Selected targets match current behavior, except where manifest mode is
|
|
126
|
+
strictly narrower and the DOM patch proof passes.
|
|
127
|
+
- Identity signatures match current behavior.
|
|
128
|
+
- Mismatches have concrete deopt reasons.
|
|
129
|
+
- Runtime capture cost and graph size do not regress.
|
|
130
|
+
|
|
131
|
+
Worth proven when correctness holds and runtime structure discovery shrinks.
|
|
132
|
+
|
|
133
|
+
## Phase 4: Diff-Driven Manifest Updates
|
|
134
|
+
|
|
135
|
+
Hypothesis: `Herb.diff` can classify template edits well enough to avoid broad
|
|
136
|
+
manifest invalidation and explain topology changes.
|
|
137
|
+
|
|
138
|
+
Implementation:
|
|
139
|
+
|
|
140
|
+
- Diff old and new template source.
|
|
141
|
+
- Keep frame topology when edits only touch text or attributes.
|
|
142
|
+
- Rebuild affected manifest entries when ERB, roots, render sites, or
|
|
143
|
+
containment change.
|
|
144
|
+
- Fall back to a full template rebuild when the diff is ambiguous.
|
|
145
|
+
|
|
146
|
+
Gate:
|
|
147
|
+
|
|
148
|
+
- No stale manifest after template edits.
|
|
149
|
+
- No missed boundary-changing edits.
|
|
150
|
+
- Diff classifications explain the rebuild scope.
|
|
151
|
+
|
|
152
|
+
Worth proven when template edits become cheap and understandable in development
|
|
153
|
+
and CI.
|
|
154
|
+
|
|
155
|
+
## Phase 5: Manifest Replay
|
|
156
|
+
|
|
157
|
+
Hypothesis: source-derived render-site IDs and manifest recipes make target
|
|
158
|
+
replay cheaper and less fragile.
|
|
159
|
+
|
|
160
|
+
Implementation:
|
|
161
|
+
|
|
162
|
+
- Reference manifest entries from replay recipes.
|
|
163
|
+
- Replay fragments, render sites, and pages against stable source-derived
|
|
164
|
+
addresses.
|
|
165
|
+
- Keep full-render extraction as the correctness oracle while this is in
|
|
166
|
+
shadow mode.
|
|
167
|
+
|
|
168
|
+
Gate:
|
|
169
|
+
|
|
170
|
+
- Replayed target HTML equals the matching target from a fresh full render.
|
|
171
|
+
- Replay time, SQL count, and payload size are lower or unchanged.
|
|
172
|
+
- Subscriber identity partitioning still prevents payload leakage.
|
|
173
|
+
|
|
174
|
+
Worth proven when narrow replay becomes stable enough to reduce full-page
|
|
175
|
+
replay dependence.
|
|
176
|
+
|
|
177
|
+
## Phase 6: Developer Report
|
|
178
|
+
|
|
179
|
+
Hypothesis: Herb can make Upkeep adoption easier by explaining view coverage and
|
|
180
|
+
fallbacks.
|
|
181
|
+
|
|
182
|
+
Implementation:
|
|
183
|
+
|
|
184
|
+
- Emit a report per app showing narrow-update eligibility, blocked templates,
|
|
185
|
+
render-site coverage, helper-hidden regions, and suggested fixes.
|
|
186
|
+
- Track before/after coverage as templates are adjusted.
|
|
187
|
+
|
|
188
|
+
Gate:
|
|
189
|
+
|
|
190
|
+
- The report identifies actionable template changes.
|
|
191
|
+
- Applying those changes increases fragment or render-site coverage.
|
|
192
|
+
- Fewer proof cases fall back to page frames.
|
|
193
|
+
|
|
194
|
+
Worth proven when Rails developers can improve Upkeep behavior without knowing
|
|
195
|
+
the internal graph model.
|
|
196
|
+
|
|
197
|
+
## Production Phase A: ActionView Integration
|
|
198
|
+
|
|
199
|
+
Hypothesis: real Rails templates can use the same Herb source instrumentation
|
|
200
|
+
as the proof renderer without changing ActionView semantics.
|
|
201
|
+
|
|
202
|
+
Implementation:
|
|
203
|
+
|
|
204
|
+
- Build manifests from Rails-resolved template source.
|
|
205
|
+
- Apply `SourceInstrumenter` before ActionView compiles supported ERB
|
|
206
|
+
templates.
|
|
207
|
+
- Attach manifest path and fingerprint to page, fragment, and render-site
|
|
208
|
+
frames in the Rails capture path.
|
|
209
|
+
- Remove any ActionView-only duplicate marker path once the Herb path passes.
|
|
210
|
+
|
|
211
|
+
Gate:
|
|
212
|
+
|
|
213
|
+
- Existing ActionView capture tests still pass.
|
|
214
|
+
- Rendered Rails HTML has exactly the expected Upkeep markers.
|
|
215
|
+
- Target selection and replay correctness match the current Rails behavior.
|
|
216
|
+
- Helpers, locals, controller requests, collection rendering, and layouts keep
|
|
217
|
+
working.
|
|
218
|
+
|
|
219
|
+
Worth proven when the real gem runtime uses Herb for template structure, not
|
|
220
|
+
just the proof renderer.
|
|
221
|
+
|
|
222
|
+
## Production Phase B: Manifest Cache Updates
|
|
223
|
+
|
|
224
|
+
Hypothesis: `ManifestDiff` can drive an actual manifest cache update strategy
|
|
225
|
+
without stale topology.
|
|
226
|
+
|
|
227
|
+
Implementation:
|
|
228
|
+
|
|
229
|
+
- Add a manifest cache keyed by virtual template path and source digest.
|
|
230
|
+
- Use `ManifestDiff` when a cached source changes.
|
|
231
|
+
- Reuse stable topology for content-only changes.
|
|
232
|
+
- Rebuild manifest entries for root, render-site, ERB, or parse-affecting
|
|
233
|
+
changes.
|
|
234
|
+
- Emit the diff classification in developer/debug reports.
|
|
235
|
+
|
|
236
|
+
Gate:
|
|
237
|
+
|
|
238
|
+
- Template edit tests prove no stale manifest after content-only and
|
|
239
|
+
topology-changing edits.
|
|
240
|
+
- Boundary-changing edits are never classified as safe topology reuse.
|
|
241
|
+
- Cache invalidation explains every rebuild or fallback.
|
|
242
|
+
|
|
243
|
+
Worth proven when development reloads and repeated captures avoid broad
|
|
244
|
+
manifest rebuilding while staying correct.
|
|
245
|
+
|
|
246
|
+
## Production Phase C: Manifest Replay Optimization
|
|
247
|
+
|
|
248
|
+
Hypothesis: replay can use manifest addresses to avoid avoidable full-document
|
|
249
|
+
work while keeping full-render extraction as the oracle during the transition.
|
|
250
|
+
|
|
251
|
+
Implementation:
|
|
252
|
+
|
|
253
|
+
- Reference manifest entries from Rails replay recipes.
|
|
254
|
+
- Prefer manifest-known fragment and render-site addresses when extracting
|
|
255
|
+
replay targets.
|
|
256
|
+
- Keep full-render extraction as a comparison oracle until every replay target
|
|
257
|
+
matches.
|
|
258
|
+
- Remove any duplicate replay path after the manifest path matches.
|
|
259
|
+
|
|
260
|
+
Gate:
|
|
261
|
+
|
|
262
|
+
- Fragment, render-site, and page replay output still equals fresh full render
|
|
263
|
+
target HTML.
|
|
264
|
+
- Replay SQL count, replay time, allocations, or extraction cost improves or
|
|
265
|
+
remains neutral.
|
|
266
|
+
- Identity partitioning and authorization surfaces remain unchanged.
|
|
267
|
+
|
|
268
|
+
Worth proven when manifest replay is cheaper or more explainable without
|
|
269
|
+
weakening correctness.
|
|
270
|
+
|
|
271
|
+
## Production Phase D: Performance Gates
|
|
272
|
+
|
|
273
|
+
Hypothesis: Herb's source-derived structure reduces runtime work enough to
|
|
274
|
+
justify making it the default.
|
|
275
|
+
|
|
276
|
+
Implementation:
|
|
277
|
+
|
|
278
|
+
- Add benchmark measurements for capture time, allocations, replay time, SQL
|
|
279
|
+
count, graph size, and payload bytes.
|
|
280
|
+
- Compare the pre-Herb runtime path against the Herb path while both still
|
|
281
|
+
exist.
|
|
282
|
+
- Report neutral, improved, and regressed metrics explicitly.
|
|
283
|
+
|
|
284
|
+
Gate:
|
|
285
|
+
|
|
286
|
+
- Correctness proofs pass.
|
|
287
|
+
- Capture, replay, graph, SQL, and payload metrics do not regress beyond an
|
|
288
|
+
intentional and documented threshold.
|
|
289
|
+
- Any regression has an explanation and a follow-up before Herb becomes the
|
|
290
|
+
only path.
|
|
291
|
+
|
|
292
|
+
Worth proven when Herb is not just cleaner, but also measurably safe to enable
|
|
293
|
+
by default.
|
|
294
|
+
|
|
295
|
+
## Default Herb Cutover
|
|
296
|
+
|
|
297
|
+
Hypothesis: after production phases A-D pass, Upkeep should have one template
|
|
298
|
+
structure path: Herb.
|
|
299
|
+
|
|
300
|
+
Implementation:
|
|
301
|
+
|
|
302
|
+
- Make Herb instrumentation the default behavior.
|
|
303
|
+
- Remove stale non-Herb marker, render-site, replay, and report paths rather
|
|
304
|
+
than keeping double paths.
|
|
305
|
+
- Keep explicit deopts for unsupported templates instead of silently falling
|
|
306
|
+
back to an untracked structural model.
|
|
307
|
+
|
|
308
|
+
Gate:
|
|
309
|
+
|
|
310
|
+
- Full test suite, ActionView capture tests, end-to-end proof, identity proof,
|
|
311
|
+
auth proof, manifest diff tests, developer report tests, and performance
|
|
312
|
+
gates pass.
|
|
313
|
+
- No duplicate source-instrumentation or post-render marker path remains.
|
|
314
|
+
- The gem packaging decision is explicit: Herb support either ships as the
|
|
315
|
+
runtime path or is cleanly excluded with the feature disabled.
|
|
316
|
+
|
|
317
|
+
Worth proven when Herb becomes Upkeep's single structural source of truth.
|
|
318
|
+
|
|
319
|
+
## Cleanup Rule
|
|
320
|
+
|
|
321
|
+
Each phase must retire or fold the previous experimental path into the new
|
|
322
|
+
abstraction before it is committed. Temporary shadow paths are acceptable only
|
|
323
|
+
inside the phase branch and must either become the main path or be removed
|
|
324
|
+
before the phase commit.
|