paper_trail_diff 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -0
  3. data/QUICKSTART.md +302 -0
  4. data/README.md +258 -19
  5. data/lib/paper_trail_diff/activity_boundary.rb +34 -3
  6. data/lib/paper_trail_diff/activity_child_candidate_loader.rb +73 -0
  7. data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +261 -45
  8. data/lib/paper_trail_diff/activity_history.rb +136 -0
  9. data/lib/paper_trail_diff/activity_range.rb +28 -12
  10. data/lib/paper_trail_diff/activity_snapshot_delta.rb +64 -0
  11. data/lib/paper_trail_diff/activity_snapshot_sequence.rb +3 -3
  12. data/lib/paper_trail_diff/activity_timeline_builder.rb +54 -45
  13. data/lib/paper_trail_diff/activity_version_collector.rb +14 -17
  14. data/lib/paper_trail_diff/association_diff_traversal.rb +184 -0
  15. data/lib/paper_trail_diff/branch_snapshot_refresher.rb +5 -3
  16. data/lib/paper_trail_diff/collection_comparator.rb +155 -0
  17. data/lib/paper_trail_diff/collection_identity_index.rb +53 -0
  18. data/lib/paper_trail_diff/collection_transition.rb +25 -0
  19. data/lib/paper_trail_diff/comparison_batch.rb +151 -0
  20. data/lib/paper_trail_diff/engine.rb +5 -45
  21. data/lib/paper_trail_diff/errors.rb +9 -0
  22. data/lib/paper_trail_diff/historical_snapshot_store.rb +55 -3
  23. data/lib/paper_trail_diff/instrumentation.rb +30 -0
  24. data/lib/paper_trail_diff/live_endpoint_batch_loader.rb +133 -0
  25. data/lib/paper_trail_diff/live_endpoint_provider.rb +48 -0
  26. data/lib/paper_trail_diff/live_graph_collector.rb +52 -0
  27. data/lib/paper_trail_diff/paper_trail_adapter.rb +61 -34
  28. data/lib/paper_trail_diff/preloaded_endpoint_batch_loader.rb +48 -0
  29. data/lib/paper_trail_diff/prepared_edge_loader.rb +54 -17
  30. data/lib/paper_trail_diff/prepared_history.rb +14 -4
  31. data/lib/paper_trail_diff/prepared_history_loader.rb +17 -9
  32. data/lib/paper_trail_diff/prepared_record_index.rb +138 -10
  33. data/lib/paper_trail_diff/snapshot.rb +66 -3
  34. data/lib/paper_trail_diff/snapshot_traversal.rb +97 -0
  35. data/lib/paper_trail_diff/step.rb +9 -0
  36. data/lib/paper_trail_diff/support.rb +18 -0
  37. data/lib/paper_trail_diff/time_activity_timeline_builder.rb +111 -0
  38. data/lib/paper_trail_diff/time_range.rb +85 -0
  39. data/lib/paper_trail_diff/time_version_range.rb +53 -0
  40. data/lib/paper_trail_diff/timeline_builder.rb +13 -7
  41. data/lib/paper_trail_diff/timeline_range.rb +72 -0
  42. data/lib/paper_trail_diff/traversal.rb +80 -0
  43. data/lib/paper_trail_diff/traversal_emitter.rb +42 -0
  44. data/lib/paper_trail_diff/traversal_entry.rb +94 -0
  45. data/lib/paper_trail_diff/version.rb +1 -1
  46. data/lib/paper_trail_diff/version_association_candidate_scope.rb +73 -0
  47. data/lib/paper_trail_diff.rb +84 -13
  48. data/sig/generated/paper_trail_diff/activity_boundary.rbs +17 -2
  49. data/sig/generated/paper_trail_diff/activity_child_candidate_loader.rbs +40 -0
  50. data/sig/generated/paper_trail_diff/activity_event_snapshot_refresher.rbs +49 -8
  51. data/sig/generated/paper_trail_diff/activity_history.rbs +79 -0
  52. data/sig/generated/paper_trail_diff/activity_range.rbs +14 -5
  53. data/sig/generated/paper_trail_diff/activity_snapshot_delta.rbs +33 -0
  54. data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +2 -2
  55. data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +15 -10
  56. data/sig/generated/paper_trail_diff/activity_version_collector.rbs +2 -5
  57. data/sig/generated/paper_trail_diff/association_diff_traversal.rbs +58 -0
  58. data/sig/generated/paper_trail_diff/branch_snapshot_refresher.rbs +2 -2
  59. data/sig/generated/paper_trail_diff/collection_comparator.rbs +59 -0
  60. data/sig/generated/paper_trail_diff/collection_identity_index.rbs +29 -0
  61. data/sig/generated/paper_trail_diff/collection_transition.rbs +18 -0
  62. data/sig/generated/paper_trail_diff/comparison_batch.rbs +65 -0
  63. data/sig/generated/paper_trail_diff/engine.rbs +0 -12
  64. data/sig/generated/paper_trail_diff/errors.rbs +12 -0
  65. data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +22 -2
  66. data/sig/generated/paper_trail_diff/instrumentation.rbs +12 -0
  67. data/sig/generated/paper_trail_diff/live_endpoint_batch_loader.rbs +52 -0
  68. data/sig/generated/paper_trail_diff/live_endpoint_provider.rbs +23 -0
  69. data/sig/generated/paper_trail_diff/live_graph_collector.rbs +27 -0
  70. data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +21 -10
  71. data/sig/generated/paper_trail_diff/preloaded_endpoint_batch_loader.rbs +24 -0
  72. data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +22 -8
  73. data/sig/generated/paper_trail_diff/prepared_history.rbs +6 -0
  74. data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +4 -2
  75. data/sig/generated/paper_trail_diff/prepared_record_index.rbs +61 -6
  76. data/sig/generated/paper_trail_diff/snapshot.rbs +37 -2
  77. data/sig/generated/paper_trail_diff/snapshot_traversal.rbs +29 -0
  78. data/sig/generated/paper_trail_diff/step.rbs +7 -0
  79. data/sig/generated/paper_trail_diff/support.rbs +6 -0
  80. data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +46 -0
  81. data/sig/generated/paper_trail_diff/time_range.rbs +41 -0
  82. data/sig/generated/paper_trail_diff/time_version_range.rbs +27 -0
  83. data/sig/generated/paper_trail_diff/timeline_builder.rbs +6 -5
  84. data/sig/generated/paper_trail_diff/timeline_range.rbs +41 -0
  85. data/sig/generated/paper_trail_diff/traversal.rbs +41 -0
  86. data/sig/generated/paper_trail_diff/traversal_emitter.rbs +16 -0
  87. data/sig/generated/paper_trail_diff/traversal_entry.rbs +52 -0
  88. data/sig/generated/paper_trail_diff/version_association_candidate_scope.rbs +32 -0
  89. data/sig/generated/paper_trail_diff.rbs +24 -8
  90. metadata +49 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e85c7b69b7fe2c0c5b8fac36c28e94f0e8cb3e24bb0e84fa312a8c8c4de9a244
