paper_trail_diff 0.11.0 → 0.13.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -0
  3. data/QUICKSTART.md +149 -79
  4. data/README.md +179 -1367
  5. data/docs/architecture.md +88 -0
  6. data/docs/performance.md +309 -0
  7. data/docs/reference.md +977 -0
  8. data/docs/testing.md +146 -0
  9. data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +2 -12
  10. data/lib/paper_trail_diff/activity_boundary.rb +20 -8
  11. data/lib/paper_trail_diff/activity_child_candidate_loader.rb +29 -0
  12. data/lib/paper_trail_diff/activity_collection_event_applier.rb +10 -7
  13. data/lib/paper_trail_diff/activity_collection_route_updater.rb +1 -11
  14. data/lib/paper_trail_diff/activity_event_record_normalizer.rb +4 -0
  15. data/lib/paper_trail_diff/activity_event_record_resolver.rb +66 -6
  16. data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +6 -4
  17. data/lib/paper_trail_diff/activity_grouping.rb +12 -0
  18. data/lib/paper_trail_diff/activity_history.rb +9 -3
  19. data/lib/paper_trail_diff/activity_relationship.rb +2 -0
  20. data/lib/paper_trail_diff/activity_root_snapshot_refresher.rb +1 -0
  21. data/lib/paper_trail_diff/activity_snapshot_sequence.rb +9 -2
  22. data/lib/paper_trail_diff/activity_timeline_builder.rb +39 -35
  23. data/lib/paper_trail_diff/activity_version_collector.rb +27 -18
  24. data/lib/paper_trail_diff/analysis.rb +9 -4
  25. data/lib/paper_trail_diff/analysis_batch.rb +28 -7
  26. data/lib/paper_trail_diff/association_traversal.rb +23 -0
  27. data/lib/paper_trail_diff/batch_boundary_resolver.rb +18 -26
  28. data/lib/paper_trail_diff/batch_safety.rb +55 -0
  29. data/lib/paper_trail_diff/batched_root_analyzer.rb +14 -5
  30. data/lib/paper_trail_diff/batched_root_versions.rb +9 -37
  31. data/lib/paper_trail_diff/branch_snapshot_refresher.rb +7 -8
  32. data/lib/paper_trail_diff/deleted_analysis_batch.rb +93 -0
  33. data/lib/paper_trail_diff/direct_record_state_loader.rb +81 -0
  34. data/lib/paper_trail_diff/endpoint.rb +10 -0
  35. data/lib/paper_trail_diff/engine.rb +4 -2
  36. data/lib/paper_trail_diff/errors.rb +9 -0
  37. data/lib/paper_trail_diff/historical_association_reifier.rb +26 -1
  38. data/lib/paper_trail_diff/historical_filter.rb +181 -0
  39. data/lib/paper_trail_diff/historical_snapshot_store.rb +51 -35
  40. data/lib/paper_trail_diff/instrumentation.rb +2 -2
  41. data/lib/paper_trail_diff/nested_comparator.rb +59 -24
  42. data/lib/paper_trail_diff/paper_trail_adapter.rb +109 -55
  43. data/lib/paper_trail_diff/prepared_activity_index.rb +121 -0
  44. data/lib/paper_trail_diff/prepared_edge_loader.rb +11 -11
  45. data/lib/paper_trail_diff/prepared_history.rb +21 -30
  46. data/lib/paper_trail_diff/prepared_history_loader.rb +14 -2
  47. data/lib/paper_trail_diff/prepared_record_index.rb +102 -29
  48. data/lib/paper_trail_diff/prepared_snapshot_builder.rb +64 -0
  49. data/lib/paper_trail_diff/prepared_state_association_reader.rb +77 -0
  50. data/lib/paper_trail_diff/root_version_plan.rb +25 -2
  51. data/lib/paper_trail_diff/root_version_selection.rb +16 -5
  52. data/lib/paper_trail_diff/scoped_analysis.rb +17 -2
  53. data/lib/paper_trail_diff/scoped_analysis_builder.rb +47 -0
  54. data/lib/paper_trail_diff/scoped_root_selection.rb +106 -13
  55. data/lib/paper_trail_diff/snapshot.rb +9 -0
  56. data/lib/paper_trail_diff/snapshot_normalizer.rb +41 -2
  57. data/lib/paper_trail_diff/step.rb +10 -5
  58. data/lib/paper_trail_diff/stored_time_precision.rb +68 -0
  59. data/lib/paper_trail_diff/support.rb +13 -3
  60. data/lib/paper_trail_diff/time_activity_timeline_builder.rb +19 -7
  61. data/lib/paper_trail_diff/time_range.rb +22 -2
  62. data/lib/paper_trail_diff/time_version_range.rb +1 -15
  63. data/lib/paper_trail_diff/timeline_builder.rb +5 -4
  64. data/lib/paper_trail_diff/timeline_range.rb +25 -43
  65. data/lib/paper_trail_diff/timeline_snapshot_provider.rb +13 -2
  66. data/lib/paper_trail_diff/traversal.rb +1 -1
  67. data/lib/paper_trail_diff/value_objects.rb +66 -0
  68. data/lib/paper_trail_diff/version.rb +1 -1
  69. data/lib/paper_trail_diff/version_association_candidate_scope.rb +1 -1
  70. data/lib/paper_trail_diff/version_boundary.rb +48 -0
  71. data/lib/paper_trail_diff/version_query.rb +33 -0
  72. data/lib/paper_trail_diff/version_range.rb +10 -38
  73. data/lib/paper_trail_diff.rb +81 -43
  74. data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +0 -3
  75. data/sig/generated/paper_trail_diff/activity_boundary.rbs +9 -4
  76. data/sig/generated/paper_trail_diff/activity_child_candidate_loader.rbs +11 -0
  77. data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +4 -4
  78. data/sig/generated/paper_trail_diff/activity_collection_route_updater.rbs +0 -3
  79. data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +19 -3
  80. data/sig/generated/paper_trail_diff/activity_event_snapshot_refresher.rbs +2 -2
  81. data/sig/generated/paper_trail_diff/activity_grouping.rbs +3 -0
  82. data/sig/generated/paper_trail_diff/activity_history.rbs +6 -2
  83. data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +7 -2
  84. data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +8 -3
  85. data/sig/generated/paper_trail_diff/activity_version_collector.rbs +4 -8
  86. data/sig/generated/paper_trail_diff/analysis.rbs +2 -2
  87. data/sig/generated/paper_trail_diff/analysis_batch.rbs +14 -3
  88. data/sig/generated/paper_trail_diff/association_traversal.rbs +11 -0
  89. data/sig/generated/paper_trail_diff/batch_boundary_resolver.rbs +7 -10
  90. data/sig/generated/paper_trail_diff/batch_safety.rbs +24 -0
  91. data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +8 -2
  92. data/sig/generated/paper_trail_diff/batched_root_versions.rbs +4 -16
  93. data/sig/generated/paper_trail_diff/branch_snapshot_refresher.rbs +2 -2
  94. data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +44 -0
  95. data/sig/generated/paper_trail_diff/direct_record_state_loader.rbs +42 -0
  96. data/sig/generated/paper_trail_diff/endpoint.rbs +5 -0
  97. data/sig/generated/paper_trail_diff/engine.rbs +4 -2
  98. data/sig/generated/paper_trail_diff/errors.rbs +12 -0
  99. data/sig/generated/paper_trail_diff/historical_association_reifier.rbs +8 -0
  100. data/sig/generated/paper_trail_diff/historical_filter.rbs +75 -0
  101. data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +17 -4
  102. data/sig/generated/paper_trail_diff/nested_comparator.rbs +31 -18
  103. data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +36 -15
  104. data/sig/generated/paper_trail_diff/prepared_activity_index.rbs +56 -0
  105. data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +3 -3
  106. data/sig/generated/paper_trail_diff/prepared_history.rbs +12 -14
  107. data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +2 -0
  108. data/sig/generated/paper_trail_diff/prepared_record_index.rbs +43 -6
  109. data/sig/generated/paper_trail_diff/prepared_snapshot_builder.rbs +24 -0
  110. data/sig/generated/paper_trail_diff/prepared_state_association_reader.rbs +33 -0
  111. data/sig/generated/paper_trail_diff/root_version_plan.rbs +15 -2
  112. data/sig/generated/paper_trail_diff/root_version_selection.rbs +8 -1
  113. data/sig/generated/paper_trail_diff/scoped_analysis.rbs +14 -2
  114. data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +22 -0
  115. data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +46 -6
  116. data/sig/generated/paper_trail_diff/snapshot.rbs +4 -0
  117. data/sig/generated/paper_trail_diff/snapshot_normalizer.rbs +13 -0
  118. data/sig/generated/paper_trail_diff/step.rbs +8 -2
  119. data/sig/generated/paper_trail_diff/stored_time_precision.rbs +36 -0
  120. data/sig/generated/paper_trail_diff/support.rbs +7 -0
  121. data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +7 -2
  122. data/sig/generated/paper_trail_diff/time_range.rbs +9 -0
  123. data/sig/generated/paper_trail_diff/time_version_range.rbs +0 -6
  124. data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
  125. data/sig/generated/paper_trail_diff/timeline_range.rbs +10 -14
  126. data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +2 -0
  127. data/sig/generated/paper_trail_diff/value_objects.rbs +45 -0
  128. data/sig/generated/paper_trail_diff/version_boundary.rbs +23 -0
  129. data/sig/generated/paper_trail_diff/version_query.rbs +15 -0
  130. data/sig/generated/paper_trail_diff/version_range.rbs +5 -14
  131. data/sig/generated/paper_trail_diff.rbs +37 -27
  132. metadata +31 -5
