paper_trail_diff 0.14.0 → 0.15.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +343 -0
  3. data/QUICKSTART.md +21 -4
  4. data/README.md +58 -55
  5. data/docs/architecture.md +32 -11
  6. data/docs/performance.md +116 -391
  7. data/docs/reference.md +345 -106
  8. data/docs/testing.md +63 -7
  9. data/lib/paper_trail_diff/activity_boundary.rb +83 -26
  10. data/lib/paper_trail_diff/activity_child_candidate_loader.rb +2 -1
  11. data/lib/paper_trail_diff/activity_event.rb +41 -10
  12. data/lib/paper_trail_diff/activity_history.rb +34 -9
  13. data/lib/paper_trail_diff/activity_range.rb +18 -3
  14. data/lib/paper_trail_diff/activity_snapshot_sequence.rb +76 -37
  15. data/lib/paper_trail_diff/activity_target_candidate_loader.rb +5 -5
  16. data/lib/paper_trail_diff/activity_timeline_builder.rb +266 -111
  17. data/lib/paper_trail_diff/activity_transaction_grouper.rb +2 -1
  18. data/lib/paper_trail_diff/activity_tree_filter.rb +114 -0
  19. data/lib/paper_trail_diff/activity_version_collector.rb +7 -5
  20. data/lib/paper_trail_diff/activity_view.rb +69 -0
  21. data/lib/paper_trail_diff/analysis.rb +13 -1
  22. data/lib/paper_trail_diff/analysis_batch.rb +24 -30
  23. data/lib/paper_trail_diff/approximate_paths.rb +48 -0
  24. data/lib/paper_trail_diff/association_activity_edge.rb +6 -6
  25. data/lib/paper_trail_diff/association_discovery.rb +9 -1
  26. data/lib/paper_trail_diff/association_key.rb +56 -0
  27. data/lib/paper_trail_diff/association_traversal.rb +13 -4
  28. data/lib/paper_trail_diff/batch_boundary_resolver.rb +3 -4
  29. data/lib/paper_trail_diff/batch_preload.rb +53 -0
  30. data/lib/paper_trail_diff/batched_root_analyzer.rb +12 -14
  31. data/lib/paper_trail_diff/batched_root_versions.rb +38 -14
  32. data/lib/paper_trail_diff/change_row.rb +133 -0
  33. data/lib/paper_trail_diff/collection_comparator.rb +5 -32
  34. data/lib/paper_trail_diff/configuration.rb +35 -12
  35. data/lib/paper_trail_diff/deleted_analysis_batch.rb +18 -18
  36. data/lib/paper_trail_diff/deleted_root_filter.rb +79 -0
  37. data/lib/paper_trail_diff/deprecation.rb +61 -0
  38. data/lib/paper_trail_diff/diagnostics.rb +84 -99
  39. data/lib/paper_trail_diff/endpoint.rb +26 -3
  40. data/lib/paper_trail_diff/errors.rb +3 -1
  41. data/lib/paper_trail_diff/fallback_paths.rb +38 -0
  42. data/lib/paper_trail_diff/historical_association_reifier.rb +2 -4
  43. data/lib/paper_trail_diff/historical_filter.rb +19 -10
  44. data/lib/paper_trail_diff/historical_snapshot_store.rb +57 -67
  45. data/lib/paper_trail_diff/live_endpoint_batch_loader.rb +1 -35
  46. data/lib/paper_trail_diff/nested_comparator.rb +19 -18
  47. data/lib/paper_trail_diff/paper_trail_adapter.rb +122 -127
  48. data/lib/paper_trail_diff/preloaded_endpoint_batch_loader.rb +13 -1
  49. data/lib/paper_trail_diff/prepared_activity_index.rb +1 -1
  50. data/lib/paper_trail_diff/prepared_edge_loader.rb +2 -1
  51. data/lib/paper_trail_diff/prepared_history.rb +9 -1
  52. data/lib/paper_trail_diff/prepared_history_loader.rb +1 -0
  53. data/lib/paper_trail_diff/prepared_record_index.rb +38 -7
  54. data/lib/paper_trail_diff/root_version_selection.rb +4 -2
  55. data/lib/paper_trail_diff/scope_condition.rb +76 -21
  56. data/lib/paper_trail_diff/scope_ranking.rb +12 -2
  57. data/lib/paper_trail_diff/scoped_analysis.rb +11 -0
  58. data/lib/paper_trail_diff/scoped_analysis_builder.rb +16 -8
  59. data/lib/paper_trail_diff/scoped_root_selection.rb +18 -12
  60. data/lib/paper_trail_diff/snapshot.rb +16 -1
  61. data/lib/paper_trail_diff/support.rb +29 -2
  62. data/lib/paper_trail_diff/time_range.rb +32 -10
  63. data/lib/paper_trail_diff/timeline_builder.rb +7 -7
  64. data/lib/paper_trail_diff/timeline_range.rb +9 -1
  65. data/lib/paper_trail_diff/timeline_snapshot_provider.rb +24 -17
  66. data/lib/paper_trail_diff/transaction_cut.rb +181 -0
  67. data/lib/paper_trail_diff/traversal_preparer.rb +134 -29
  68. data/lib/paper_trail_diff/version.rb +1 -1
  69. data/lib/paper_trail_diff/version_boundary.rb +37 -8
  70. data/lib/paper_trail_diff/version_query.rb +34 -10
  71. data/lib/paper_trail_diff/version_scope_filter.rb +9 -10
  72. data/lib/paper_trail_diff/version_sequence_diagnostics.rb +31 -16
  73. data/lib/paper_trail_diff/version_table.rb +60 -0
  74. data/lib/paper_trail_diff.rb +98 -56
  75. data/sig/generated/paper_trail_diff/activity_boundary.rbs +54 -19
  76. data/sig/generated/paper_trail_diff/activity_event.rbs +25 -4
  77. data/sig/generated/paper_trail_diff/activity_history.rbs +21 -6
  78. data/sig/generated/paper_trail_diff/activity_range.rbs +9 -2
  79. data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +48 -9
  80. data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +134 -30
  81. data/sig/generated/paper_trail_diff/activity_tree_filter.rbs +73 -0
  82. data/sig/generated/paper_trail_diff/activity_version_collector.rbs +2 -0
  83. data/sig/generated/paper_trail_diff/activity_view.rbs +42 -0
  84. data/sig/generated/paper_trail_diff/analysis.rbs +8 -1
  85. data/sig/generated/paper_trail_diff/analysis_batch.rbs +8 -15
  86. data/sig/generated/paper_trail_diff/approximate_paths.rbs +23 -0
  87. data/sig/generated/paper_trail_diff/association_discovery.rbs +5 -0
  88. data/sig/generated/paper_trail_diff/association_key.rbs +29 -0
  89. data/sig/generated/paper_trail_diff/association_traversal.rbs +4 -0
  90. data/sig/generated/paper_trail_diff/batch_preload.rbs +29 -0
  91. data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +9 -10
  92. data/sig/generated/paper_trail_diff/batched_root_versions.rbs +14 -4
  93. data/sig/generated/paper_trail_diff/change_row.rbs +86 -0
  94. data/sig/generated/paper_trail_diff/collection_comparator.rbs +2 -16
  95. data/sig/generated/paper_trail_diff/configuration.rbs +19 -3
  96. data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +8 -8
  97. data/sig/generated/paper_trail_diff/deleted_root_filter.rbs +48 -0
  98. data/sig/generated/paper_trail_diff/deprecation.rbs +35 -0
  99. data/sig/generated/paper_trail_diff/diagnostics.rbs +37 -30
  100. data/sig/generated/paper_trail_diff/endpoint.rbs +11 -2
  101. data/sig/generated/paper_trail_diff/errors.rbs +3 -1
  102. data/sig/generated/paper_trail_diff/fallback_paths.rbs +22 -0
  103. data/sig/generated/paper_trail_diff/historical_filter.rbs +12 -2
  104. data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +33 -28
  105. data/sig/generated/paper_trail_diff/live_endpoint_batch_loader.rbs +0 -15
  106. data/sig/generated/paper_trail_diff/nested_comparator.rbs +10 -7
  107. data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +54 -52
  108. data/sig/generated/paper_trail_diff/preloaded_endpoint_batch_loader.rbs +9 -1
  109. data/sig/generated/paper_trail_diff/prepared_history.rbs +5 -0
  110. data/sig/generated/paper_trail_diff/prepared_record_index.rbs +19 -2
  111. data/sig/generated/paper_trail_diff/root_version_selection.rbs +2 -0
  112. data/sig/generated/paper_trail_diff/scope_condition.rbs +43 -9
  113. data/sig/generated/paper_trail_diff/scope_ranking.rbs +8 -0
  114. data/sig/generated/paper_trail_diff/scoped_analysis.rbs +6 -0
  115. data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +6 -1
  116. data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +8 -4
  117. data/sig/generated/paper_trail_diff/snapshot.rbs +8 -0
  118. data/sig/generated/paper_trail_diff/support.rbs +15 -0
  119. data/sig/generated/paper_trail_diff/time_range.rbs +20 -6
  120. data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
  121. data/sig/generated/paper_trail_diff/timeline_range.rbs +2 -0
  122. data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +17 -7
  123. data/sig/generated/paper_trail_diff/transaction_cut.rbs +94 -0
  124. data/sig/generated/paper_trail_diff/traversal_preparer.rbs +66 -8
  125. data/sig/generated/paper_trail_diff/version_boundary.rbs +20 -3
  126. data/sig/generated/paper_trail_diff/version_query.rbs +18 -2
  127. data/sig/generated/paper_trail_diff/version_scope_filter.rbs +4 -5
  128. data/sig/generated/paper_trail_diff/version_sequence_diagnostics.rbs +16 -9
  129. data/sig/generated/paper_trail_diff/version_table.rbs +30 -0
  130. data/sig/generated/paper_trail_diff.rbs +29 -26
  131. metadata +46 -10
  132. data/lib/paper_trail_diff/activity_grouping.rb +0 -38
  133. data/lib/paper_trail_diff/batch_safety.rb +0 -63
  134. data/lib/paper_trail_diff/time_activity_timeline_builder.rb +0 -223
  135. data/sig/generated/paper_trail_diff/activity_grouping.rbs +0 -22
  136. data/sig/generated/paper_trail_diff/batch_safety.rbs +0 -30
  137. data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +0 -92