4
- data.tar.gz: fa7118652d053ac5f882a6382c412534ca3c9fdbc3b613192652e7a3080fa205
3
+ metadata.gz: 4d60569584c3d30bd35c62c1f0f699fd8859545d81f873c926dfdd8268aed3df
4
+ data.tar.gz: 4961a77099927fff3476641b0409cb234e1f398e9b84e9cb3cc9e11b2d580064
5
5
  SHA512:
6
- metadata.gz: 04d797a11eadc6702892f0e581da107bc6e1a4f2c41f7bf9759bddc1f01534b29389eeb12830b945be378c5cebe4ae3336235326ce21d7423b1f10e3bc48db28
7
- data.tar.gz: 15cf3d7c0b099e2fb4c4db6db84c64718afd121d42fc91f8f63e34513d68db54f2c1365354eed648faef435e4d6a4e1645115978f80cbf2834e4e382efb3c773
6
+ metadata.gz: 2cf13bffaca3d199b2332b0e3d1c7c7c00ef7c6f82a4c9e5a75c51f376889ff57c60e1bfab1f578a7122e972698318d2720bce5f8da2755abea50aebf6e7083e
7
+ data.tar.gz: a071cd71f7fb766105eda1e018991ed8030885822da23382404797984b265818c0012378aea5912a507fb7bc24e9294331201b0cf1918860df3d9613476d580d
data/CHANGELOG.md CHANGED
@@ -3,6 +3,68 @@
3
3
  All notable changes to this project will be documented in this file. The
4
4
  project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.3.0] - 2026-08-10