@@ -5,11 +5,15 @@ require 'paper_trail'
5
5
 
6
6
  require_relative 'paper_trail_diff/version'
7
7
  require_relative 'paper_trail_diff/support'
8
+ require_relative 'paper_trail_diff/version_query'
9
+ require_relative 'paper_trail_diff/stored_time_precision'
10
+ require_relative 'paper_trail_diff/version_boundary'
8
11
  require_relative 'paper_trail_diff/instrumentation'
9
12
  require_relative 'paper_trail_diff/errors'
10
13
  require_relative 'paper_trail_diff/configuration'
11
14
  require_relative 'paper_trail_diff/endpoint'
12
15
  require_relative 'paper_trail_diff/association_traversal'
16
+ require_relative 'paper_trail_diff/batch_safety'
13
17
  require_relative 'paper_trail_diff/traversal_preparer'
14
18
  require_relative 'paper_trail_diff/association_discovery'
15
19
  require_relative 'paper_trail_diff/diagnostics'
@@ -28,6 +32,10 @@ require_relative 'paper_trail_diff/nested_comparator'
28
32
  require_relative 'paper_trail_diff/engine'
29
33
  require_relative 'paper_trail_diff/historical_association_reifier'
30
34
  require_relative 'paper_trail_diff/prepared_record_index'