@@ -70,6 +70,14 @@ module PaperTrailDiff
70
70
  # : (Array[RecordSnapshot], before: RecordSnapshot?, after: RecordSnapshot?, membership_preserved: bool) -> AssociationSnapshot
71
71
  def transition_to: (Array[RecordSnapshot], before: RecordSnapshot?, after: RecordSnapshot?, membership_preserved: bool) -> AssociationSnapshot
72
72
 
73
+ # Collections are documented in record type, then ID, order wherever they
74
+ # were read from. Most arrive sorted, so they are checked before sorting.
75
+ # : (Array[RecordSnapshot]) -> Array[RecordSnapshot]
76
+ def ordered: (Array[RecordSnapshot]) -> Array[RecordSnapshot]
77
+
78
+ # : (Array[Array[untyped]]) -> bool
79
+ def sorted?: (Array[Array[untyped]]) -> bool
80
+
73
81
  # : (AssociationSnapshot) -> CollectionTransition?
74
82
  def transition_from: (AssociationSnapshot) -> CollectionTransition?
75
83
 
@@ -12,6 +12,13 @@ module PaperTrailDiff
12
12
  # : (untyped) -> untyped
13
13
  def self?.serialize: (untyped) -> untyped
14
14
 
15
+ # Records sort by type first so that mixed id types stay comparable, then
16
+ # naturally within one type. Ordering by the printed form instead would put
17
+ # id 10 before id 2, which is deterministic but reads as unsorted wherever a
18
+ # result is rendered.
19
+ # : (untyped, untyped) -> Array[untyped]
20
+ def self?.sortable_identity: (untyped, untyped) -> Array[untyped]
21
+
15
22
  # : (untyped) -> Array[untyped]
