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.

Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +424 -0
  4. data/docs/architecture/ambient-inputs-roadmap.md +306 -0
  5. data/docs/architecture/herb-roadmap.md +324 -0
  6. data/docs/architecture/identity-and-sharing.md +187 -0
  7. data/docs/architecture/query-dependencies.md +230 -0
  8. data/docs/cost-model-roadmap.md +703 -0
  9. data/docs/guides/getting-started.md +282 -0
  10. data/docs/handoff-2026-05-15.md +230 -0
  11. data/docs/production_roadmap.md +372 -0
  12. data/docs/shared-warm-scale-roadmap.md +214 -0
  13. data/docs/single-subscriber-cold-roadmap.md +192 -0
  14. data/docs/testing.md +113 -0
  15. data/lib/generators/upkeep/install/install_generator.rb +90 -0
  16. data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +31 -0
  17. data/lib/generators/upkeep/install/templates/subscription.js +107 -0
  18. data/lib/generators/upkeep/install/templates/upkeep.rb +6 -0
  19. data/lib/upkeep/active_record_query.rb +294 -0
  20. data/lib/upkeep/capture/request.rb +150 -0
  21. data/lib/upkeep/dag/subscription_shape.rb +244 -0
  22. data/lib/upkeep/dag.rb +370 -0
  23. data/lib/upkeep/delivery/action_cable_adapter.rb +43 -0
  24. data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
  25. data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
  26. data/lib/upkeep/delivery/transport.rb +194 -0
  27. data/lib/upkeep/delivery/turbo_streams.rb +275 -0
  28. data/lib/upkeep/delivery.rb +7 -0
  29. data/lib/upkeep/dependencies.rb +466 -0
  30. data/lib/upkeep/herb/developer_report.rb +116 -0
  31. data/lib/upkeep/herb/manifest_cache.rb +83 -0
  32. data/lib/upkeep/herb/manifest_diff.rb +183 -0
  33. data/lib/upkeep/herb/source_instrumenter.rb +84 -0
  34. data/lib/upkeep/herb/template_manifest.rb +377 -0
  35. data/lib/upkeep/invalidation/collection_append.rb +84 -0
  36. data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
  37. data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
  38. data/lib/upkeep/invalidation/collection_remove.rb +57 -0
  39. data/lib/upkeep/invalidation/planner.rb +341 -0
  40. data/lib/upkeep/invalidation.rb +7 -0
  41. data/lib/upkeep/rails/action_view_capture.rb +765 -0
  42. data/lib/upkeep/rails/cable/channel.rb +108 -0
  43. data/lib/upkeep/rails/cable/subscriber_identity.rb +214 -0
  44. data/lib/upkeep/rails/cable.rb +4 -0
  45. data/lib/upkeep/rails/client_subscription.rb +37 -0
  46. data/lib/upkeep/rails/configuration.rb +57 -0
  47. data/lib/upkeep/rails/controller_runtime.rb +137 -0
  48. data/lib/upkeep/rails/install.rb +28 -0
  49. data/lib/upkeep/rails/railtie.rb +36 -0
  50. data/lib/upkeep/rails/replay.rb +176 -0
  51. data/lib/upkeep/rails/testing.rb +36 -0
  52. data/lib/upkeep/rails.rb +276 -0
  53. data/lib/upkeep/replay.rb +408 -0
  54. data/lib/upkeep/runtime.rb +1075 -0
  55. data/lib/upkeep/shared_streams.rb +72 -0
  56. data/lib/upkeep/subscriptions/active_record_store.rb +292 -0
  57. data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +291 -0
  58. data/lib/upkeep/subscriptions/active_registry.rb +93 -0
  59. data/lib/upkeep/subscriptions/async_durable_writer.rb +136 -0
  60. data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
  61. data/lib/upkeep/subscriptions/layered_reverse_index.rb +122 -0
  62. data/lib/upkeep/subscriptions/persistent_reverse_index.rb +144 -0
  63. data/lib/upkeep/subscriptions/registrar.rb +36 -0
  64. data/lib/upkeep/subscriptions/reverse_index.rb +294 -0
  65. data/lib/upkeep/subscriptions/shape.rb +116 -0
  66. data/lib/upkeep/subscriptions/store.rb +159 -0
  67. data/lib/upkeep/subscriptions.rb +7 -0
  68. data/lib/upkeep/targeting.rb +135 -0
  69. data/lib/upkeep/version.rb +5 -0
  70. data/lib/upkeep-rails.rb +3 -0
  71. data/lib/upkeep.rb +14 -0
  72. data/upkeep-rails.gemspec +53 -0
  73. 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.