35
+ require_relative 'paper_trail_diff/direct_record_state_loader'
36
+ require_relative 'paper_trail_diff/prepared_state_association_reader'
37
+ require_relative 'paper_trail_diff/prepared_snapshot_builder'
38
+ require_relative 'paper_trail_diff/prepared_activity_index'
31
39
  require_relative 'paper_trail_diff/prepared_history'
32
40
  require_relative 'paper_trail_diff/version_association_candidate_scope'
33
41
  require_relative 'paper_trail_diff/prepared_edge_loader'
@@ -66,8 +74,11 @@ require_relative 'paper_trail_diff/step'
66
74
  require_relative 'paper_trail_diff/analysis'
67
75
  require_relative 'paper_trail_diff/activity_root_steps'
68
76
  require_relative 'paper_trail_diff/analysis_batch'
77
+ require_relative 'paper_trail_diff/deleted_analysis_batch'
69
78
  require_relative 'paper_trail_diff/scoped_analysis'
79
+ require_relative 'paper_trail_diff/historical_filter'
70
80
  require_relative 'paper_trail_diff/scoped_root_selection'
81
+ require_relative 'paper_trail_diff/scoped_analysis_builder'
71
82
  require_relative 'paper_trail_diff/batched_root_analyzer'
72
83
  require_relative 'paper_trail_diff/version_range'
73
84
  require_relative 'paper_trail_diff/version_sequence_diagnostics'
@@ -92,10 +103,16 @@ require_relative 'paper_trail_diff/paper_trail_adapter'
92
103
  # type attribute_changes = Hash[String, ValueChange]
93
104
  # type association_diff = ToOneAssociationDiff | CollectionAssociationDiff
94
105
  # type association_diffs = Hash[String, association_diff]
106
+ # type nested_change = ValueChange | ArrayChange
107
+ # type nested_changes = Hash[Array[String], nested_change]
95
108
  # type association_snapshots = Hash[String, AssociationSnapshot]
96
109
  # type identity = Array[untyped]
97
110
  # type comparison_input = Hash[String | Symbol, untyped]
98
111
  # type comparison_results = Hash[identity, Diff]
112
+ # type report_options = {
113
+ # within: untyped, activity: bool, version_scope: untyped,
114
+ # close_on: Symbol?, snapshots: bool, group: Symbol?
115
+ # }
99
116
  # type ignore_option = Array[String | Symbol] | Hash[String | Symbol, untyped]
100
117
  # type reference_key = :type | :id | "type" | "id"
101
118
  # type traversal_context = :change | :included_state
@@ -115,7 +132,7 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
115
132
  has_and_belongs_to_many
116
133
  ].freeze
117
134
 
118
- class << self
135
+ class << self # rubocop:disable Metrics/ClassLength
119
136
  # Compares net state between explicit PaperTrail-version or current-record endpoints.
120
137
  #: (untyped, untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool) -> Diff