16
23
  def self?.chronological_version_key: (untyped) -> Array[untyped]
17
24
 
@@ -28,6 +35,14 @@ module PaperTrailDiff
28
35
  # : (Array[untyped]) -> Array[untyped]?
29
36
  def self?.ambiguous_pair: (Array[untyped]) -> Array[untyped]?
30
37
 
38
+ # Ids order versions saved in one instant only when they count up in one
39
+ # table. Two version tables count separately, so ids there say nothing.
40
+ # : (untyped, untyped) -> bool
41
+ def self?.orderable_tie?: (untyped, untyped) -> bool
42
+
43
+ # : (untyped) -> untyped
44
+ def self?.version_table: (untyped) -> untyped
45
+
31
46
  # Versions sharing a timestamp, whether or not their ids order them. PT-AT
32
47
  # indexes association membership per version but resolves it by timestamp,
33
48
  # so association state cannot be told apart across such a pair even when the
@@ -2,6 +2,10 @@
2
2
 
3
3
  module PaperTrailDiff
4
4
  # Validates and applies a finite wall-clock range without loading model state.
5
+ #
6
+ # A Date is a whole day and a String a local time, both in Time.zone, as
7
+ # Rails reads them, or in UTC without one: never the server's own zone. An
8
+ # inclusive Date end runs through that day.
5
9
  class TimeRange
6
10
  attr_reader begin_time: Time
7
11
 
@@ -41,12 +45,22 @@ module PaperTrailDiff
41
45
  # : (untyped, boundary: Symbol) -> Time
42
46
  def coerce_time: (untyped, boundary: Symbol) -> Time
43
47
 
44
- # Active Support 8.0 and older warn, once per process, that `to_time` will
45
- # change on a zoned time, so read zoned values without it. Every branch
46
- # gives the same instant, so no spec (and no mutation) can tell them apart.
47
- # mutant:disable
48
- # : (untyped) -> Time
49
- def time_value: (untyped) -> Time
48
+ # Anything else names its instant, and `getutc` reads it without
49
+ # `to_time`, which Active Support 8.0 and older warn will change on a
50
+ # zoned time, and without changing the caller's own value as `utc` does.
51
+ # : (untyped) -> untyped
52
+ def time_value: (untyped) -> untyped
53
+
54
+ # An inclusive date end runs through the day: to the next one's start.
55
+ # : (Range[untyped]) -> untyped
56
+ def end_value: (Range[untyped]) -> untyped
57
+
58
+ # A DateTime is a Date too, but names its instant.
59
+ # : (untyped) -> bool
60
+ def calendar_date?: (untyped) -> bool
61
+
62
+ # : () -> untyped
63
+ def zone: () -> untyped
50
64
 
51
65
  # : () -> void
52
66
  def validate_order!: () -> void
@@ -23,8 +23,8 @@ module PaperTrailDiff
23
23
  # : (RootVersionPlan) -> [Array[Step], RecordSnapshot?, RecordSnapshot?]
24
24
  def compare_history: (RootVersionPlan) -> [ Array[Step], RecordSnapshot?, RecordSnapshot? ]
25
25
 
26
- # : (RootVersionPlan) -> Array[Step]
27
- def build_steps: (RootVersionPlan) -> Array[Step]
26
+ # : (RootVersionPlan, untyped) -> Array[Step]
27
+ def build_steps: (RootVersionPlan, untyped) -> Array[Step]
28
28
 
29
29
  # : () -> [Array[Step], nil, nil]
30
30
  def empty_history: () -> [ Array[Step], nil, nil ]
@@ -73,6 +73,8 @@ module PaperTrailDiff
73
73
  # : () -> untyped
74
74
  def ordered_versions: () -> untyped
75
75
 
76
+ # A batch passes each root the window it already moved to transaction
77
+ # edges; a single record's window is moved here.
76
78
  # : (untyped) -> TimeRange?
77
79
  def build_time_range: (untyped) -> TimeRange?
78
80
 
@@ -1,23 +1,33 @@
1
1
  # Generated from lib/paper_trail_diff/timeline_snapshot_provider.rb with RBS::Inline
2
2
 
3
3
  module PaperTrailDiff
4
- # Gives TimelineBuilder one-argument access to a range-prepared snapshot store.
4
+ # Prepares the snapshot store for TimelineBuilder, one root at a time.
5
5
  class TimelineSnapshotProvider
6
6
  # : (HistoricalSnapshotStore, live_snapshotter: untyped) -> void
7
7
  def initialize: (HistoricalSnapshotStore, live_snapshotter: untyped) -> void
8
8
 