7
+
8
+ ### Added
9
+
10
+ - Add `within:` time-range selection to checkpoint timelines, activity
11
+ timelines, and combined analysis, with inclusive/exclusive end handling and
12
+ explicit errors when a final mutation cannot be reconstructed.
13
+ - Add `compare_many` for collection reports, with batched current-root loading,
14
+ bounded live-association preloading, cross-root prepared history, immutable
15
+ identity-keyed results, and scale-invariant query regression coverage.
16
+ - Add opt-in reuse of fully preloaded current endpoints and namespaced
17
+ ActiveSupport runtime instrumentation without automatic logging, including
18
+ end-to-end activity-timeline duration and step counts.
19
+
20
+ ### Changed
21
+
22
+ - Teach timeline filtering through the shared `Step#empty?` protocol in the
23
+ README and quickstart examples.
24
+ - Reuse the live graph loaded by `compare_many` while preparing historical
25
+ association state, and defer root version loading until an edge needs it.
26
+ - Reuse collection identity positions and adjacent transition hints so activity
27
+ steps compare only the changed member, and resolve direct nested collection
28
+ owners by foreign key instead of scanning every parent snapshot.
29
+ - Reuse prepared predecessor and successor scalar states for isolated activity
30
+ updates instead of reifying and deserializing each PaperTrail event again.
31
+ - Preserve both sides of nested collection membership moves by using the
32
+ general comparator when one event changes multiple parent snapshots.
33
+ - Preserve per-owner `limit`, `offset`, and owner-dependent association scopes
34
+ in `compare_many` while retaining batched preloading for safe branches.
35
+ - Replace lifetime association-identity materialization with an indexed start-state
36
+ checkpoint plus post-boundary activity and current members.
37
+ - Bound historical activity-child candidates at the selected end so membership
38
+ changes after the range cannot introduce unrelated empty timeline steps.
39
+ - Decode compatible prepared scalar version payloads without constructing an
40
+ intermediate Active Record object, with reification fallback for unsafe schemas.
41
+ - Reuse internally owned frozen collection arrays instead of defensively copying
42
+ them a second time during immutable activity reconstruction.
43
+ - Replace pre-existing versions-relation ordering when selecting a time range's
44
+ immediate trailing reconstruction boundary.
45
+ - Limit automatic CI to pull requests and `main` pushes, and cancel superseded
46
+ runs for the same pull request or ref.
47
+
48
+ ### Fixed
49
+
50
+ - Supply TZInfo's timezone database to the test bundle so zoned-time coverage
51
+ runs on Windows as well as systems with a native zoneinfo database.
52
+
53
+ ## [0.2.0] - 2026-08-09
54
+
55
+ ### Added
56
+
57
+ - Add deterministic `Diff#each_entry` and `Diff#each_change` traversal with
58
+ immutable `TraversalEntry` values for renderers, counters, exports, and
59
+ notifications.
60
+ - Add immutable version metadata and record references to activity boundaries.
61
+ - Give checkpoint `Step` objects `from_boundary` and `to_boundary` readers and
62
+ give both timeline step types an `empty?` predicate.
63
+ - Add a Rails-focused quickstart with minimal endpoint, timeline, ignore, and
64
+ association examples.
65
+ - Execute selected README and quickstart examples in isolated core and PT-AT
66
+ test sessions so documented behavior cannot silently drift.
67
+
6
68
  ## [0.1.0] - 2026-08-09
7
69
 
8
70
  ### Added
