paper_trail_diff 0.4.0 → 0.5.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.
@@ -2,13 +2,17 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module PaperTrailDiff
5
- # PaperTrail/ActiveRecord boundary that produces plain record snapshots.
6
- class PaperTrailAdapter
5
+ # PaperTrail/ActiveRecord boundary that produces plain record snapshots. It is
6
+ # deliberately the widest class here: it fronts every public operation and is
7
+ # the only place allowed to know about both PaperTrail and the pure engine.
8
+ # Reconstruction logic lives in the collaborators it wires together.
9
+ class PaperTrailAdapter # rubocop:disable Metrics/ClassLength
7
10
  #: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
8
11
  def initialize(associations:, ignore:, reload_live_endpoints: true)
9
12
  @association_tree = AssociationTree.build(associations)
10
13
  @ignore_policy = IgnorePolicy.build(ignore, association_paths: @association_tree.paths)
11
14
  @traversal = AssociationTraversal.new(@association_tree)
15
+ @traversal_preparer = TraversalPreparer.new(tree: @association_tree, traversal: @traversal)
12
16
  @live_endpoints = LiveEndpointProvider.new(
13
17
  tree: @association_tree, traversal: @traversal, reload: reload_live_endpoints
14
18
  )
@@ -18,9 +22,7 @@ module PaperTrailDiff
18
22
  tree: @association_tree, ignore_policy: @ignore_policy,
19
23
  traversal: @traversal, pool: @snapshot_pool
20
24
  )
21
- @historical_store = build_historical_store
22
- @timeline_snapshotter = TimelineSnapshotProvider.new(@historical_store)
23
- @activity_snapshotter = build_activity_snapshotter
25
+ build_snapshotters
24
26
  end
25
27
 
26
28
  #: (untyped, untyped) -> Diff
@@ -40,7 +42,7 @@ module PaperTrailDiff
40
42
  Instrumentation.instrument('compare_many', payload) do