9
- # : (untyped, Array[untyped]) -> void
10
- def prepare: (untyped, Array[untyped]) -> void
9
+ # : (untyped, Array[untyped]) -> PreparedTimelineSnapshots
10
+ def prepare: (untyped, Array[untyped]) -> PreparedTimelineSnapshots
11
+
12
+ @live_snapshotter: untyped
13
+
14
+ @store: HistoricalSnapshotStore
15
+ end
16
+
17
+ # One root's timeline snapshots, read with the history prepared for it.
18
+ # Adjacent steps share endpoints, so each immutable snapshot, including a
19
+ # nil create state, is kept for this root only and released with it, and
20
+ # batch memory does not retain every reconstructed graph.
21
+ class PreparedTimelineSnapshots
22
+ # : (PreparedHistory?, store: HistoricalSnapshotStore, live_snapshotter: untyped) -> void
23
+ def initialize: (PreparedHistory?, store: HistoricalSnapshotStore, live_snapshotter: untyped) -> void
11
24
 
12
25
  # A window closing on current state ends at the live record rather than at a
13
26
  # version, which is the one endpoint no version history can reconstruct.
14
27
  # : (untyped) -> RecordSnapshot?
15
28
  def call: (untyped) -> RecordSnapshot?
16
29
 
17
- private
18
-
19
- # : (untyped) -> RecordSnapshot?
20
- def live_snapshot: (untyped) -> RecordSnapshot?
30
+ @history: PreparedHistory?
21
31
 
22
32
  @live_snapshotter: untyped
23
33
 
@@ -0,0 +1,94 @@
1
+ # Generated from lib/paper_trail_diff/transaction_cut.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # Moves each edge of a wall-clock window that falls inside one of a root's
5
+ # transactions back to where that transaction began, so the transaction is
6
+ # wholly in the later window.
7
+ #
8
+ # PT-AT reads a root's associations at the start of the transaction of the
9
+ # root version it reifies. A window starting just after a child saved its
10
+ # part of such a transaction therefore starts from the state before that
11
+ # child's change, and one ending there ends at it too, so the change belonged
12
+ # to no window and surfaced at the next transaction, under its author.
13
+ #
14
+ # Only the root's own versions on either side of an edge are consulted:
15
+ # rows locked by an update keep two transactions of one root from
16
+ # interleaving, so a transaction holding a root version and crossing the edge
17
+ # holds the root's nearest version on one side of it. Each edge is moved from
18
+ # where it was asked for, never from where another edge moved, so adjacent
19
+ # windows move their shared edge alike.
20
+ module TransactionCut
21
+ # One edge of a window. A strict edge keeps versions recorded at its
22
+ # instant on the left, as an inclusive end does. A plain class rather than
23
+ # Data.define, which arrived in Ruby 3.2 while this gem supports 3.1.
24
+ class Edge
25
+ # : (Time, bool) -> void
26
+ def initialize: (Time, bool) -> void
27
+
28
+ # : (untyped) -> untyped
29
+ def left: (untyped) -> untyped
30
+
31
+ # : (untyped) -> untyped
32
+ def right: (untyped) -> untyped
33
+
34
+ # : (untyped, later: bool) -> untyped
35
+ def side: (untyped, later: bool) -> untyped
36
+
37
+ @strict: bool
38
+
39
+ @time: Time
40
+ end
41
+
42
+ # Roots per statement: two lookups each stays under SQLite's limit of 500
43
+ # terms in a compound SELECT.
44
+ SLICE: ::Integer
45
+
46
+ # The same as `windows` for a batch of roots, keyed by identity.
47
+ # : (Array[untyped], TimeRange?) -> Hash[Array[String], TimeRange]
48
+ def self.batch_windows: (Array[untyped], TimeRange?) -> Hash[Array[String], TimeRange]
49
+
50
+ # Where history for a batch must be read from: the earliest moved start.
51
+ # : (TimeRange?, Hash[Array[String], TimeRange]) -> Time?
52
+ def self.earliest_start: (TimeRange?, Hash[Array[String], TimeRange]) -> Time?
53
+
54
+ # The windows that move, keyed by root id. A root missing from the result
55
+ # keeps the window as asked.
56
+ # : (untyped, Array[untyped], TimeRange) -> Hash[String, TimeRange]
57
+ def self.windows: (untyped, Array[untyped], TimeRange) -> Hash[String, TimeRange]
58
+
59
+ # : (untyped) -> untyped
60
+ def self.transaction_versions: (untyped) -> untyped
61
+
62
+ # Each root's earliest start among its transactions that cross the edge.
63
+ # : (untyped, Array[untyped], Edge) -> Hash[String, Time]
64
+ def self.moved_edges: (untyped, Array[untyped], Edge) -> Hash[String, Time]
65
+
66
+ # The roots whose versions next to the edge each transaction holds.
67
+ # : (untyped, Array[untyped], Edge) -> Hash[untyped, Array[String]]
68
+ def self.nearest_roots: (untyped, Array[untyped], Edge) -> Hash[untyped, Array[String]]
69
+
70
+ # Each root's nearest versions on either side of an edge, as [id,
71
+ # transaction] pairs; versions tied at the nearest instant are all kept.
72
+ # One indexed lookup per root and side, where a single query over the
73
+ # roots would read every version before the edge. The lookups are written
74
+ # once with the root's id left open, as building a relation for each root
75
+ # costs more than the query. Rows read as SQL text come back as the adapter
76
+ # returns them, so the transaction is cast as the grouped query's keys are.
77
+ # : (untyped, Array[untyped], Edge) -> Array[[String, untyped]]
78
+ def self.nearest: (untyped, Array[untyped], Edge) -> Array[[ String, untyped ]]
79
+
80
+ # Both sides' lookups for a root whose id is each `?`.
81
+ # : (untyped, Edge) -> String
82
+ def self.nearest_lookups: (untyped, Edge) -> String
83
+
84
+ # : (untyped, Edge, bool) -> String
85
+ def self.nearest_lookup: (untyped, Edge, bool) -> String
86
+
87
+ # Where each transaction that has versions on both sides of the edge began.
88
+ # : (untyped, Array[untyped], Edge) -> Hash[untyped, Time]
89
+ def self.crossing_starts: (untyped, Array[untyped], Edge) -> Hash[untyped, Time]
90
+
91
+ # : (TimeRange, Time?, Time?) -> TimeRange
92
+ def self.moved: (TimeRange, Time?, Time?) -> TimeRange
93
+ end
94
+ end
@@ -3,30 +3,88 @@
3
3
  module PaperTrailDiff