121
138
  def compare(
@@ -136,21 +153,23 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
136
153
  end
137
154
 
138
155
  # Compares many independent endpoint pairs while batching current-record loading.
139
- #: (Array[comparison_input], ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool) -> comparison_results
156
+ #: (Array[comparison_input], ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool, ?allow_expensive: bool) -> comparison_results
140
157
  def compare_many(
141
158
  comparisons,
142
159
  associations: [],
143
160
  ignore: DEFAULT_IGNORED_ATTRIBUTES,
144
- reload_live_endpoints: true
161
+ reload_live_endpoints: true,
162
+ allow_expensive: false
145
163
  )
146
164
  PaperTrailAdapter.new(
147
165
  associations: associations,
148
166
  ignore: ignore,
149
- reload_live_endpoints: reload_live_endpoints
167
+ reload_live_endpoints: reload_live_endpoints, allow_expensive: allow_expensive
150
168
  ).compare_many(comparisons)
151
169
  end
152
170
 
153
- # Compares every adjacent reconstructed state in an inclusive version range.
171
+ # Compares every adjacent reconstructed state in an inclusive version range,
172
+ # by default the record's whole history through its current state.
154
173
  #: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?version_scope: untyped, ?close_on: Symbol?) -> Array[Step]
155
174
  def timeline( # rubocop:disable Metrics/ParameterLists
156
175
  record,
@@ -173,9 +192,10 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
173
192
  end
174
193
 
175
194
  # Compares adjacent root and selected-descendant activity boundaries.
176
- # `reload_live_endpoints:` applies only when `to:` is a current record; the
177
- # other range forms never read live state.
178
- #: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool) -> Array[ActivityStep]
195
+ # `reload_live_endpoints:` applies whenever the range ends at the current
196
+ # record: an explicit `to:` record, the default range, or a window with no
197
+ # later version.
198
+ #: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?) -> Array[ActivityStep]
179
199
  def activity_timeline( # rubocop:disable Metrics/ParameterLists
180
200
  record,
181
201
  from: nil,
@@ -233,47 +253,36 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
233
253
  )
234
254
  end
235
255
 
236
- # Analyzes many roots over one shared time window, preparing their selected
237
- # history once for the batch instead of once per record. Roots with no
238
- # versions in the window return an empty `Analysis`.
239
- #
240
- # Pass `records` to analyze a list you assembled, which returns a Hash keyed
241
- # by identity. Pass `scope:` with a `limit:` to have the roots selected for
242
- # you from a relation, which returns a `ScopedAnalysis` -- the same Hash,
243
- # plus the roots the relation could not reach. See `analyze_scope` for why
244
- # that second collection exists.
245
- #: (?Array[untyped]?, ?scope: untyped, ?limit: Integer?, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> (Hash[identity, Analysis] | ScopedAnalysis)
256
+ # Analyzes an array of records over one shared range, preparing their
257
+ # selected history once for the batch instead of once per record, and
258
+ # returns a frozen Hash keyed by identity. Roots with nothing to report in
259
+ # the window return an empty `Analysis`. To let a relation choose the
260
+ # records, use `analyze_scope`.
261
+ #: (Array[untyped], ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?allow_expensive: bool) -> Hash[identity, Analysis]
246
262
  def analyze_many( # rubocop:disable Metrics/ParameterLists
247
- records = nil,
248
- scope: nil,
249
- limit: nil,
263
+ records,
250
264
  within: nil,
251
265
  associations: [],
252
266
  ignore: DEFAULT_IGNORED_ATTRIBUTES,
253
267
  activity: false,
254
268
  version_scope: nil,
255
- close_on: nil
269
+ close_on: nil,
270
+ snapshots: false,
271
+ group: nil,
272
+ allow_expensive: false
256
273
  )
257
- adapter = PaperTrailAdapter.new(associations: associations, ignore: ignore)
258
- if scope
259
- raise ConfigurationError, 'pass either records or scope:, not both' unless records.nil?
260
-
261
- return adapter.analyze_scope(scope, limit: limit, within: within, activity: activity,
262
- version_scope: version_scope, close_on: close_on)
263
- end
264
- raise ConfigurationError, 'pass records or scope:' if records.nil?
265
-
266
- adapter.analyze_many(records, within: within, activity: activity,
267
- version_scope: version_scope, close_on: close_on)
274
+ PaperTrailAdapter.new(
275
+ associations: associations, ignore: ignore, allow_expensive: allow_expensive
276
+ ).analyze_many(records, within: within, activity: activity, version_scope: version_scope,
277
+ close_on: close_on, snapshots: snapshots, group: group)
268
278
  end
269
279
 
270
280
  # Analyzes every root the relation reaches whose history moved inside the
271
- # window, selecting them in a fixed number of queries rather than making the
272
- # caller rediscover them.
281
+ # window. Joined duplicates are read in bounded pages and analyzed once.
273
282
  #
274
283
  # `limit:` is required and exceeding it raises. Selection moves into the gem
275
- # here, so the bound on how much work a page can ask for has to move with
276
- # it, and a truncated audit report is worse than a refused one.
284
+ # here. The relation's own limit/offset select the live page; this separate
285
+ # ceiling rejects too many unique roots rather than truncating that page.
277
286
  #
278
287
  # Returns a `ScopedAnalysis`, which destructures:
279
288
  #
@@ -282,7 +291,8 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
282
291
  # )
283
292
  #
284
293
  # `unreachable` names roots that changed in the window but have no live row
285
- # left. A relation's conditions are evaluated against the live table, so a
294
+ # left, independently of the live page's pagination and safety ceiling.
295
+ # A relation's conditions are evaluated against the live table, so a
286
296
  # destroyed root cannot be tested against them at all -- its history is
287
297
  # intact and the state it held at destruction may well have matched. Those
288
298
  # roots are reported rather than dropped so that a page auditing deletions
@@ -291,7 +301,7 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
291
301
  # Note also that a relation selects on current state, not on state during
292
302
  # the window: `where(status: 'published')` means published *now*, which is a
293
303
  # different set from what was published while the window was open.
294
- #: (untyped, limit: Integer?, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> ScopedAnalysis
304
+ #: (untyped, limit: Integer?, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?historical_filter: untyped, ?max_filter_versions: Integer, ?include_deleted: bool, ?allow_expensive: bool) -> ScopedAnalysis
295
305
  def analyze_scope( # rubocop:disable Metrics/ParameterLists
296
306
  scope,
297
307
  limit:,
@@ -300,11 +310,39 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
300
310
  ignore: DEFAULT_IGNORED_ATTRIBUTES,
301
311
  activity: false,
302
312
  version_scope: nil,
303
- close_on: nil
313
+ close_on: nil,
314
+ snapshots: false,
315
+ group: nil,
316
+ historical_filter: nil,
317
+ max_filter_versions: HistoricalFilter::DEFAULT_VERSION_LIMIT,
318
+ include_deleted: false,
319
+ allow_expensive: false
304
320
  )
305
- PaperTrailAdapter.new(associations: associations, ignore: ignore).analyze_scope(
321
+ adapter = PaperTrailAdapter.new(associations: associations, ignore: ignore,
322
+ allow_expensive: allow_expensive)
323
+ adapter.analyze_scope(
306
324
  scope, limit: limit, within: within, activity: activity,
307
- version_scope: version_scope, close_on: close_on
325
+ version_scope: version_scope, close_on: close_on, snapshots: snapshots, group: group,
326
+ historical_filter: historical_filter, max_filter_versions: max_filter_versions,
327
+ include_deleted: include_deleted
328
+ )
329
+ end
330
+
331
+ # Recent root and selected-child activity through the current saved state.
332
+ # Historical windows remain explicit on analyze_scope; this entry point is
333
+ # deliberately named for a report through now rather than an as-of query.
334
+ #: (untyped, since: untyped, limit: Integer, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?snapshots: bool, ?group: Symbol?, ?include_deleted: bool, ?historical_filter: untyped, ?max_filter_versions: Integer, ?allow_expensive: bool) -> ScopedAnalysis
335
+ def report( # rubocop:disable Metrics/ParameterLists
336
+ scope, since:, limit:, associations: [], ignore: DEFAULT_IGNORED_ATTRIBUTES,
337
+ snapshots: false, group: nil, include_deleted: false, historical_filter: nil,
338
+ max_filter_versions: HistoricalFilter::DEFAULT_VERSION_LIMIT, allow_expensive: false
339
+ )
340
+ analyze_scope(
341
+ scope,
342
+ within: since..Time.now.utc, limit: limit, associations: associations,
343
+ ignore: ignore, activity: true, close_on: :current, snapshots: snapshots, group: group,
344
+ include_deleted: include_deleted, historical_filter: historical_filter,
345
+ max_filter_versions: max_filter_versions, allow_expensive: allow_expensive
308
346
  )
309
347
  end
310
348
 
@@ -344,7 +382,7 @@ module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
344
382
  # list that merely shifted would otherwise look changed throughout. A key
345
383
  # that was absent reads as `NestedComparator::ABSENT` rather than nil, which
346
384
  # JSON uses for a present null.
347
- #: (untyped, ?untyped) -> Hash[Array[String], ValueChange]
385
+ #: (untyped, ?untyped) -> nested_changes
348
386
  def nested_changes(change, to_value = nil)
349
387
  # Tested by type rather than by responding to `from`: ActiveSupport gives
350
388
  # String#from, so duck-typing here quietly reads a plain string as a pair.
@@ -29,9 +29,6 @@ module PaperTrailDiff
29
29
  # : (Array[RecordSnapshot], untyped, RecordSnapshot?) -> [Array[RecordSnapshot], bool]
30
30
  def replace_existing_target: (Array[RecordSnapshot], untyped, RecordSnapshot?) -> [ Array[RecordSnapshot], bool ]
31
31
 
32
- # : (RecordSnapshot, String, AssociationSnapshot) -> RecordSnapshot
33
- def replace_association: (RecordSnapshot, String, AssociationSnapshot) -> RecordSnapshot
34
-
35
32
  # : (untyped, RecordSnapshot?) -> String
36
33
  def replacement_type: (untyped, RecordSnapshot?) -> String
37
34
 
@@ -54,8 +54,8 @@ module PaperTrailDiff
54
54
  # : () -> bool
55
55
  def destroyed?: () -> bool
56
56
 
57
- # : () -> Hash[Symbol, untyped]
58
- def to_h: () -> Hash[Symbol, untyped]
57
+ # : (?metadata: bool) -> Hash[Symbol, untyped]
58
+ def to_h: (?metadata: bool) -> Hash[Symbol, untyped]
59
59
  end
60
60
 
61
61
  # One adjacent transition in a descendant-aware activity timeline.
@@ -87,7 +87,12 @@ module PaperTrailDiff
87
87
  # : () -> bool
88
88
  def empty?: () -> bool
89
89
 
90
- # : () -> Hash[Symbol, untyped]
91
- def to_h: () -> Hash[Symbol, untyped]
90
+ # The event whose change this step reports. For a transaction group, this
91
+ # is the first event in the group, not necessarily its only actor or record.
92
+ # : () -> ActivityBoundary
93
+ def source_boundary: () -> ActivityBoundary
94
+
95
+ # : (?metadata: bool, ?snapshots: bool) -> Hash[Symbol, untyped]
96
+ def to_h: (?metadata: bool, ?snapshots: bool) -> Hash[Symbol, untyped]
92
97
  end
93
98
  end
@@ -9,6 +9,14 @@ module PaperTrailDiff
9
9
  # : () -> Array[untyped]
10
10
  def call: () -> Array[untyped]
11
11
 
12
+ # One shared query retains the owner and timestamp needed to partition a
13
+ # batch's candidates back into each root's exact activity range.
14
+ # : () -> Array[Array[untyped]]
15
+ def historical_rows: () -> Array[Array[untyped]]
16
+
17
+ # : () -> Array[Array[untyped]]
18
+ def current_pairs: () -> Array[Array[untyped]]
19
+
12
20
  private
13
21
 
14
22
  @parent_class: untyped
@@ -19,6 +27,9 @@ module PaperTrailDiff
19
27
 
20
28
  @reflection: untyped
21
29
 
30
+ # : (Array[Array[untyped]]) -> Array[Array[untyped]]
31
+ def typed_rows: (Array[Array[untyped]]) -> Array[Array[untyped]]
32
+
22
33
  # : () -> Array[untyped]
23
34
  def historical_ids: () -> Array[untyped]
24
35
 
@@ -22,8 +22,8 @@ module PaperTrailDiff
22
22
 
23
23
  @route_updater: ActivityCollectionRouteUpdater
24
24
 
25
- # : (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped]) -> RecordSnapshot
26
- def apply_route: (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped]) -> RecordSnapshot
25
+ # : (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped], ?direct: bool) -> RecordSnapshot
26
+ def apply_route: (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped], ?direct: bool) -> RecordSnapshot
27
27
 
28
28
  # : (Array[Array[untyped]], untyped) -> bool
29
29
  def unsafe_through_membership_event?: (Array[Array[untyped]], untyped) -> bool
@@ -31,8 +31,8 @@ module PaperTrailDiff
31
31
  # : (Array[Array[untyped]], ActivitySnapshotDelta?) -> bool
32
32
  def unsafe_nested_membership_delta?: (Array[Array[untyped]], ActivitySnapshotDelta?) -> bool
33
33
 
34
- # : (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped]) -> [untyped, RecordSnapshot?]
35
- def route_event_state: (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped]) -> [ untyped, RecordSnapshot? ]
34
+ # : (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped], ?direct: bool) -> [untyped, RecordSnapshot?]
35
+ def route_event_state: (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, Array[untyped], ?direct: bool) -> [ untyped, RecordSnapshot? ]
36
36
 