data/QUICKSTART.md ADDED
@@ -0,0 +1,302 @@
1
+ # Quickstart
2
+
3
+ This guide gets `paper_trail_diff` running in a Rails application and shows the
4
+ smallest useful examples. Ruby 3.1 or newer and PaperTrail 16 or 17 are
5
+ supported.
6
+
7
+ ## 1. Install the gem
8
+
9
+ From the Rails application directory:
10
+
11
+ ```console
12
+ bundle add paper_trail_diff
13
+ bin/rails generate paper_trail:install --with-changes
14
+ bin/rails db:migrate
15
+ ```
16
+
17
+ Skip the generator if the application already has a PaperTrail `versions`
18
+ table. The `object_changes` column created by `--with-changes` is recommended
19
+ for efficient activity analysis, although endpoint comparison is based on
20
+ reified state rather than a PaperTrail changeset.
21
+
22
+ ## 2. Version a model
23
+
24
+ ```ruby
25
+ # app/models/article.rb
26
+ class Article < ApplicationRecord
27
+ has_paper_trail
28
+ end
29
+ ```
30
+
31
+ Restart the Rails console after changing the Gemfile or an initializer.
32
+
33
+ ## 3. Create a little history
34
+
35
+ Run this in `bin/rails console`:
36
+
37
+ <!-- executable:quickstart-history -->
38
+ ```ruby
39
+ article = Article.create!(title: "Draft")
40
+
41
+ article.update!(title: "Published")
42
+ draft_version = article.versions.last
43
+
44
+ article.update!(title: "Final")
45
+ published_version = article.versions.last
46
+ ```
47
+
48
+ The variable names are intentional. PaperTrail versions contain the state
49
+ *before* their event:
50
+
51
+ ```ruby
52
+ draft_version.reify.title # => "Draft"
53
+ published_version.reify.title # => "Published"
54
+ article.title # => "Final" (current database state)
55
+ ```
56
+
57
+ ## 4. Compare two states
58
+
59
+ <!-- executable:quickstart-compare -->
60
+ ```ruby
61
+ diff = PaperTrailDiff.compare(draft_version, published_version)
62
+
63
+ diff.empty? # => false
64
+ diff.attributes["title"].from # => "Draft"
65
+ diff.attributes["title"].to # => "Published"
66
+
67
+ diff.to_h
68
+ # => {
69
+ # record_presence_change: nil,
70
+ # attributes: {
71
+ # "title" => { from: "Draft", to: "Published" }
72
+ # },
73
+ # associations: {}
74
+ # }
75
+ ```
76
+
77
+ `compare` reports only the net difference between its endpoints. It does not
78
+ report intermediate changes.
79
+
80
+ ### Compare a version with current state
81
+
82
+ Pass the persisted record explicitly when the second endpoint should be the
83
+ current database state:
84
+
85
+ <!-- executable:quickstart-current -->
86
+ ```ruby
87
+ diff = PaperTrailDiff.compare(published_version, article)
88
+
89
+ diff.attributes["title"].to # => "Final"
90
+ ```
91
+
92
+ The record must be persisted, not destroyed, and have no unsaved changes.
93
+ Current state is never inferred automatically.
94
+
95
+ ## 5. Build a checkpoint timeline
96
+
97
+ Create one more historical endpoint:
98
+
99
+ <!-- executable:quickstart-timeline-state -->
100
+ ```ruby
101
+ article.update!(title: "Archived")
102
+ final_version = article.versions.last # reifies to "Final"
103
+ ```
104
+
105
+ Then compare every adjacent root version:
106
+
107
+ <!-- executable:quickstart-timeline -->
108
+ ```ruby
109
+ steps = PaperTrailDiff.timeline(
110
+ article,
111
+ from: draft_version,
112
+ to: final_version
113
+ )
114
+
115
+ steps.map do |step|
116
+ change = step.diff.attributes["title"]
117
+ [change.from, change.to]
118
+ end
119
+ # => [["Draft", "Published"], ["Published", "Final"]]
120
+
121
+ visible_steps = steps.reject(&:empty?)
122
+ ```
123
+
124
+ Empty steps remain in the timeline. Filter them only when the application's
125
+ display does not need every version boundary.
126
+
127
+ ### Select mutations by time
128
+
129
+ Use `within:` when the application has a reporting window rather than saved
130
+ endpoint versions:
131
+
132
+ <!-- executable:quickstart-time-range -->
133
+ ```ruby
134
+ window = draft_version.created_at...final_version.created_at
135
+ ranged_steps = PaperTrailDiff.timeline(article, within: window)
136
+
137
+ ranged_steps.map do |step|
138
+ change = step.diff.attributes["title"]
139
+ [change.from, change.to]
140
+ end
141
+ # => [["Draft", "Published"], ["Published", "Final"]]
142
+ ```
143
+
144
+ The half-open range selects the mutations recorded by `draft_version` and
145
+ `published_version`. The excluded `final_version` is still used as the trailing
146
+ boundary needed to reveal the last selected mutation. PaperTrail versions are
147
+ pre-change snapshots, so a range containing mutations must have a later root
148
+ version. The gem raises `PaperTrailDiff::IncompleteTimeRangeError` instead of
149
+ assuming that the current record is the missing endpoint.
150
+
151
+ ## 6. Choose the right API
152
+
153
+ | Need | Call |
154
+ | --- | --- |
155
+ | Net difference between two endpoints | `compare` |
156
+ | Net differences for many current records | `compare_many` |
157
+ | One step per root-record version | `timeline` |
158
+ | Steps for root and selected child versions | `activity_timeline` |
159
+ | Net diff and timelines from one history pass | `analyze` |
160
+
161
+ `timeline` is a root-checkpoint timeline. A child change becomes visible at the
162
+ next root boundary. `activity_timeline` can make a versioned child change its
163
+ own boundary and can end at an explicitly supplied current record.
164
+
165
+ ## 7. Ignore noise fields
166
+
167
+ `updated_at` is ignored by default. Passing `ignore:` replaces that default:
168
+
169
+ <!-- executable:quickstart-ignore -->
170
+ ```ruby
171
+ PaperTrailDiff.compare(
172
+ draft_version,
173
+ published_version,
174
+ ignore: %i[updated_at lock_version]
175
+ )
176
+
177
+ PaperTrailDiff.compare(
178
+ draft_version,
179
+ published_version,
180
+ ignore: [] # compare every available scalar attribute
181
+ )
182
+ ```
183
+
184
+ See the main README for exact path-specific ignore rules.
185
+
186
+ ## 8. Add association history when needed
187
+
188
+ Associations are optional. Add PT-AT only when the application needs historical
189
+ association reconstruction:
190
+
191
+ ```console
192
+ bundle add paper_trail-association_tracking
193
+ bin/rails generate paper_trail_association_tracking:install
194
+ bin/rails db:migrate
195
+ ```
196
+
197
+ The generator creates `version_associations` and enables
198
+ `PaperTrail.config.track_associations`. Every model whose historical state is
199
+ needed must be versioned:
200
+
201
+ ```ruby
202
+ class Article < ApplicationRecord
203
+ has_many :comments, dependent: :destroy
204
+ has_paper_trail synchronize_version_creation_timestamp: false
205
+ end
206
+
207
+ class Comment < ApplicationRecord
208
+ belongs_to :article
209
+ has_paper_trail
210
+ end
211
+ ```
212
+
213
+ Take an explicit root checkpoint before the example change:
214
+
215
+ <!-- executable:quickstart-association -->
216
+ ```ruby
217
+ Article.transaction do
218
+ Article.find(article.id).paper_trail.save_with_version
219
+ end
220
+ before = article.versions.reload.last
221
+
222
+ article.comments.create!(body: "First comment")
223
+ current = Article.find(article.id)
224
+
225
+ diff = PaperTrailDiff.compare(
226
+ before,
227
+ current,
228
+ associations: [:comments]
229
+ )
230
+
231
+ diff.associations["comments"].added.first.attributes["body"]
232
+ # => "First comment"
233
+ ```
234
+
235
+ Nested paths are explicit and bounded:
236
+
237
+ ```ruby
238
+ diff = PaperTrailDiff.compare(
239
+ before,
240
+ current,
241
+ associations: ["comments.replies.author"]
242
+ )
243
+ ```
244
+
245
+ To see versioned child events without touching the article after each change:
246
+
247
+ <!-- executable:quickstart-activity -->
248
+ ```ruby
249
+ steps = PaperTrailDiff.activity_timeline(
250
+ article,
251
+ from: before,
252
+ to: current,
253
+ associations: ["comments"]
254
+ )
255
+
256
+ steps.reject(&:empty?).each do |step|
257
+ boundary = step.to_boundary
258
+ puts "#{boundary.item_type} ##{boundary.item_id}"
259
+ end
260
+ ```
261
+
262
+ Use a later transaction-backed root checkpoint instead of `to: current` when
263
+ the result must remain reproducible after the database changes again.
264
+
265
+ For a historical reporting window, replace `from:` and `to:` with `within:`:
266
+
267
+ ```ruby
268
+ steps = PaperTrailDiff.activity_timeline(
269
+ article,
270
+ within: report_start...report_end,
271
+ associations: ["comments"]
272
+ )
273
+ ```
274
+
275
+ This selects versioned comment activity directly, even if the article had no
276
+ mutation during the window. Take a root checkpoint after `report_end`; it gives
277
+ the gem the historical context needed to expose the final child mutation.
278
+
279
+ ## 9. Common surprises
280
+
281
+ - PaperTrail stores pre-change snapshots. The current record is not represented
282
+ by `versions.last`; pass the record explicitly when current state is wanted.
283
+ - A `within:` window containing mutations needs a later root checkpoint. The
284
+ trailing version is reconstruction context and may appear as a step's ending
285
+ boundary even though its own mutation is outside the window.
286
+ - `ignore:` replaces the default list. Include `updated_at` yourself when using
287
+ a custom list and you still want it ignored.
288
+ - Historical associations require PT-AT to be installed, loaded, migrated, and
289
+ enabled. Restart the console after setup.
290
+ - Only requested association paths are traversed. The gem never recursively
291
+ discovers the whole model graph.
292
+ - Historical output cannot contain data that PaperTrail or PT-AT did not record
293
+ or can no longer reconstruct.
294
+
295
+ For performance guidance, HABTM limitations, diagnostics, discovery, path-aware
296
+ ignore rules, and complete result shapes, continue with the
297
+ [README](README.md).
298
+
299
+ Upstream setup references:
300
+ [PaperTrail installation](https://github.com/paper-trail-gem/paper_trail#1b-installation)
301
+ and
302
+ [PT-AT installation](https://github.com/westonganger/paper_trail-association_tracking#install).