4
4
  # Checks that a model can be traversed before any reconstruction starts, and
5
5
  # that association tracking is actually available when history is involved.
6
+ # `diagnose` reports the same refusals instead of raising the first.
6
7
  class TraversalPreparer
7
- # : (tree: AssociationTree, traversal: AssociationTraversal) -> void
8
- def initialize: (tree: AssociationTree, traversal: AssociationTraversal) -> void
8
+ # One reason to refuse a request: the error `call` raises, and the code
9
+ # and path `diagnose` reports.
10
+ class Refusal
11
+ attr_reader error: untyped
12
+
13
+ attr_reader code: Symbol
14
+
15
+ attr_reader message: String
16
+
17
+ attr_reader path: String?
18
+
19
+ # : (untyped, Symbol, String, ?String?) -> void
20
+ def initialize: (untyped, Symbol, String, ?String?) -> void
21
+ end
22
+
23
+ PROBLEMS: untyped
24
+
25
+ # Each model a path's history needs versions of and has none, with the
26
+ # refusal's code: the target, then each model a has_many :through goes
27
+ # through. A polymorphic target is not known until a row names it;
28
+ # `diagnose` reports that separately rather than guessing here.
29
+ # : (untyped) -> Array[[Symbol, untyped]]
30
+ def self.unversioned_models: (untyped) -> Array[[ Symbol, untyped ]]
31
+
32
+ # A has_many :through reads past membership from the versions of the
33
+ # models it goes through, so each of them needs history too.
34
+ # : (untyped) -> Array[untyped]
35
+ def self.through_models: (untyped) -> Array[untyped]
36
+
37
+ # : (tree: AssociationTree, traversal: AssociationTraversal, ?allow_approximate: bool) -> void
38
+ def initialize: (tree: AssociationTree, traversal: AssociationTraversal, ?allow_approximate: bool) -> void
9
39
 
10
40
  # : (untyped, historical: bool) -> void
11
41
  def call: (untyped, historical: bool) -> void
12
42
 
43
+ # Every reason to refuse a request over the model, in the order `call`
44
+ # raises them. Live-to-live comparison reads current state, so only the
45
+ # traversal itself is checked for it.
46
+ # : (untyped, historical: bool) -> Array[Refusal]
47
+ def refusals: (untyped, historical: bool) -> Array[Refusal]
48
+
13
49
  private
14
50
 
51
+ @allow_approximate: bool
52
+
53
+ @refusals: Hash[Array[untyped], Array[Refusal]]
54
+
15
55
  @traversal: AssociationTraversal
16
56
 
17
57
  @tree: AssociationTree
18
58
 
59
+ # : (untyped, bool) -> Array[Refusal]
60
+ def collect: (untyped, bool) -> Array[Refusal]
61
+
62
+ # : (untyped) -> Refusal?
63
+ def traversal_refusal: (untyped) -> Refusal?
64
+
65
+ # A path prepared history cannot read is read with PT-AT, which rebuilds
66
+ # an association from its current members, swapping in older versions of
67
+ # them. A record that entered the association since, such as one moved
68
+ # into a scope or restored from a soft delete, then reads as a member it
69
+ # was not, and one that left reads as missing. Only a caller who accepts
70
+ # that may read such a path.
71
+ # : (untyped) -> Array[Refusal]
72
+ def approximate_refusals: (untyped) -> Array[Refusal]
73
+
19
74
  # A model PaperTrail never versioned has no history to reconstruct, so a
20
75
  # comparison over it can only ever answer "nothing changed" -- which is a
21
76
  # wrong answer rather than an empty one. Live-to-live comparison reads
22
77
  # current state and is unaffected, so this applies to historical work only.
23
- # : (untyped) -> void
24
- def ensure_versioned_targets!: (untyped) -> void
78
+ # : (untyped) -> Array[Refusal]
79
+ def unversioned_refusals: (untyped) -> Array[Refusal]
80
+
81
+ # : (String, untyped, Symbol) -> Refusal
82
+ def unversioned: (String, untyped, Symbol) -> Refusal
25
83
 
26
- # : (untyped) -> bool
27
- def versioned?: (untyped) -> bool
84
+ # : () -> bool
85
+ def association_tracking?: () -> bool
28
86
 
29
- # : () -> void
30
- def ensure_association_tracking!: () -> void
87
+ # : () -> Refusal
88
+ def tracking_refusal: () -> Refusal
31
89
  end
32
90
  end
@@ -11,13 +11,30 @@ module PaperTrailDiff
11
11
  # : (untyped, untyped) -> bool
12
12
  def self?.at_or_after?: (untyped, untyped) -> bool
13
13
 