37
37
  # : (untyped, Array[untyped], SnapshotNormalizer, Array[untyped]) -> RecordSnapshot?
38
38
  def normalized_event_record: (untyped, Array[untyped], SnapshotNormalizer, Array[untyped]) -> RecordSnapshot?
@@ -37,8 +37,5 @@ module PaperTrailDiff
37
37
 
38
38
  # : (AssociationSnapshot, untyped, ActivityCollectionRouteChange) -> Integer?
39
39
  def nested_owner_position: (AssociationSnapshot, untyped, ActivityCollectionRouteChange) -> Integer?
40
-
41
- # : (RecordSnapshot, String, AssociationSnapshot) -> RecordSnapshot
42
- def replace_association: (RecordSnapshot, String, AssociationSnapshot) -> RecordSnapshot
43
40
  end
44
41
  end
@@ -5,11 +5,17 @@ module PaperTrailDiff
5
5
  class ActivityEventRecordResolver
6
6
  RECORD_AFTER_HANDLERS: untyped
7
7
 
8
- # : (?record_transition: untyped) -> void
9
- def initialize: (?record_transition: untyped) -> void
8
+ # : (?record_transition: untyped, ?prepared_record_after: untyped) -> void
9
+ def initialize: (?record_transition: untyped, ?prepared_record_after: untyped) -> void
10
+
11
+ # : (untyped, ?direct: bool) -> untyped
12
+ def record_after: (untyped, ?direct: bool) -> untyped
13
+
14
+ # : (untyped) -> [bool, untyped]
15
+ def direct_after: (untyped) -> [ bool, untyped ]
10
16
 