41
43
  ComparisonBatch.new(
42
44
  comparisons,
43
- live_loader: @live_endpoints.method(:call), preparer: method(:prepare_traversal!),
45
+ live_loader: @live_endpoints.method(:call), preparer: @traversal_preparer.method(:call),
44
46
  history_preparer: @historical_store.method(:prepare_batch),
45
47
  historical_snapshotter: method(:historical_snapshot),
46
48
  live_normalizer: method(:normalize_live_snapshot)
@@ -50,7 +52,7 @@ module PaperTrailDiff
50
52
 
51
53
  #: (untyped, from: untyped, to: untyped, within: untyped) -> Array[Step]
52
54
  def timeline(record, from:, to:, within:)
53
- prepare_traversal!(record.class, historical: true)
55
+ @traversal_preparer.call(record.class, historical: true)
54
56
  builder = TimelineBuilder.new(
55
57
  record,
56
58
  from: from,
@@ -65,7 +67,7 @@ module PaperTrailDiff
65
67
  def activity_timeline(record, from:, to:, within:)
66
68
  payload = @instrumentation_payload.merge(model_type: record.class.base_class.name.to_s)
67
69
  Instrumentation.instrument('activity_timeline', payload) do
68
- prepare_traversal!(record.class, historical: true)
70
+ @traversal_preparer.call(record.class, historical: true)
69
71
  reject_live_habtm_activity!(record.class) if Endpoint.record?(to)
70
72
  steps = activity_builder(record, from: from, to: to, within: within).build
71
73
  payload[:step_count] = steps.length
@@ -76,11 +78,11 @@ module PaperTrailDiff
76
78
  #: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool) -> Analysis
77
79
  def analyze(record, from:, to:, within:, activity: false)
78
80
  if activity
79
- prepare_traversal!(record.class, historical: true)
81
+ @traversal_preparer.call(record.class, historical: true)
80
82
  return activity_builder(record, from: from, to: to, within: within).analyze
81
83
  end
82
84
 
83
- prepare_traversal!(record.class, historical: true)
85
+ @traversal_preparer.call(record.class, historical: true)
84
86
  TimelineBuilder.new(
85
87
  record,
86
88
  from: from,
@@ -90,6 +92,22 @@ module PaperTrailDiff
90
92
  ).analyze
91
93
  end
92
94
 
95
+ # Analyzes many roots over one shared range, preparing their history once.
96
+ #: (Array[untyped], within: untyped, ?activity: bool) -> Hash[identity, Analysis]
97
+ def analyze_many(records, within:, activity: false)
98
+ count = records.is_a?(Array) ? records.length : 0
99
+ payload = @instrumentation_payload.merge(comparison_count: count)
100
+ Instrumentation.instrument('analyze_many', payload) do
101
+ AnalysisBatch.new(
102
+ records,
103
+ time_range: within.nil? ? nil : TimeRange.new(within),
104
+ live_loader: @live_endpoints.method(:call),
105
+ history_preparer: @historical_store.method(:prepare_batch),
106
+ analyzer: batched_root_analyzer(activity)
107
+ ).call
108
+ end
109
+ end
110
+
93
111
  private
94
112
 
95
113
  # @rbs @association_tree: AssociationTree
@@ -97,19 +115,38 @@ module PaperTrailDiff
97
115
  # @rbs @instrumentation_payload: Hash[Symbol, untyped]
98
116
  # @rbs @ignore_policy: IgnorePolicy
99
117
  # @rbs @traversal: AssociationTraversal
118
+ # @rbs @traversal_preparer: TraversalPreparer
100
119
  # @rbs @snapshot_pool: SnapshotPool
101
120
  # @rbs @normalizer: SnapshotNormalizer
102
121
  # @rbs @historical_store: HistoricalSnapshotStore
103
122
  # @rbs @timeline_snapshotter: TimelineSnapshotProvider
104
123
  # @rbs @activity_snapshotter: ActivitySnapshotProvider
105
124
 
125
+ #: () -> void
126
+ def build_snapshotters
127
+ @historical_store = build_historical_store
128
+ @timeline_snapshotter = TimelineSnapshotProvider.new(@historical_store)
129
+ @activity_snapshotter = build_activity_snapshotter
130
+ end
131
+
132
+ #: (bool) -> BatchedRootAnalyzer
133
+ def batched_root_analyzer(activity)
134
+ BatchedRootAnalyzer.new(
135
+ tree: @association_tree,
136
+ timeline_snapshotter: @timeline_snapshotter,
137
+ activity_snapshotter: @activity_snapshotter,
138
+ preparer: @traversal_preparer.method(:call),
139
+ activity: activity
140
+ )
141
+ end
142
+
106
143
  #: () -> HistoricalSnapshotStore
107
144
  def build_historical_store
108
145
  HistoricalSnapshotStore.new(
109
146
  tree: @association_tree,
110
147
  traversal: @traversal,
111
148
  normalizer: @normalizer,
112
- preparer: method(:prepare_traversal!)
149
+ preparer: @traversal_preparer.method(:call)
113
150
  )
114
151
  end
115
152
 
@@ -180,30 +217,9 @@ module PaperTrailDiff
180
217
 
181
218
  #: (untyped) -> RecordSnapshot
182
219
  def normalize_live_snapshot(current)
183
- prepare_traversal!(current.class, historical: false)
220
+ @traversal_preparer.call(current.class, historical: false)
184
221
  @normalizer.call(current, reifier: LiveAssociationReader.new) ||
185
222
  raise(InvalidEndpointError, 'current record endpoint could not be normalized')
186
223
  end
187
-
188
- #: (untyped, historical: bool) -> void
189
- def prepare_traversal!(model_class, historical:)
190
- return if @association_tree.empty?
191
-
192
- ensure_association_tracking! if historical
193
- @traversal.validate!(model_class)
194
- end
195
-
196
- #: () -> void
197
- def ensure_association_tracking!
198
- paper_trail = Object.const_get(:PaperTrail) #: untyped
199
- config = paper_trail.config #: untyped
200
- available = defined?(::PaperTrailAssociationTracking) &&
201
- config.respond_to?(:track_associations?) &&
202
- config.track_associations?
203
- return if available
204
-
205
- message = 'association tracking must be loaded and enabled to compare historical associations'
206
- raise AssociationTrackingUnavailableError, message
207
- end
208
224
  end
209
225
  end
@@ -4,10 +4,10 @@
4
4
  module PaperTrailDiff
5
5
  # Selects and compares a chronological slice of a record's version history.
6
6
  class TimelineBuilder
7
- #: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped) -> void
8
- def initialize(record, from:, to:, snapshotter:, within: nil)
7
+ #: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?) -> void
8
+ def initialize(record, from:, to:, snapshotter:, within: nil, versions: nil) # rubocop:disable Metrics/ParameterLists
9
9
  @record = record
10
- @range = TimelineRange.new(record, from: from, to: to, within: within)
10
+ @range = TimelineRange.new(record, from: from, to: to, within: within, versions: versions)
11
11
  @snapshotter = snapshotter
12
12
  end
13
13
 
@@ -10,9 +10,10 @@ module PaperTrailDiff
10
10
  attr_reader :to #: untyped
11
11
  attr_reader :time_range #: TimeRange?
12
12
 
13
- #: (untyped, from: untyped, to: untyped, within: untyped) -> void
14
- def initialize(record, from:, to:, within:)
13
+ #: (untyped, from: untyped, to: untyped, within: untyped, ?versions: Array[untyped]?) -> void
14
+ def initialize(record, from:, to:, within:, versions: nil)
15
15
  @record = record
16
+ @versions = versions&.freeze
16
17
  @requested_from = from
17
18
  @requested_to = to
18
19
  @time_range = build_time_range(within)
@@ -29,8 +30,13 @@ module PaperTrailDiff
29
30
  (symbolic?(@requested_from) && @from.nil?) || (symbolic?(@requested_to) && @to.nil?)
30
31
  end
31
32
 
33
+ # A batch may have selected these versions already, in which case reselecting
34
+ # them per record would undo the batching.
32
35
  #: (?context_required: bool) -> Array[untyped]
33
36
  def select(context_required: false)
37
+ preselected = @versions
38
+ return preselected if preselected
39
+
34
40
  range = time_range
35
41
  if range
36
42
  return TimeVersionRange.new(@record, time_range: range).select(
@@ -61,6 +67,7 @@ module PaperTrailDiff
61
67
  private
62
68
 
63
69
  # @rbs @record: untyped
70
+ # @rbs @versions: Array[untyped]?
64
71
  # @rbs @requested_from: untyped
65
72
  # @rbs @requested_to: untyped
66
73
  # @rbs @from: untyped
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module PaperTrailDiff
5
+ # Checks that a model can be traversed before any reconstruction starts, and
6
+ # that association tracking is actually available when history is involved.
7
+ class TraversalPreparer
8
+ #: (tree: AssociationTree, traversal: AssociationTraversal) -> void
9
+ def initialize(tree:, traversal:)
10
+ @tree = tree
11
+ @traversal = traversal
12
+ end
13
+
14
+ #: (untyped, historical: bool) -> void
15
+ def call(model_class, historical:)
16
+ return if @tree.empty?
17
+
18
+ ensure_association_tracking! if historical
19
+ @traversal.validate!(model_class)
20
+ end
21
+
22
+ private
23
+
24
+ # @rbs @tree: AssociationTree
25
+ # @rbs @traversal: AssociationTraversal
26
+
27
+ #: () -> void
28
+ def ensure_association_tracking!
29
+ paper_trail = Object.const_get(:PaperTrail) #: untyped
30
+ config = paper_trail.config #: untyped
31
+ available = defined?(::PaperTrailAssociationTracking) &&
32
+ config.respond_to?(:track_associations?) &&
33
+ config.track_associations?
34
+ return if available
35
+
36
+ message = 'association tracking must be loaded and enabled to compare historical associations'
37
+ raise AssociationTrackingUnavailableError, message
38
+ end
39
+ end
40
+ end
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module PaperTrailDiff
5
- VERSION = '0.4.0'
5
+ VERSION = '0.5.0'
6
6
  end
@@ -10,6 +10,7 @@ require_relative 'paper_trail_diff/errors'
10
10
  require_relative 'paper_trail_diff/configuration'
11
11
  require_relative 'paper_trail_diff/endpoint'
12
12
  require_relative 'paper_trail_diff/association_traversal'
13
+ require_relative 'paper_trail_diff/traversal_preparer'
13
14
  require_relative 'paper_trail_diff/association_discovery'
14
15
  require_relative 'paper_trail_diff/diagnostics'
15
16
  require_relative 'paper_trail_diff/collection_identity_index'
@@ -36,7 +37,9 @@ require_relative 'paper_trail_diff/live_endpoint_batch_loader'
36
37
  require_relative 'paper_trail_diff/preloaded_endpoint_batch_loader'
37
38
  require_relative 'paper_trail_diff/live_endpoint_provider'
38
39
  require_relative 'paper_trail_diff/live_graph_collector'
40
+ require_relative 'paper_trail_diff/batch_boundary_resolver'
39
41
  require_relative 'paper_trail_diff/comparison_batch'
42
+ require_relative 'paper_trail_diff/batched_root_versions'
40
43
  require_relative 'paper_trail_diff/snapshot_normalizer'
41
44
  require_relative 'paper_trail_diff/historical_snapshot_store'
42
45
  require_relative 'paper_trail_diff/timeline_snapshot_provider'
@@ -56,6 +59,8 @@ require_relative 'paper_trail_diff/activity_boundary'
56
59
  require_relative 'paper_trail_diff/step'
57
60
  require_relative 'paper_trail_diff/analysis'
58
61
  require_relative 'paper_trail_diff/activity_root_steps'
62
+ require_relative 'paper_trail_diff/analysis_batch'
63
+ require_relative 'paper_trail_diff/batched_root_analyzer'
59
64
  require_relative 'paper_trail_diff/version_range'
60
65
  require_relative 'paper_trail_diff/time_range'
61
66
  require_relative 'paper_trail_diff/time_version_range'
@@ -90,8 +95,9 @@ require_relative 'paper_trail_diff/paper_trail_adapter'
90
95
  # type traversal_record_path = Array[RecordReference]
91
96
  # end
92
97
 
93
- # Structured version comparison for PaperTrail.
94
- module PaperTrailDiff
98
+ # Structured version comparison for PaperTrail. Each method is a thin,
99
+ # documented entry point, so this reads as an API listing rather than logic.
100
+ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
95
101
  DEFAULT_IGNORED_ATTRIBUTES = ['updated_at'].freeze
96
102
  SUPPORTED_ASSOCIATION_MACROS = %i[
97
103
  belongs_to
@@ -154,16 +160,23 @@ module PaperTrailDiff
154
160
  end
155
161
 
156
162
  # Compares adjacent root and selected-descendant activity boundaries.
157
- #: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option) -> Array[ActivityStep]
163
+ # `reload_live_endpoints:` applies only when `to:` is a current record; the
164
+ # other range forms never read live state.
165
+ #: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool) -> Array[ActivityStep]
158
166
  def activity_timeline( # rubocop:disable Metrics/ParameterLists
159
167
  record,
160
168
  from: nil,
161
169
  to: nil,
162
170
  within: nil,
163
171
  associations: [],
164
- ignore: DEFAULT_IGNORED_ATTRIBUTES
172
+ ignore: DEFAULT_IGNORED_ATTRIBUTES,
173
+ reload_live_endpoints: true
165
174
  )
166
- PaperTrailAdapter.new(associations: associations, ignore: ignore).activity_timeline(
175
+ PaperTrailAdapter.new(
176
+ associations: associations,
177
+ ignore: ignore,
178
+ reload_live_endpoints: reload_live_endpoints
179
+ ).activity_timeline(
167
180
  record,
168
181
  from: from,
169
182
  to: to,
@@ -191,6 +204,24 @@ module PaperTrailDiff
191
204
  )
192
205
  end
193
206
 
207
+ # Analyzes many roots over one shared time window, preparing their selected
208
+ # history once for the batch instead of once per record. Roots with no
209
+ # versions in the window return an empty `Analysis`.
210
+ #: (Array[untyped], ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool) -> Hash[identity, Analysis]
211
+ def analyze_many(
212
+ records,
213
+ within: nil,
214
+ associations: [],
215
+ ignore: DEFAULT_IGNORED_ATTRIBUTES,
216
+ activity: false
217
+ )
218
+ PaperTrailAdapter.new(associations: associations, ignore: ignore).analyze_many(
219
+ records,
220
+ within: within,
221
+ activity: activity
222
+ )
223
+ end
224
+
194
225
  # Returns the association macros this release can normalize.
195
226
  #: () -> Array[Symbol]
196
227
  def supported_association_macros
@@ -0,0 +1,42 @@
1
+ # Generated from lib/paper_trail_diff/analysis_batch.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Builds one Analysis per root over a shared range, selecting every root's
5
+ # versions and preparing their association history once for the whole batch
6
+ # rather than once per root.
7
+ class AnalysisBatch
8
+ # : (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped) -> void
9
+ def initialize: (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped) -> void
10
+
11
+ # : () -> Hash[identity, Analysis]
12
+ def call: () -> Hash[identity, Analysis]
13
+
14
+ private
15
+
16
+ @analyzer: untyped
17
+
18
+ @history_preparer: untyped
19
+
20
+ @live_loader: untyped
21
+
22
+ @records: Array[untyped]
23
+
24
+ @time_range: TimeRange?
25
+
26
+ # : () -> Array[untyped]
27
+ def validated_records: () -> Array[untyped]
28
+
29
+ # Association history is prepared per model class across every selected root
30
+ # version, which is the work that would otherwise repeat for each record.
31
+ # The roots are preloaded first, because preparation reads their current
32
+ # association state as a fallback and would otherwise walk it one root at a
33
+ # time.
34
+ # : (Array[untyped], Hash[Array[String], Array[untyped]]) -> void
35
+ def prepare: (Array[untyped], Hash[Array[String], Array[untyped]]) -> void
36
+
37
+ # A root with no versions in range has nothing to report, which is an empty
38
+ # result rather than a failed request.
39
+ # : (untyped, Array[untyped]) -> Analysis
40
+ def analysis_for: (untyped, Array[untyped]) -> Analysis
41
+ end
42
+ end
@@ -0,0 +1,62 @@
1
+ # Generated from lib/paper_trail_diff/batch_boundary_resolver.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Resolves `:first` and `:last` endpoints for a whole batch in two queries per
5
+ # model class. Left to the caller this is a per-root lookup, which reintroduces
6
+ # exactly the queries a batched comparison exists to remove.
7
+ class BatchBoundaryResolver
8
+ BOUNDARIES: untyped
9
+
10
+ # : (untyped) -> bool
11
+ def self.symbolic?: (untyped) -> bool
12
+
13
+ # : (Array[[untyped, untyped]]) -> void
14
+ def initialize: (Array[[ untyped, untyped ]]) -> void
15
+
16
+ # Returns the pairs with every resolvable symbol replaced. A symbol that
17
+ # names history the record does not have is left in place, so the caller can
18
+ # decide what an absent history means rather than being handed nil.
19
+ # : () -> Array[[untyped, untyped]]
20
+ def call: () -> Array[[ untyped, untyped ]]
21
+
22
+ private
23
+
24
+ @pairs: Array[[ untyped, untyped ]]
25
+
26
+ # : (untyped) -> bool
27
+ def symbolic?: (untyped) -> bool
28
+
29
+ # : (untyped, untyped, Hash[Array[String], Hash[Symbol, untyped]]) -> untyped
30
+ def resolve: (untyped, untyped, Hash[Array[String], Hash[Symbol, untyped]]) -> untyped
31
+
32
+ # A symbol carries no identity of its own, so the pair's other endpoint has
33
+ # to say which record is meant.
34
+ # : (untyped) -> Array[String]?
35
+ def anchor_identity: (untyped) -> Array[String]?
36
+
37
+ # : () -> Hash[Array[String], Hash[Symbol, untyped]]
38
+ def boundary_index: () -> Hash[Array[String], Hash[Symbol, untyped]]
39
+
40
+ # : () -> Array[[untyped, untyped]]
41
+ def anchors: () -> Array[[ untyped, untyped ]]
42
+
43
+ # : (untyped) -> [untyped, untyped]?
44
+ def anchor: (untyped) -> [ untyped, untyped ]?
45
+
46
+ # : (Hash[Array[String], Hash[Symbol, untyped]], untyped, Array[untyped]) -> void
47
+ def add_model_boundaries: (Hash[Array[String], Hash[Symbol, untyped]], untyped, Array[untyped]) -> void
48
+
49
+ # Two grouped queries name each root's outermost versions, and one more
50
+ # loads them, whatever the size of the batch.
51
+ # : (untyped, Array[untyped]) -> [Hash[untyped, untyped], Hash[untyped, untyped], Hash[untyped, untyped]]
52
+ def boundary_versions: (untyped, Array[untyped]) -> [ Hash[untyped, untyped], Hash[untyped, untyped], Hash[untyped, untyped] ]
53
+
54
+ # Grouped keys come back with whatever type the column uses, so match on the
55
+ # string form rather than assuming integers.
56
+ # : (Hash[untyped, untyped], untyped) -> untyped
57
+ def boundary_key: (Hash[untyped, untyped], untyped) -> untyped
58
+
59
+ # : (untyped, untyped) -> Array[String]
60
+ def identity_key: (untyped, untyped) -> Array[String]
61
+ end
62
+ end
@@ -0,0 +1,28 @@
1
+ # Generated from lib/paper_trail_diff/batched_root_analyzer.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Builds one root's Analysis inside a batch, from versions the batch already
5
+ # selected and history it already prepared.
6
+ class BatchedRootAnalyzer
7
+ # : (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool) -> void
8
+ def initialize: (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool) -> void
9
+
10
+ # : (untyped, Array[untyped]) -> Analysis
11
+ def call: (untyped, Array[untyped]) -> Analysis
12
+
13
+ private
14
+
15
+ @activity: bool
16
+
17
+ @activity_snapshotter: untyped
18
+
19
+ @preparer: untyped
20
+
21
+ @timeline_snapshotter: untyped
22
+
23
+ @tree: AssociationTree
24
+
25
+ # : (untyped, Array[untyped]) -> Analysis
26
+ def activity_analysis: (untyped, Array[untyped]) -> Analysis
27
+ end
28
+ end
@@ -0,0 +1,62 @@
1
+ # Generated from lib/paper_trail_diff/batched_root_versions.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Selects each root's versions for a batched range in a fixed number of
5
+ # queries. Only the range forms that mean the same thing for every root are
6
+ # supported: a shared wall-clock window, or each root's own whole history.
7
+ class BatchedRootVersions
8
+ # : (Array[untyped], time_range: TimeRange?) -> void
9
+ def initialize: (Array[untyped], time_range: TimeRange?) -> void
10
+
11
+ # Returns root versions per record identity, in chronological order.
12
+ # : () -> Hash[Array[String], Array[untyped]]
13
+ def call: () -> Hash[Array[String], Array[untyped]]
14
+
15
+ private
16
+
17
+ @records: Array[untyped]
18
+
19
+ @time_range: TimeRange?
20
+
21
+ # : (untyped, Array[untyped], Hash[Array[String], Array[untyped]]) -> void
22
+ def select_model: (untyped, Array[untyped], Hash[Array[String], Array[untyped]]) -> void
23
+
24
+ # The window's own versions plus, when it has one, the later version needed
25
+ # to reveal the last mutation inside it. A window closing on the root's own
26
+ # destruction needs no later version, because none can exist.
27
+ # : (Array[untyped], untyped) -> Array[untyped]
28
+ def versions_for: (Array[untyped], untyped) -> Array[untyped]
29
+
30
+ # : (Array[untyped]) -> bool
31
+ def terminal_destroy?: (Array[untyped]) -> bool
32
+
33
+ # : (untyped, Array[untyped], TimeRange?) -> Hash[String, Array[untyped]]
34
+ def grouped_versions: (untyped, Array[untyped], TimeRange?) -> Hash[String, Array[untyped]]
35
+
36
+ # One row per root: the earliest version after the window, found without a
37
+ # window function so the query stays portable.
38
+ # : (untyped, Array[untyped], TimeRange) -> Hash[String, untyped]
39
+ def trailing_versions: (untyped, Array[untyped], TimeRange) -> Hash[String, untyped]
40
+
41
+ # : (untyped, TimeRange) -> untyped
42
+ def no_earlier_trailing: (untyped, TimeRange) -> untyped
43
+
44
+ # Mirrors the window's own end handling, so a version sitting exactly on an
45
+ # inclusive boundary stays inside the window instead of masking the trailing
46
+ # version that follows it.
47
+ # : (untyped, untyped, TimeRange) -> untyped
48
+ def preceding: (untyped, untyped, TimeRange) -> untyped
49
+
50
+ # : (untyped, TimeRange) -> untyped
51
+ def after_window: (untyped, TimeRange) -> untyped
52
+
53
+ # : (untyped, Array[untyped]) -> untyped
54
+ def base_scope: (untyped, Array[untyped]) -> untyped
55
+
56
+ # : (untyped) -> Array[untyped]
57
+ def ordered: (untyped) -> Array[untyped]
58
+
59
+ # : (untyped, untyped) -> Array[String]
60
+ def identity: (untyped, untyped) -> Array[String]
61
+ end
62
+ end
@@ -23,6 +23,21 @@ module PaperTrailDiff
23
23
 
24
24
  @preparer: untyped
25
25
 
26
+ # : (Array[[untyped, untyped]]) -> Hash[identity, RecordSnapshot]
27
+ def prepared_live_snapshots: (Array[[ untyped, untyped ]]) -> Hash[identity, RecordSnapshot]
28
+
29
+ # : (untyped) -> bool
30
+ def symbolic?: (untyped) -> bool
31
+
32
+ # A boundary symbol that stayed unresolved means the root has no recorded
33
+ # history. An absent history is an empty result rather than a failed
34
+ # request, matching how the timeline APIs answer the same question.
35
+ # : (Array[[untyped, untyped]], Hash[identity, RecordSnapshot]) -> comparison_results
36
+ def results: (Array[[ untyped, untyped ]], Hash[identity, RecordSnapshot]) -> comparison_results
37
+
38
+ # : (untyped, untyped) -> identity
39
+ def entry_identity: (untyped, untyped) -> identity
40
+
26
41
  # : (untyped, untyped, Hash[identity, RecordSnapshot]) -> Diff
27
42
  def compare: (untyped, untyped, Hash[identity, RecordSnapshot]) -> Diff
28
43
 
@@ -9,7 +9,6 @@ module PaperTrailDiff
9
9
  # : (untyped, Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
10
10
  def prepare: (untyped, Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
11
11
 
12
- # Prepares selected history for several roots of the same model class.
13
12
  # : (Array[untyped], Array[untyped]) -> void
14
13
  def prepare_batch: (Array[untyped], Array[untyped]) -> void
15
14
 
@@ -1,7 +1,10 @@
1
1
  # Generated from lib/paper_trail_diff/paper_trail_adapter.rb with RBS::Inline
2
2
 
3
3
  module PaperTrailDiff
4
- # PaperTrail/ActiveRecord boundary that produces plain record snapshots.
4
+ # PaperTrail/ActiveRecord boundary that produces plain record snapshots. It is
5
+ # deliberately the widest class here: it fronts every public operation and is
6
+ # the only place allowed to know about both PaperTrail and the pure engine.
7
+ # Reconstruction logic lives in the collaborators it wires together.
5
8
  class PaperTrailAdapter
6
9
  # : (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
7
10
  def initialize: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
@@ -22,6 +25,10 @@ module PaperTrailDiff
22
25
  # : (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool) -> Analysis
23
26
  def analyze: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool) -> Analysis
24
27
 
28
+ # Analyzes many roots over one shared range, preparing their history once.
29
+ # : (Array[untyped], within: untyped, ?activity: bool) -> Hash[identity, Analysis]
30
+ def analyze_many: (Array[untyped], within: untyped, ?activity: bool) -> Hash[identity, Analysis]
31
+
25
32
  private
26
33
 
27
34
  @activity_snapshotter: ActivitySnapshotProvider
@@ -44,6 +51,14 @@ module PaperTrailDiff
44
51
 
45
52
  @traversal: AssociationTraversal
46
53
 
54
+ @traversal_preparer: TraversalPreparer
55
+
56
+ # : () -> void
57
+ def build_snapshotters: () -> void
58
+
59
+ # : (bool) -> BatchedRootAnalyzer
60
+ def batched_root_analyzer: (bool) -> BatchedRootAnalyzer
61
+
47
62
  # : () -> HistoricalSnapshotStore
48
63
  def build_historical_store: () -> HistoricalSnapshotStore
49
64
 
@@ -73,11 +88,5 @@ module PaperTrailDiff
73
88
 
74
89
  # : (untyped) -> RecordSnapshot
75
90
  def normalize_live_snapshot: (untyped) -> RecordSnapshot
76
-
77
- # : (untyped, historical: bool) -> void
78
- def prepare_traversal!: (untyped, historical: bool) -> void
79
-
80
- # : () -> void
81
- def ensure_association_tracking!: () -> void
82
91
  end
83
92
  end
@@ -3,8 +3,8 @@
3
3
  module PaperTrailDiff
4
4
  # Selects and compares a chronological slice of a record's version history.
5
5
  class TimelineBuilder
6
- # : (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped) -> void
7
- def initialize: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped) -> void
6
+ # : (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?) -> void
7
+ def initialize: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?) -> void
8
8
 
9
9
  # : () -> Array[Step]
10
10
  def build: () -> Array[Step]
@@ -11,14 +11,16 @@ module PaperTrailDiff
11
11
 
12
12
  attr_reader time_range: TimeRange?
13
13
 
14
- # : (untyped, from: untyped, to: untyped, within: untyped) -> void
15
- def initialize: (untyped, from: untyped, to: untyped, within: untyped) -> void
14
+ # : (untyped, from: untyped, to: untyped, within: untyped, ?versions: Array[untyped]?) -> void
15
+ def initialize: (untyped, from: untyped, to: untyped, within: untyped, ?versions: Array[untyped]?) -> void
16
16
 
17
17
  # A record with no versions has no first or last boundary to resolve, which
18
18
  # is an empty history rather than a bad request.
19
19
  # : () -> bool
20
20
  def unresolved?: () -> bool
21
21
 
22
+ # A batch may have selected these versions already, in which case reselecting
23
+ # them per record would undo the batching.
22
24
  # : (?context_required: bool) -> Array[untyped]
23
25
  def select: (?context_required: bool) -> Array[untyped]
24
26
 
@@ -43,6 +45,8 @@ module PaperTrailDiff
43
45
 
44
46
  @to: untyped
45
47
 
48
+ @versions: Array[untyped]?
49
+
46
50
  # : () -> Array[untyped]
47
51
  def empty_versions: () -> Array[untyped]
48
52