14
+ # A range starting at a version starts before that version's transaction,
15
+ # as PT-AT reads associations there. A version saved earlier in the same
16
+ # transaction, such as the child update behind a `touch: true` parent
17
+ # version, can be recorded before the start, so the start moves back to
18
+ # the earliest version of each root's first transaction in the range.
19
+ # History loaded from the start alone would read that child after its
20
+ # change.
21
+ # : (Array[untyped], untyped) -> untyped
22
+ def self?.transaction_start: (Array[untyped], untyped) -> untyped
23
+
24
+ # The transaction of each root's first version at or after the start.
25
+ # : (Array[untyped], untyped) -> Array[untyped]
26
+ def self?.opening_transactions: (Array[untyped], untyped) -> Array[untyped]
27
+
28
+ # A custom version class need not carry the column, and PaperTrail leaves
29
+ # it nil outside a transaction. Both read as "no transaction here", which
30
+ # groups with nothing.
31
+ # : (untyped) -> untyped
32
+ def self?.transaction_id: (untyped) -> untyped
33
+
14
34
  # : (untyped) -> bool
15
35
  def self?.version_record?: (untyped) -> bool
16
36
 
17
37
  # : (untyped, untyped) -> bool
18
38
  def self?.same_version?: (untyped, untyped) -> bool
19
-
20
- # : (untyped, untyped) -> bool
21
- def self?.orderable_tie?: (untyped, untyped) -> bool
22
39
  end
23
40
  end
@@ -9,13 +9,29 @@ module PaperTrailDiff
9
9
  # : (untyped) -> untyped
10
10
  def self?.chronological: (untyped) -> untyped
11
11
 
12
+ # Every version of a model's items. They are read from the version class
13
+ # rather than through the model's versions association, whose scope may
14
+ # leave some out: a history replayed without them credits their changes to
15
+ # the next version kept.
16
+ # : (untyped) -> untyped
17
+ def self?.for_model: (untyped) -> untyped
18
+
19
+ # The versions of the given items, read the same way.
20
+ # : (untyped, untyped) -> untyped
21
+ def self?.for_items: (untyped, untyped) -> untyped
22
+
12
23
  # Keeps each item's earliest version among those the block also accepts:
13
24
  # no other version of the same item that the block accepts is earlier. A
14
25
  # correlated NOT EXISTS stays portable where window functions are not.
15
26
  # : (untyped) ?{ (untyped) -> untyped } -> untyped
16
27
  def self?.no_earlier_version: (untyped) ?{ (untyped) -> untyped } -> untyped
17
28
 
18
- # : (untyped, untyped) ?{ (untyped) -> untyped } -> untyped
19
- def self?.earlier_version: (untyped, untyped) ?{ (untyped) -> untyped } -> untyped
29
+ # The same for either side of an instant: each item's earliest version
30
+ # when the versions lie after it, and its latest when they lie before.
31
+ # : (untyped, later: bool) ?{ (untyped) -> untyped } -> untyped
32
+ def self?.no_nearer_version: (untyped, later: bool) ?{ (untyped) -> untyped } -> untyped
33
+
34
+ # : (untyped, untyped, later: bool) ?{ (untyped) -> untyped } -> untyped
35
+ def self?.nearer_version: (untyped, untyped, later: bool) ?{ (untyped) -> untyped } -> untyped
20
36
  end
21
37
  end
@@ -6,6 +6,10 @@ module PaperTrailDiff
6
6
  # caller, because the one that reveals the last selected mutation is drawn
7
7
  # from them.
8
8
  class VersionScopeFilter
9
+ # A filter is a callable that narrows the version relation.
10
+ # : (untyped) -> untyped
11
+ def self.validated: (untyped) -> untyped
12
+
9
13
  # : (untyped) -> void
10
14
  def initialize: (untyped) -> void
11
15
 
@@ -16,11 +20,6 @@ module PaperTrailDiff
16
20
  # : (untyped) -> Set[untyped]?
17
21
  def chosen_ids: (untyped) -> Set[untyped]?
18
22
 
19
- private
20
-
21
23
  @scope: untyped
22
-
23
- # : (untyped) -> untyped
24
- def validated: (untyped) -> untyped
25
24
  end
26
25
  end
@@ -3,17 +3,17 @@
3
3
  module PaperTrailDiff
4
4
  # Checks the recorded sequence itself, before any question of association
5
5
  # setup: whether the versions can be ordered at all, and whether timestamps
6
- # they share hide changes that ordering alone cannot recover.
6
+ # they share hide changes from a path PT-AT reads.
7
7
  class VersionSequenceDiagnostics
8
- # : (untyped, untyped, ?associations_selected: bool) -> void
9
- def initialize: (untyped, untyped, ?associations_selected: bool) -> void
8
+ # : (untyped, untyped, ?fallback_selected: bool) -> void
9
+ def initialize: (untyped, untyped, ?fallback_selected: bool) -> void
10
10
 
11
11
  # : () -> Array[DiagnosticIssue]
12
12
  def call: () -> Array[DiagnosticIssue]
13
13
 
14
14
  private
15
15
 
16
- @associations_selected: bool
16
+ @fallback_selected: bool
17
17
 
18
18
  @from_version: untyped
19
19
 
@@ -25,15 +25,22 @@ module PaperTrailDiff
25
25
  # : (Array[untyped]) -> DiagnosticIssue?
26
26
  def unorderable: (Array[untyped]) -> DiagnosticIssue?
27
27
 
28
- # Association membership is recorded per version but resolved by timestamp,
29
- # so a tie hides any association change across that pair. Unlike an
30
- # unorderable sequence this is not always wrong: if nothing associated
31
- # changed between them the result is correct, and the gem cannot tell which
32
- # it is, because not seeing the change is the symptom. So it warns.
28
+ # Loaded history orders versions that share a timestamp by id, but PT-AT
29
+ # places association changes by timestamp alone, so a path it reads loses
30
+ # any change between such a pair. Unlike an unorderable sequence this is
31
+ # not always wrong: if nothing that path reaches changed between them the
32
+ # result is correct, and the gem cannot tell which it is, because not
33
+ # seeing the change is the symptom. So it warns.
33
34
  # : (Array[untyped]) -> DiagnosticIssue?