11
17
  # : (untyped) -> untyped
12
- def record_after: (untyped) -> untyped
18
+ def fallback_record_after: (untyped) -> untyped
13
19
 
14
20
  # : (untyped) -> ActivitySnapshotDelta?
15
21
  def snapshot_delta: (untyped) -> ActivitySnapshotDelta?
@@ -30,8 +36,18 @@ module PaperTrailDiff
30
36
 
31
37
  private
32
38
 
39
+ @direct_loader: DirectRecordStateLoader
40
+
41
+ @prepared_record_after: untyped
42
+
33
43
  @record_transition: untyped
34
44
 
45
+ # : (untyped) -> PreparedRecordState?
46
+ def direct_record_after: (untyped) -> PreparedRecordState?
47
+
48
+ # : (untyped) -> untyped
49
+ def prepared_changeset: (untyped) -> untyped
50
+
35
51
  # : (untyped, untyped) -> Hash[untyped, untyped]
36
52
  def after_attributes: (untyped, untyped) -> Hash[untyped, untyped]
37
53
 
@@ -3,8 +3,8 @@
3
3
  module PaperTrailDiff
4
4
  # Dispatches safe descendant events to focused immutable snapshot appliers.
5
5
  class ActivityEventSnapshotRefresher
6
- # : (traversal: AssociationTraversal, pool: SnapshotPool, components: untyped, ?association_reader: untyped, ?record_transition: untyped) -> void
7
- def initialize: (traversal: AssociationTraversal, pool: SnapshotPool, components: untyped, ?association_reader: untyped, ?record_transition: untyped) -> void
6
+ # : (traversal: AssociationTraversal, pool: SnapshotPool, components: untyped, ?association_reader: untyped, ?record_transition: untyped, ?prepared_record_after: untyped) -> void
7
+ def initialize: (traversal: AssociationTraversal, pool: SnapshotPool, components: untyped, ?association_reader: untyped, ?record_transition: untyped, ?prepared_record_after: untyped) -> void
8
8
 
9
9
  # : (SnapshotPool, untyped) -> void
10
10
  def configure_appliers: (SnapshotPool, untyped) -> void
@@ -6,6 +6,9 @@ module PaperTrailDiff
6
6
  # window -- but a transaction has to collapse the same way in both, or the
7
7
  # same history would read differently depending on how it was asked for.
8
8
  module ActivityGrouping
9
+ # : (Symbol?, ?activity: bool, ?snapshots: bool) -> void
10
+ def self.validate!: (Symbol?, ?activity: bool, ?snapshots: bool) -> void
11
+
9
12
  private
10
13
 
11
14
  # : () -> bool
@@ -11,8 +11,12 @@ module PaperTrailDiff
11
11
 
12
12
  attr_reader last_snapshot: RecordSnapshot?
13
13
 
14
- # : (steps: Array[ActivityStep], root_snapshots: Hash[Array[untyped], RecordSnapshot?], first_snapshot: RecordSnapshot?, last_snapshot: RecordSnapshot?) -> void
15
- def initialize: (steps: Array[ActivityStep], root_snapshots: Hash[Array[untyped], RecordSnapshot?], first_snapshot: RecordSnapshot?, last_snapshot: RecordSnapshot?) -> void
14
+ attr_reader last_boundary: ActivityBoundary?
15
+
16
+ attr_reader last_event_snapshot: RecordSnapshot?
17
+
18
+ # : (steps: Array[ActivityStep], root_snapshots: Hash[Array[untyped], RecordSnapshot?], first_snapshot: RecordSnapshot?, last_snapshot: RecordSnapshot?, ?last_boundary: ActivityBoundary?, ?last_event_snapshot: RecordSnapshot?) -> void
19
+ def initialize: (steps: Array[ActivityStep], root_snapshots: Hash[Array[untyped], RecordSnapshot?], first_snapshot: RecordSnapshot?, last_snapshot: RecordSnapshot?, ?last_boundary: ActivityBoundary?, ?last_event_snapshot: RecordSnapshot?) -> void
16
20
 