34
35
  def tied_timestamps: (Array[untyped]) -> DiagnosticIssue?
35
36
 
36
37
  # : () -> Array[untyped]
37
38
  def ordered_range_versions: () -> Array[untyped]
39
+
40
+ # Between the two versions, in either order, or from the one version on
41
+ # when the other endpoint is the current record. A version that recorded
42
+ # no time bounds nothing.
43
+ # : () -> Range[untyped]?
44
+ def time_range: () -> Range[untyped]?
38
45
  end
39
46
  end
@@ -0,0 +1,30 @@
1
+ # Generated from lib/paper_trail_diff/version_table.rb with RBS::Inline
2
+
3
+ module PaperTrailDiff
4
+ # PT-AT reads an association's history from its owner's version tables: it
5
+ # looks for a child's versions in the owner's version table, and numbers a
6
+ # transaction by the first version saved in it, in whichever table. A model
7
+ # whose versions, or version associations, live in another table than its
8
+ # owner's would read other records' versions or none, so it is refused.
9
+ module VersionTable
10
+ # Why the association, or a part of a through, cannot be read across
11
+ # version tables.
12
+ # : (untyped) -> String?
13
+ def self.unsupported_reason: (untyped) -> String?
14
+
15
+ # The reflection, or the first part of a through, whose ends keep versions
16
+ # in different tables. A polymorphic belongs_to names no single target,
17
+ # and an unversioned target has no history to compare.
18
+ # : (untyped) -> untyped
19
+ def self.separate_part: (untyped) -> untyped
20
+
21
+ # PT-AT's version association rows joined to the versions they were saved
22
+ # with. Its `belongs_to :version` names PaperTrail's default class whatever
23
+ # table a model's versions are in, so the join names the table instead.
24
+ # : (untyped, untyped) -> untyped
25
+ def self.joined: (untyped, untyped) -> untyped
26
+
27
+ # : (untyped) -> String
28
+ def self.tables: (untyped) -> String
29
+ end
30
+ end
@@ -9,36 +9,33 @@ module PaperTrailDiff
9
9
  SUPPORTED_ASSOCIATION_MACROS: untyped
10
10
 
11
11
  # Compares net state between explicit PaperTrail-version or current-record endpoints.
12
- # : (untyped, untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool) -> Diff
13
- def self.compare: (untyped, untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool) -> Diff
12
+ # : (untyped, untyped, ?associations: association_option, ?ignore: ignore_option, ?reload_live_endpoints: bool?, ?allow_approximate: bool) -> Diff
13
+ def self.compare: (untyped, untyped, ?associations: association_option, ?ignore: ignore_option, ?reload_live_endpoints: bool?, ?allow_approximate: bool) -> Diff
14
14
 
15
15
  # Compares many independent endpoint pairs while batching current-record loading.
16
- # : (Array[comparison_input], ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool, ?allow_expensive: bool) -> comparison_results
17
- def self.compare_many: (Array[comparison_input], ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool, ?allow_expensive: bool) -> comparison_results
16
+ # : (Array[comparison_input], ?associations: association_option, ?ignore: ignore_option, ?reload_live_endpoints: bool?, ?allow_expensive: bool?, ?allow_approximate: bool) -> comparison_results
17
+ def self.compare_many: (Array[comparison_input], ?associations: association_option, ?ignore: ignore_option, ?reload_live_endpoints: bool?, ?allow_expensive: bool?, ?allow_approximate: bool) -> comparison_results
18
18
 
19
19
  # Compares every adjacent reconstructed state in an inclusive version range,
20
20
  # by default the record's whole history through its current state.
21
- # : (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?version_scope: untyped, ?close_on: Symbol?) -> Array[Step]
22
- def self.timeline: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?version_scope: untyped, ?close_on: Symbol?) -> Array[Step]
21
+ # : (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: association_option, ?ignore: ignore_option, ?version_scope: untyped, ?close_on: Symbol?, ?allow_approximate: bool) -> Array[Step]
22
+ def self.timeline: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: association_option, ?ignore: ignore_option, ?version_scope: untyped, ?close_on: Symbol?, ?allow_approximate: bool) -> Array[Step]
23
23
 
24
24
  # Compares adjacent root and selected-descendant activity boundaries.
25
- # `reload_live_endpoints:` applies whenever the range ends at the current
26
- # record: an explicit `to:` record, the default range, or a window with no
27
- # later version.
28
- # : (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]
29
- def self.activity_timeline: (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]
25
+ # : (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: association_option, ?ignore: ignore_option, ?reload_live_endpoints: bool?, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?recorded_changes: bool, ?allow_approximate: bool) -> Array[ActivityStep]
26
+ def self.activity_timeline: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: association_option, ?ignore: ignore_option, ?reload_live_endpoints: bool?, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?recorded_changes: bool, ?allow_approximate: bool) -> Array[ActivityStep]
30
27
 
31
28
  # Builds an endpoint diff and root-checkpoint timeline while normalizing each version once.
32
- # : (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?) -> Analysis
33
- def self.analyze: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?) -> Analysis
29
+ # : (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: association_option, ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?recorded_changes: bool, ?allow_approximate: bool) -> Analysis
30
+ def self.analyze: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: association_option, ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?recorded_changes: bool, ?allow_approximate: bool) -> Analysis
34
31
 