17
21
  # : () -> ActivityHistory
18
22
  def self.empty: () -> ActivityHistory
@@ -3,8 +3,8 @@
3
3
  module PaperTrailDiff
4
4
  # Optional full/partial snapshot operations consumed by the activity sequence.
5
5
  class ActivitySnapshotProvider
6
- # : (snapshotter: untyped, refresher: untyped, preparer: untyped) -> void
7
- def initialize: (snapshotter: untyped, refresher: untyped, preparer: untyped) -> void
6
+ # : (snapshotter: untyped, refresher: untyped, preparer: untyped, ?activity_index: untyped) -> void
7
+ def initialize: (snapshotter: untyped, refresher: untyped, preparer: untyped, ?activity_index: untyped) -> void
8
8
 
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
@@ -18,6 +18,11 @@ module PaperTrailDiff
18
18
  # : (untyped, untyped, RecordSnapshot?, event: ActivityEvent) -> RecordSnapshot?
19
19
  def advance_root: (untyped, untyped, RecordSnapshot?, event: ActivityEvent) -> RecordSnapshot?
20
20
 
21
+ # : () -> PreparedActivityIndex?
22
+ def activity_index: () -> PreparedActivityIndex?
23
+
24
+ @activity_index: untyped
25
+
21
26
  @preparer: untyped
22
27
 
23
28
  @refresher: untyped
@@ -44,6 +44,14 @@ module PaperTrailDiff
44
44
  # : () -> Array[ActivityStep]
45
45
  def build_to_current: () -> Array[ActivityStep]
46
46
 
47
+ # : () -> Analysis
48
+ def analyze_current: () -> Analysis
49
+
50
+ # The explicit live endpoint and a batch's live closing endpoint use the
51
+ # same selected plan. A version filter can close earlier, on its successor.
52
+ # : () -> [RootVersionPlan, Array[ActivityEvent], ActivityHistory, RecordSnapshot?, ActivityBoundary?]
53
+ def current_history: () -> [ RootVersionPlan, Array[ActivityEvent], ActivityHistory, RecordSnapshot?, ActivityBoundary? ]
54
+
47
55
  # : () -> [RecordSnapshot?, untyped]
48
56
  def capture_current: () -> [ RecordSnapshot?, untyped ]
49
57
 
@@ -56,9 +64,6 @@ module PaperTrailDiff
56
64
  # : (Array[untyped], ?end_at: untyped) -> void
57
65
  def prepare_history: (Array[untyped], ?end_at: untyped) -> void
58
66
 
59
- # : (Array[untyped], Array[ActivityEvent], ?current: untyped, ?final_boundary: ActivityBoundary?, ?final_snapshot: RecordSnapshot?) -> Array[ActivityStep]
60
- def build_event_steps: (Array[untyped], Array[ActivityEvent], ?current: untyped, ?final_boundary: ActivityBoundary?, ?final_snapshot: RecordSnapshot?) -> Array[ActivityStep]
61
-
62
67
  # Appends the transition into an explicit closing boundary, which is either
63
68
  # a requested current record or the absence a final root destroy leaves.
64
69
  # : (ActivityHistory, Array[ActivityEvent], ActivityBoundary?, RecordSnapshot?) -> Array[ActivityStep]
@@ -3,14 +3,16 @@
3
3
  module PaperTrailDiff
4
4
  # Collects root and selected-descendant versions relevant to one root history range.
5
5
  class ActivityVersionCollector
6
- # : (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?range_start: untyped, ?range_end: untyped) -> void
7
- def initialize: (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?range_start: untyped, ?range_end: untyped) -> void
6
+ # : (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?range_start: untyped, ?range_end: untyped, ?prepared: PreparedActivityIndex?) -> void
7
+ def initialize: (untyped, root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, ?range_start: untyped, ?range_end: untyped, ?prepared: PreparedActivityIndex?) -> void
8
8
 
9
9
  # : () -> Array[ActivityEvent]
10
10
  def call: () -> Array[ActivityEvent]
11
11
 
12
12
  private
13
13
 
14
+ @prepared: PreparedActivityIndex?
15
+
14
16
  @range: ActivityRange
15
17
 
16
18
  @record: untyped
@@ -58,11 +60,5 @@ module PaperTrailDiff
58
60
 
59
61
  # : (untyped, untyped) -> untyped
60
62
  def target_class: (untyped, untyped) -> untyped
61
-
62
- # : (untyped) -> bool
63
- def in_range?: (untyped) -> bool
64
-
65
- # : (String, String) -> String
66
- def join_path: (String, String) -> String
67
63
  end
68
64
  end
@@ -26,7 +26,7 @@ module PaperTrailDiff
26
26
  # Reconstructed endpoint states are opt-in, because they carry the whole
27
27
  # selected graph whether or not anything changed, which dwarfs the rest of
28
28
  # the payload for a wide graph.
29
- # : (?snapshots: bool) -> Hash[Symbol, untyped]
30
- def to_h: (?snapshots: bool) -> Hash[Symbol, untyped]
29
+ # : (?snapshots: bool, ?metadata: bool) -> Hash[Symbol, untyped]
30
+ def to_h: (?snapshots: bool, ?metadata: bool) -> Hash[Symbol, untyped]
31
31
  end
32
32
  end
@@ -5,8 +5,8 @@ module PaperTrailDiff
5
5
  # versions and preparing their association history once for the whole batch
6
6
  # rather than once per root.
7
7
  class AnalysisBatch