35
32
  # Analyzes an array of records over one shared range, preparing their
36
33
  # selected history once for the batch instead of once per record, and
37
34
  # returns a frozen Hash keyed by identity. Roots with nothing to report in
38
35
  # the window return an empty `Analysis`. To let a relation choose the
39
36
  # records, use `analyze_scope`.
40
- # : (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]
41
- def self.analyze_many: (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]
37
+ # : (Array[untyped], ?within: untyped, ?associations: association_option, ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?allow_expensive: bool?, ?recorded_changes: bool, ?allow_approximate: bool) -> Hash[identity, Analysis]
38
+ def self.analyze_many: (Array[untyped], ?within: untyped, ?associations: association_option, ?ignore: ignore_option, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?allow_expensive: bool?, ?recorded_changes: bool, ?allow_approximate: bool) -> Hash[identity, Analysis]
42
39
 
43
40
  # Analyzes every root the relation reaches whose history moved inside the
44
41
  # window. With `activity: true`, a change to a record on one of the
@@ -66,15 +63,19 @@ module PaperTrailDiff
66
63
  # Note also that a relation selects on current state, not on state during
67
64
  # the window: `where(status: 'published')` means published *now*, which is a
68
65
  # different set from what was published while the window was open.
69
- # : (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
70
- def self.analyze_scope: (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
66
+ # : (untyped, limit: Integer?, ?within: untyped, ?associations: association_option, ?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?, ?recorded_changes: bool, ?allow_approximate: bool) -> ScopedAnalysis
67
+ def self.analyze_scope: (untyped, limit: Integer?, ?within: untyped, ?associations: association_option, ?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?, ?recorded_changes: bool, ?allow_approximate: bool) -> ScopedAnalysis
71
68
 
72
69
  # Recent root and selected-child activity through the current saved state,
73
70
  # for every root that changed or whose requested associations did.
74
71
  # Historical windows remain explicit on analyze_scope; this entry point is
75
72
  # deliberately named for a report through now rather than an as-of query.
76
- # : (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
77
- def self.report: (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
73
+ # : (untyped, since: untyped, limit: Integer, ?associations: association_option, ?ignore: ignore_option, ?snapshots: bool, ?group: Symbol?, ?include_deleted: bool, ?historical_filter: untyped, ?max_filter_versions: Integer, ?allow_expensive: bool?, ?recorded_changes: bool, ?allow_approximate: bool) -> ScopedAnalysis
74
+ def self.report: (untyped, since: untyped, limit: Integer, ?associations: association_option, ?ignore: ignore_option, ?snapshots: bool, ?group: Symbol?, ?include_deleted: bool, ?historical_filter: untyped, ?max_filter_versions: Integer, ?allow_expensive: bool?, ?recorded_changes: bool, ?allow_approximate: bool) -> ScopedAnalysis
75
+
76
+ # Warns about deprecated options; see Deprecation.
77
+ # : () -> untyped
78
+ def self.deprecator: () -> untyped
78
79
 
79
80
  # Returns the association macros this release can normalize.
80
81
  # : () -> Array[Symbol]
@@ -84,9 +85,11 @@ module PaperTrailDiff
84
85
  # : (untyped, ?max_depth: Integer) -> Array[AssociationDescriptor]
85
86
  def self.association_paths: (untyped, ?max_depth: Integer) -> Array[AssociationDescriptor]
86
87
 
87
- # Reports known reconstruction hazards without mutating application state.
88
- # : (untyped, untyped, ?associations: Array[String | Symbol]) -> DiagnosticReport
89
- def self.diagnose: (untyped, untyped, ?associations: Array[String | Symbol]) -> DiagnosticReport
88
+ # Reports known reconstruction hazards without mutating application state,
89
+ # for the endpoints `compare` takes: two versions, or a version and the
90
+ # current record. Every reason `compare` would refuse them is an error.
91
+ # : (untyped, untyped, ?associations: association_option, ?allow_approximate: bool) -> DiagnosticReport
92
+ def self.diagnose: (untyped, untyped, ?associations: association_option, ?allow_approximate: bool) -> DiagnosticReport
90
93
 
91
94
  # Looks inside an attribute the database stores whole, such as a JSON or
92
95
  # jsonb column, and reports which keys changed.
@@ -111,9 +114,7 @@ module PaperTrailDiff
111
114
 
112
115
  # Reads what PaperTrail recorded with one version: its `object_changes`,
113
116
  # typed by the version's model, as `{ "title" => <from "Draft" to "Published"> }`.
114
- #
115
- # versions = PaperTrail::Version.where(id: steps.map { _1.source_boundary.version_id })
116
- # versions.to_h { [_1.id, PaperTrailDiff.recorded_changes(_1)] }
117
+ # Activity methods read it for every step source with `recorded_changes: true`.
117
118
  #
118
119
  # A step's diff compares the states before and after an event, so it also
119
120
  # includes anything saved without a version in between; this does not.
@@ -149,10 +150,12 @@ module PaperTrailDiff
149
150
 
150
151
  type comparison_results = Hash[identity, Diff]
151
152
 
152
- type report_options = { within: untyped, activity: bool, version_scope: untyped, close_on: Symbol?, snapshots: bool, group: Symbol? }
153
+ type report_options = { within: untyped, activity: bool, version_scope: untyped, view: ActivityView }
153
154
 
154
155
  type ignore_option = Array[String | Symbol] | Hash[String | Symbol, untyped]
155
156
 
157
+ type association_option = String | Symbol | Array[untyped] | Hash[untyped, untyped]
158
+
156
159
  type reference_key = :type | :id | "type" | "id"
157
160
 
158
161
  type traversal_context = :change | :included_state