8
- # : (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped, ?version_scope: untyped, ?close_on_current: bool) -> void
9
- def initialize: (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped, ?version_scope: untyped, ?close_on_current: bool) -> void
8
+ # : (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped, ?version_scope: untyped, ?close_on_current: bool, ?context_required: bool) -> void
9
+ def initialize: (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped, ?version_scope: untyped, ?close_on_current: bool, ?context_required: bool) -> void
10
10
 
11
11
  # : () -> Hash[identity, Analysis]
12
12
  def call: () -> Hash[identity, Analysis]
@@ -17,6 +17,8 @@ module PaperTrailDiff
17
17
 
18
18
  @close_on_current: bool
19
19
 
20
+ @context_required: bool
21
+
20
22
  @history_preparer: untyped
21
23
 
22
24
  @live_loader: untyped
@@ -43,8 +45,17 @@ module PaperTrailDiff
43
45
  # : (Array[untyped], Hash[Array[String], RootVersionPlan], Hash[Array[String], untyped]) -> void
44
46
  def prepare: (Array[untyped], Hash[Array[String], RootVersionPlan], Hash[Array[String], untyped]) -> void
45
47
 
48
+ # : (Array[untyped], Hash[Array[String], RootVersionPlan]) -> bool
49
+ def closes_live?: (Array[untyped], Hash[Array[String], RootVersionPlan]) -> bool
50
+
51
+ # A plan closing on the live record reads history up to now; the others
52
+ # stop at the window, so a past window does not load everything since.
53
+ # : (Array[untyped], ?live: bool) -> Hash[Symbol, untyped]
54
+ def preparation_range: (Array[untyped], ?live: bool) -> Hash[Symbol, untyped]
55
+
46
56
  # A root with no versions in range has nothing to report, which is an empty
47
- # result rather than a failed request.
57
+ # result rather than a failed request -- unless an activity view closes it
58
+ # on current state, where its descendants may still have moved.
48
59
  # : (untyped, RootVersionPlan) -> Analysis
49
60
  def analysis_for: (untyped, RootVersionPlan) -> Analysis
50
61
  end
@@ -3,6 +3,11 @@
3
3
  module PaperTrailDiff
4
4
  # Resolves and validates ActiveRecord reflections for a bounded association tree.
5
5
  class AssociationTraversal
6
+ # The class a `belongs_to` points at for this record, reading a polymorphic
7
+ # type column when there is one. Nil when that type names no class.
8
+ # : (untyped, untyped) -> untyped
9
+ def self.belongs_to_target: (untyped, untyped) -> untyped
10
+
6
11
  # : (AssociationTree) -> void
7
12
  def initialize: (AssociationTree) -> void
8
13
 
@@ -18,6 +23,12 @@ module PaperTrailDiff
18
23
  # : (untyped) -> Array[String]
19
24
  def habtm_paths: (untyped) -> Array[String]
20
25
 
26
+ # HABTM join rows have no versions of their own, so membership changes
27
+ # cannot be split into events once a range runs past the last root version
28
+ # into live state.
29
+ # : (untyped) -> void
30
+ def reject_live_habtm_activity!: (untyped) -> void
31
+
21
32
  # : (untyped, AssociationTree, path: String) -> Array[untyped]
22
33
  def reflections_for: (untyped, AssociationTree, path: String) -> Array[untyped]
23
34
 
@@ -1,7 +1,7 @@
1
1
  # Generated from lib/paper_trail_diff/batch_boundary_resolver.rb with RBS::Inline
2
2
 
3
3
  module PaperTrailDiff
4
- # Resolves `:first` and `:last` endpoints for a whole batch in two queries per
4
+ # Resolves `:first` endpoints for a whole batch in a fixed number of queries per
5
5
  # model class. Left to the caller this is a per-root lookup, which reintroduces
6
6
  # exactly the queries a batched comparison exists to remove.
7
7
  class BatchBoundaryResolver
@@ -46,15 +46,12 @@ module PaperTrailDiff
46
46
  # : (Hash[Array[String], Hash[Symbol, untyped]], untyped, Array[untyped]) -> void
47
47
  def add_model_boundaries: (Hash[Array[String], Hash[Symbol, untyped]], untyped, Array[untyped]) -> void
48
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
49
+ # Each root's earliest version by time, in one query whatever the size of
50
+ # the batch. The lowest id is not the earliest version when ids are not
51
+ # sequential, as with UUIDs, so time decides and the usual ordering breaks
52
+ # a tie -- refusing one that ids cannot order.
53
+ # : (untyped, Array[untyped]) -> Hash[String, untyped]
54
+ def first_versions: (untyped, Array[untyped]) -> Hash[String, untyped]
58
55
 
59
56
  # : (untyped, untyped) -> Array[String]
60
57
  def identity_key: (untyped, untyped) -> Array[String]
@@ -0,0 +1,24 @@
1
+ # Generated from lib/paper_trail_diff/batch_safety.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Refuses known per-record association loading before a batch reconstructs it.
5
+ class BatchSafety
6
+ # : (AssociationTraversal, allow_expensive: bool) -> void
7
+ def initialize: (AssociationTraversal, allow_expensive: bool) -> void
8
+
9
+ # : (untyped, ?historical: bool) -> void
10
+ def call: (untyped, ?historical: bool) -> void
11
+
12
+ private
13
+
14
+ @allow_expensive: bool
15
+
16
+ @traversal: AssociationTraversal
17
+
18
+ # : (untyped, String, bool) -> void
19
+ def check: (untyped, String, bool) -> void
20
+
21
+ # : (untyped, bool) -> String?
22
+ def reason_for: (untyped, bool) -> String?
23
+ end
24
+ end