jekyll-relationships 0.1.0.alpha → 0.2.0.alpha
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.
- checksums.yaml +4 -4
- data/lib/jekyll-relationships/configuration/debug_setting.rb +72 -12
- data/lib/jekyll-relationships/configuration/defaults.rb +3 -1
- data/lib/jekyll-relationships/configuration/frontmatter.rb +28 -1
- data/lib/jekyll-relationships/configuration/parser.rb +20 -3
- data/lib/jekyll-relationships/configuration/prune_rule_settings.rb +33 -5
- data/lib/jekyll-relationships/configuration/tree_frontmatter.rb +8 -2
- data/lib/jekyll-relationships/configuration.rb +43 -8
- data/lib/jekyll-relationships/debug_logger.rb +94 -3
- data/lib/jekyll-relationships/definitions/normal_relationship.rb +13 -2
- data/lib/jekyll-relationships/definitions/prune_rule.rb +19 -3
- data/lib/jekyll-relationships/definitions/tree_relationship.rb +8 -2
- data/lib/jekyll-relationships/documents/registry.rb +182 -33
- data/lib/jekyll-relationships/engine/normal_seed.rb +222 -0
- data/lib/jekyll-relationships/engine/persisted_links.rb +365 -0
- data/lib/jekyll-relationships/engine/raw_path_state.rb +46 -116
- data/lib/jekyll-relationships/engine/relationship_state.rb +67 -56
- data/lib/jekyll-relationships/engine/session.rb +159 -19
- data/lib/jekyll-relationships/engine/write_back.rb +3 -42
- data/lib/jekyll-relationships/engine.rb +168 -46
- data/lib/jekyll-relationships/pruning/rule_pruner.rb +7 -7
- data/lib/jekyll-relationships/pruning/tree_phase.rb +128 -124
- data/lib/jekyll-relationships/pruning/tree_provenance.rb +1 -1
- data/lib/jekyll-relationships/references/accumulator.rb +87 -9
- data/lib/jekyll-relationships/references/template.rb +46 -11
- data/lib/jekyll-relationships/resolvers/base.rb +59 -2
- data/lib/jekyll-relationships/support/frontmatter_matcher.rb +76 -0
- data/lib/jekyll-relationships/support/placeholders.rb +1 -0
- data/lib/jekyll-relationships/trees/edge_builder.rb +43 -11
- data/lib/jekyll-relationships/trees/graph.rb +135 -17
- data/lib/jekyll-relationships/trees/root_distances.rb +44 -0
- data/lib/jekyll-relationships/version.rb +1 -1
- data/lib/jekyll-relationships.rb +1 -0
- data/readme.md +185 -26
- metadata +6 -2
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'jekyll-relationships/engine/session'
|
|
4
4
|
require 'jekyll-relationships/engine/raw_path_state'
|
|
5
5
|
require 'jekyll-relationships/engine/relationship_state'
|
|
6
|
+
require 'jekyll-relationships/engine/normal_seed'
|
|
7
|
+
require 'jekyll-relationships/engine/persisted_links'
|
|
6
8
|
require 'jekyll-relationships/engine/write_back'
|
|
7
9
|
require 'jekyll-relationships/trees/graph'
|
|
8
10
|
|
|
@@ -27,7 +29,9 @@ class Engine
|
|
|
27
29
|
@configuration = Configuration.new(@site.config)
|
|
28
30
|
return unless @configuration.enabled?
|
|
29
31
|
|
|
30
|
-
@debug_logger = DebugLogger.new
|
|
32
|
+
@debug_logger = DebugLogger.new(
|
|
33
|
+
reference_key_property: @configuration.reference_template.key_property
|
|
34
|
+
)
|
|
31
35
|
@run_logger = RunLogger.new
|
|
32
36
|
@registry = Documents::Registry.new(site: @site, collections: @configuration.collections)
|
|
33
37
|
@data_path = Jekyll::Plugins::Relationships::Support::DataPath.new
|
|
@@ -39,33 +43,33 @@ class Engine
|
|
|
39
43
|
return if @configuration.collections.empty?
|
|
40
44
|
|
|
41
45
|
@registry.validate_collections!
|
|
42
|
-
@registry.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
tree_graph: prune_result.fetch(:tree_graph)
|
|
46
|
+
@registry.validate_identity_schemes!(identity_schemes: @configuration.identity_schemes)
|
|
47
|
+
initial_active_document_ids = all_active_document_ids
|
|
48
|
+
initial_tree_seed_graph = build_tree_graph(active_document_ids: initial_active_document_ids)
|
|
49
|
+
normal_seed = NormalSeed.new(engine: self, active_document_ids: initial_active_document_ids)
|
|
50
|
+
persisted_links = PersistedLinks.new(engine: self)
|
|
51
|
+
final_result = final_result_for(
|
|
52
|
+
initial_active_document_ids: initial_active_document_ids,
|
|
53
|
+
initial_tree_seed_graph: initial_tree_seed_graph,
|
|
54
|
+
normal_seed: normal_seed,
|
|
55
|
+
persisted_links: persisted_links
|
|
53
56
|
)
|
|
54
|
-
final_session.
|
|
57
|
+
final_session = final_result.fetch(:session)
|
|
58
|
+
final_tree_graph = final_result.fetch(:tree_graph)
|
|
55
59
|
final_normal_graph = Pruning::NormalGraph.new(session: final_session)
|
|
56
60
|
|
|
57
61
|
log_relationship_summary(
|
|
58
62
|
session: final_session,
|
|
59
63
|
normal_graph: final_normal_graph,
|
|
60
|
-
tree_graph:
|
|
64
|
+
tree_graph: final_tree_graph
|
|
61
65
|
)
|
|
62
66
|
@run_logger.pruning_summary(
|
|
63
|
-
removed_documents_by_collection: removed_documents_by_collection(
|
|
67
|
+
removed_documents_by_collection: removed_documents_by_collection(final_result.fetch(:removed_documents))
|
|
64
68
|
) if @configuration.pruning_enabled?
|
|
65
69
|
|
|
66
70
|
final_session.write_back!
|
|
67
|
-
|
|
68
|
-
remove_inactive_documents!(active_document_ids:
|
|
71
|
+
final_tree_graph.write_back!
|
|
72
|
+
remove_inactive_documents!(active_document_ids: final_result.fetch(:active_document_ids))
|
|
69
73
|
@run_logger.finish!
|
|
70
74
|
end
|
|
71
75
|
|
|
@@ -92,31 +96,55 @@ class Engine
|
|
|
92
96
|
tree_graph
|
|
93
97
|
end
|
|
94
98
|
|
|
95
|
-
#
|
|
96
|
-
def
|
|
97
|
-
|
|
99
|
+
# Returns the final active graphs, either directly or via prune iteration.
|
|
100
|
+
def final_result_for(initial_active_document_ids:, initial_tree_seed_graph:, normal_seed:, persisted_links:)
|
|
101
|
+
return resolved_result_for(
|
|
102
|
+
active_document_ids: initial_active_document_ids,
|
|
103
|
+
tree_graph: initial_tree_seed_graph.deep_dup,
|
|
104
|
+
normal_seed: normal_seed,
|
|
105
|
+
persisted_links: persisted_links,
|
|
106
|
+
removed_documents: []
|
|
107
|
+
) unless @configuration.pruning_enabled?
|
|
108
|
+
|
|
109
|
+
prune_result_for(
|
|
110
|
+
initial_active_document_ids: initial_active_document_ids,
|
|
111
|
+
initial_tree_seed_graph: initial_tree_seed_graph,
|
|
112
|
+
normal_seed: normal_seed,
|
|
113
|
+
persisted_links: persisted_links
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Runs the configured prune rounds and returns the final active graphs.
|
|
118
|
+
def prune_result_for(initial_active_document_ids:, initial_tree_seed_graph:, normal_seed:, persisted_links:)
|
|
119
|
+
tree_phase = Pruning::TreePhase.new(
|
|
120
|
+
engine: self,
|
|
121
|
+
provenance: Pruning::TreeProvenance.new(tree_graph: initial_tree_seed_graph)
|
|
122
|
+
)
|
|
123
|
+
current_active_document_ids = initial_active_document_ids.dup
|
|
124
|
+
current_tree_seed_graph = initial_tree_seed_graph
|
|
98
125
|
removed_documents = {}
|
|
99
|
-
current_tree_graph = original_tree_graph
|
|
100
|
-
return {
|
|
101
|
-
active_document_ids: current_active_document_ids,
|
|
102
|
-
removed_documents: [],
|
|
103
|
-
tree_graph: current_tree_graph
|
|
104
|
-
} unless @configuration.pruning_enabled?
|
|
105
126
|
|
|
106
|
-
tree_phase = Pruning::TreePhase.new(engine: self, provenance: tree_provenance)
|
|
107
127
|
@configuration.prune_settings.prune_rounds.times do
|
|
108
|
-
tree_phase_result = tree_phase.process(
|
|
128
|
+
tree_phase_result = tree_phase.process(seed_graph: current_tree_seed_graph)
|
|
129
|
+
tree_seed_update = apply_removed_documents_to_seeds!(
|
|
130
|
+
tree_phase: tree_phase,
|
|
131
|
+
tree_seed_graph: current_tree_seed_graph,
|
|
132
|
+
normal_seed: normal_seed,
|
|
133
|
+
persisted_links: persisted_links,
|
|
134
|
+
current_active_document_ids: current_active_document_ids,
|
|
135
|
+
documents: tree_phase_result.fetch(:removed_documents)
|
|
136
|
+
)
|
|
137
|
+
current_tree_seed_graph = tree_seed_update.fetch(:tree_seed_graph)
|
|
109
138
|
register_removed_documents!(
|
|
110
139
|
removed_documents: removed_documents,
|
|
111
|
-
documents:
|
|
140
|
+
documents: tree_seed_update.fetch(:removed_documents)
|
|
112
141
|
)
|
|
113
|
-
current_active_document_ids = tree_phase_result.fetch(:active_document_ids)
|
|
114
|
-
current_tree_graph = tree_phase_result.fetch(:tree_graph)
|
|
115
142
|
|
|
116
|
-
session =
|
|
117
|
-
engine: self,
|
|
143
|
+
session = build_session(
|
|
118
144
|
active_document_ids: current_active_document_ids,
|
|
119
|
-
tree_graph:
|
|
145
|
+
tree_graph: tree_phase_result.fetch(:graph),
|
|
146
|
+
normal_seed: normal_seed,
|
|
147
|
+
persisted_links: persisted_links
|
|
120
148
|
)
|
|
121
149
|
session.resolve_all_relationships!
|
|
122
150
|
|
|
@@ -124,35 +152,122 @@ class Engine
|
|
|
124
152
|
graph: Pruning::NormalGraph.new(session: session),
|
|
125
153
|
rules: @configuration.normal_prune_rules
|
|
126
154
|
).prune!
|
|
127
|
-
if normal_removed_documents.empty?
|
|
155
|
+
if normal_removed_documents.empty? && !tree_seed_changes_require_iteration?(tree_seed_update)
|
|
128
156
|
return {
|
|
129
157
|
active_document_ids: current_active_document_ids,
|
|
130
158
|
removed_documents: removed_documents.values,
|
|
131
|
-
tree_graph:
|
|
159
|
+
tree_graph: tree_phase_result.fetch(:graph),
|
|
160
|
+
session: session
|
|
132
161
|
}
|
|
133
162
|
end
|
|
134
163
|
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
normal_seed_update = apply_removed_documents_to_seeds!(
|
|
165
|
+
tree_phase: tree_phase,
|
|
166
|
+
tree_seed_graph: current_tree_seed_graph,
|
|
167
|
+
normal_seed: normal_seed,
|
|
168
|
+
persisted_links: persisted_links,
|
|
169
|
+
current_active_document_ids: current_active_document_ids,
|
|
137
170
|
documents: normal_removed_documents
|
|
138
171
|
)
|
|
139
|
-
|
|
140
|
-
post_normal_tree_phase = tree_phase.process(active_document_ids: current_active_document_ids)
|
|
172
|
+
current_tree_seed_graph = normal_seed_update.fetch(:tree_seed_graph)
|
|
141
173
|
register_removed_documents!(
|
|
142
174
|
removed_documents: removed_documents,
|
|
143
|
-
documents:
|
|
175
|
+
documents: normal_seed_update.fetch(:removed_documents)
|
|
144
176
|
)
|
|
145
|
-
current_active_document_ids = post_normal_tree_phase.fetch(:active_document_ids)
|
|
146
|
-
current_tree_graph = post_normal_tree_phase.fetch(:tree_graph)
|
|
147
177
|
end
|
|
148
178
|
|
|
149
|
-
|
|
179
|
+
resolved_result_for(
|
|
150
180
|
active_document_ids: current_active_document_ids,
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
tree_graph: current_tree_seed_graph.deep_dup,
|
|
182
|
+
normal_seed: normal_seed,
|
|
183
|
+
persisted_links: persisted_links,
|
|
184
|
+
removed_documents: removed_documents.values
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Builds one final resolved session for the currently surviving seeds.
|
|
189
|
+
def resolved_result_for(active_document_ids:, tree_graph:, normal_seed:, persisted_links:, removed_documents:)
|
|
190
|
+
session = build_session(
|
|
191
|
+
active_document_ids: active_document_ids,
|
|
192
|
+
tree_graph: tree_graph,
|
|
193
|
+
normal_seed: normal_seed,
|
|
194
|
+
persisted_links: persisted_links
|
|
195
|
+
)
|
|
196
|
+
session.resolve_all_relationships!
|
|
197
|
+
{
|
|
198
|
+
active_document_ids: active_document_ids,
|
|
199
|
+
removed_documents: removed_documents,
|
|
200
|
+
tree_graph: tree_graph,
|
|
201
|
+
session: session
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Builds one normal-resolution session for one active graph snapshot.
|
|
206
|
+
def build_session(active_document_ids:, tree_graph:, normal_seed:, persisted_links:)
|
|
207
|
+
Session.new(
|
|
208
|
+
engine: self,
|
|
209
|
+
active_document_ids: active_document_ids,
|
|
210
|
+
tree_graph: tree_graph,
|
|
211
|
+
normal_seed: normal_seed,
|
|
212
|
+
persisted_links: persisted_links
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Applies removals to both seeds and returns the updated tree seed graph.
|
|
217
|
+
def apply_removed_documents_to_seeds!(tree_phase:, tree_seed_graph:, normal_seed:, persisted_links:, current_active_document_ids:, documents:)
|
|
218
|
+
requested_removed_documents = unique_documents(documents)
|
|
219
|
+
remove_from_normal_side!(
|
|
220
|
+
normal_seed: normal_seed,
|
|
221
|
+
persisted_links: persisted_links,
|
|
222
|
+
current_active_document_ids: current_active_document_ids,
|
|
223
|
+
documents: requested_removed_documents
|
|
224
|
+
)
|
|
225
|
+
tree_seed_result = tree_phase.apply_seed_removals(
|
|
226
|
+
seed_graph: tree_seed_graph,
|
|
227
|
+
documents: requested_removed_documents
|
|
228
|
+
)
|
|
229
|
+
extra_tree_removed_documents = unique_documents(
|
|
230
|
+
tree_seed_result.fetch(:removed_documents).reject do |document|
|
|
231
|
+
requested_removed_documents.any? { |requested_document| requested_document.object_id == document.object_id }
|
|
232
|
+
end
|
|
233
|
+
)
|
|
234
|
+
remove_from_normal_side!(
|
|
235
|
+
normal_seed: normal_seed,
|
|
236
|
+
persisted_links: persisted_links,
|
|
237
|
+
current_active_document_ids: current_active_document_ids,
|
|
238
|
+
documents: extra_tree_removed_documents
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
{
|
|
242
|
+
tree_seed_graph: tree_seed_result.fetch(:seed_graph),
|
|
243
|
+
removed_documents: unique_documents(requested_removed_documents + tree_seed_result.fetch(:removed_documents)),
|
|
244
|
+
seed_changed: tree_seed_result.fetch(:seed_changed)
|
|
153
245
|
}
|
|
154
246
|
end
|
|
155
247
|
|
|
248
|
+
# Removes documents from the normal seed, persisted overlay, and active set.
|
|
249
|
+
def remove_from_normal_side!(normal_seed:, persisted_links:, current_active_document_ids:, documents:)
|
|
250
|
+
documents = unique_documents(documents)
|
|
251
|
+
return if documents.empty?
|
|
252
|
+
|
|
253
|
+
normal_seed.remove_documents!(documents)
|
|
254
|
+
persisted_links.remove_documents!(documents)
|
|
255
|
+
documents.each do |document|
|
|
256
|
+
current_active_document_ids.delete(document.object_id)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Returns true when tree-seed changes should schedule another outer round.
|
|
261
|
+
#
|
|
262
|
+
# Tree resolvers do not yet exist, so tree pruning can settle and flow
|
|
263
|
+
# directly into the same round's normal phase. When tree resolvers are added
|
|
264
|
+
# later, changing the seed here should trigger another full outer rebuild.
|
|
265
|
+
def tree_seed_changes_require_iteration?(tree_seed_update)
|
|
266
|
+
return false unless tree_seed_update.fetch(:seed_changed)
|
|
267
|
+
|
|
268
|
+
false
|
|
269
|
+
end
|
|
270
|
+
|
|
156
271
|
# Groups removed documents by collection label for summary logging.
|
|
157
272
|
def removed_documents_by_collection(documents)
|
|
158
273
|
Array(documents).each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |document, grouped_documents|
|
|
@@ -167,6 +282,13 @@ class Engine
|
|
|
167
282
|
end
|
|
168
283
|
end
|
|
169
284
|
|
|
285
|
+
# Returns one document array with later duplicates removed by object id.
|
|
286
|
+
def unique_documents(documents)
|
|
287
|
+
Array(documents).each_with_object({}) do |document, unique_documents|
|
|
288
|
+
unique_documents[document.object_id] ||= document
|
|
289
|
+
end.values
|
|
290
|
+
end
|
|
291
|
+
|
|
170
292
|
# Logs the configured relationship summary for the final active graph.
|
|
171
293
|
def log_relationship_summary(session:, normal_graph:, tree_graph:)
|
|
172
294
|
tree_graph_view = Pruning::TreePhase::TreeGraphView.new(tree_graph: tree_graph)
|
|
@@ -6,12 +6,11 @@ module Plugins
|
|
|
6
6
|
module Relationships
|
|
7
7
|
module Pruning
|
|
8
8
|
|
|
9
|
-
# Applies
|
|
9
|
+
# Applies configured prune-rule predicates to a mutable graph view.
|
|
10
10
|
#
|
|
11
11
|
# The graph view may represent either resolved normal relationships or the
|
|
12
|
-
# current tree adjacency. Pruning is recursive within the supplied graph
|
|
13
|
-
# removing one document can
|
|
14
|
-
# keeps deleting documents until every rule is satisfied.
|
|
12
|
+
# current tree adjacency. Pruning is recursive within the supplied graph because
|
|
13
|
+
# removing one document can make relationship predicates select others.
|
|
15
14
|
class RulePruner
|
|
16
15
|
# Builds one pruner over one mutable graph view and one ordered rule list.
|
|
17
16
|
def initialize(graph:, rules:, subject_documents_resolver: nil)
|
|
@@ -20,7 +19,7 @@ class RulePruner
|
|
|
20
19
|
@subject_documents_resolver = subject_documents_resolver
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
# Removes documents until
|
|
22
|
+
# Removes documents until no configured rule selects another candidate.
|
|
24
23
|
def prune!
|
|
25
24
|
removed_documents = []
|
|
26
25
|
|
|
@@ -40,12 +39,13 @@ class RulePruner
|
|
|
40
39
|
|
|
41
40
|
private
|
|
42
41
|
|
|
43
|
-
# Finds every document
|
|
42
|
+
# Finds every document selected by at least one prune rule.
|
|
44
43
|
def prune_candidates
|
|
45
44
|
@rules.each_with_object({}) do |rule, candidates|
|
|
46
45
|
subject_documents_for(rule).each do |document|
|
|
47
46
|
next if candidates.key?(document.object_id)
|
|
48
|
-
next unless rule.
|
|
47
|
+
next unless rule.frontmatter_selected?(document)
|
|
48
|
+
next unless rule.relationship_selected?(graph: @graph, document: document)
|
|
49
49
|
|
|
50
50
|
candidates[document.object_id] = document
|
|
51
51
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'jekyll-relationships/trees/root_distances'
|
|
4
|
+
|
|
3
5
|
module Jekyll
|
|
4
6
|
module Plugins
|
|
5
7
|
|
|
@@ -21,6 +23,7 @@ class TreePhase
|
|
|
21
23
|
@documents_by_collection = Hash.new { |hash, key| hash[key] = {} }
|
|
22
24
|
@parents = Hash.new { |hash, key| hash[key] = {} }
|
|
23
25
|
@children = Hash.new { |hash, key| hash[key] = {} }
|
|
26
|
+
@root_distance_cache = nil
|
|
24
27
|
register_documents!
|
|
25
28
|
register_edges!
|
|
26
29
|
end
|
|
@@ -30,6 +33,32 @@ class TreePhase
|
|
|
30
33
|
@documents_by_collection[collection].values
|
|
31
34
|
end
|
|
32
35
|
|
|
36
|
+
# Returns every currently active tree document.
|
|
37
|
+
def tree_documents
|
|
38
|
+
@documents_by_collection.values.flat_map(&:values)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the direct parent documents for one active document.
|
|
42
|
+
def parent_documents_for(document)
|
|
43
|
+
return [] unless active_document?(document)
|
|
44
|
+
|
|
45
|
+
@parents[document].keys
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the direct child documents for one active document.
|
|
49
|
+
def child_documents_for(document)
|
|
50
|
+
return [] unless active_document?(document)
|
|
51
|
+
|
|
52
|
+
@children[document].keys
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns the shortest current distance from one document to any root.
|
|
56
|
+
def root_distance_for(document)
|
|
57
|
+
return nil unless active_document?(document)
|
|
58
|
+
|
|
59
|
+
root_distances_for_graph[document.object_id]
|
|
60
|
+
end
|
|
61
|
+
|
|
33
62
|
# Returns the relevant neighbour documents for one configured tree member.
|
|
34
63
|
def neighbour_documents_for_member(member:, document:, inverse:)
|
|
35
64
|
return inverse_neighbours_for(member: member, document: document) if inverse
|
|
@@ -49,6 +78,7 @@ class TreePhase
|
|
|
49
78
|
end
|
|
50
79
|
@children.delete(document)
|
|
51
80
|
@parents.delete(document)
|
|
81
|
+
clear_root_distance_cache
|
|
52
82
|
true
|
|
53
83
|
end
|
|
54
84
|
|
|
@@ -116,6 +146,21 @@ class TreePhase
|
|
|
116
146
|
combined[document.object_id] ||= document
|
|
117
147
|
end.values
|
|
118
148
|
end
|
|
149
|
+
|
|
150
|
+
# Returns true when one document is still active in the mutable view.
|
|
151
|
+
def active_document?(document)
|
|
152
|
+
@documents_by_collection[document.collection.label].key?(document.object_id)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Calculates the current root distances and caches them until the view changes.
|
|
156
|
+
def root_distances_for_graph
|
|
157
|
+
@root_distance_cache ||= Trees::RootDistances.for(graph: self)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Clears the cached root distances after a structural tree change.
|
|
161
|
+
def clear_root_distance_cache
|
|
162
|
+
@root_distance_cache = nil
|
|
163
|
+
end
|
|
119
164
|
end
|
|
120
165
|
|
|
121
166
|
# Builds one tree phase helper for one engine and one immutable provenance set.
|
|
@@ -125,134 +170,88 @@ class TreePhase
|
|
|
125
170
|
@provenance = provenance
|
|
126
171
|
end
|
|
127
172
|
|
|
128
|
-
#
|
|
129
|
-
def process(
|
|
130
|
-
|
|
173
|
+
# Resolves tree pruning against one concrete tree seed graph.
|
|
174
|
+
def process(seed_graph:)
|
|
175
|
+
current_tree_graph = seed_graph.deep_dup
|
|
131
176
|
removed_documents = []
|
|
132
|
-
|
|
133
|
-
orphan_result = stabilise_orphans!(
|
|
134
|
-
current_active_document_ids: current_active_document_ids,
|
|
135
|
-
removed_documents: removed_documents
|
|
136
|
-
)
|
|
137
|
-
current_tree_graph = orphan_result.fetch(:graph)
|
|
177
|
+
stabilise_orphans!(graph: current_tree_graph, removed_documents: removed_documents)
|
|
138
178
|
return {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
removed_documents: removed_documents
|
|
179
|
+
graph: current_tree_graph,
|
|
180
|
+
removed_documents: unique_documents(removed_documents)
|
|
142
181
|
} if @configuration.tree_prune_rules.empty?
|
|
143
182
|
|
|
144
|
-
eligible_document_ids_by_rule = eligible_document_ids_by_rule(graph: current_tree_graph)
|
|
145
|
-
|
|
146
183
|
loop do
|
|
147
|
-
pruned_documents = prune_tree_rules(
|
|
148
|
-
graph: current_tree_graph,
|
|
149
|
-
eligible_document_ids_by_rule: eligible_document_ids_by_rule
|
|
150
|
-
)
|
|
184
|
+
pruned_documents = prune_tree_rules(graph: current_tree_graph)
|
|
151
185
|
if pruned_documents.empty?
|
|
152
186
|
return {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
removed_documents: removed_documents
|
|
187
|
+
graph: current_tree_graph,
|
|
188
|
+
removed_documents: unique_documents(removed_documents)
|
|
156
189
|
}
|
|
157
190
|
end
|
|
158
191
|
|
|
159
|
-
remove_documents!(
|
|
160
|
-
current_active_document_ids: current_active_document_ids,
|
|
161
|
-
documents: pruned_documents
|
|
162
|
-
)
|
|
192
|
+
current_tree_graph.remove_documents!(pruned_documents)
|
|
163
193
|
removed_documents.concat(pruned_documents)
|
|
164
|
-
current_tree_graph
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
194
|
+
stabilise_orphans!(graph: current_tree_graph, removed_documents: removed_documents)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Applies committed removals to one tree seed graph and repairs the tree.
|
|
199
|
+
#
|
|
200
|
+
# The tree seed only changes because documents have permanently died. Resolver
|
|
201
|
+
# output never flows back into the seed, but structural repair such as orphan
|
|
202
|
+
# reattachment must be reflected so the next round starts from the right tree.
|
|
203
|
+
def apply_seed_removals(seed_graph:, documents:)
|
|
204
|
+
documents_to_remove = Array(documents).select do |document|
|
|
205
|
+
seed_graph.participating?(document) && seed_graph.active_document?(document)
|
|
206
|
+
end
|
|
207
|
+
return {
|
|
208
|
+
seed_graph: seed_graph,
|
|
209
|
+
removed_documents: [],
|
|
210
|
+
seed_changed: false
|
|
211
|
+
} if documents_to_remove.empty?
|
|
212
|
+
|
|
213
|
+
next_seed_graph = seed_graph.deep_dup
|
|
214
|
+
removed_documents = documents_to_remove.select do |document|
|
|
215
|
+
next_seed_graph.remove_document!(document)
|
|
168
216
|
end
|
|
217
|
+
stabilise_orphans!(graph: next_seed_graph, removed_documents: removed_documents)
|
|
218
|
+
|
|
219
|
+
{
|
|
220
|
+
seed_graph: next_seed_graph,
|
|
221
|
+
removed_documents: unique_documents(removed_documents),
|
|
222
|
+
seed_changed: removed_documents.any?
|
|
223
|
+
}
|
|
169
224
|
end
|
|
170
225
|
|
|
171
226
|
private
|
|
172
227
|
|
|
173
|
-
#
|
|
174
|
-
def stabilise_orphans!(
|
|
228
|
+
# Removes or reattaches orphans until the graph stabilises.
|
|
229
|
+
def stabilise_orphans!(graph:, removed_documents:)
|
|
175
230
|
loop do
|
|
176
|
-
|
|
177
|
-
base_graph = build_tree_graph(active_document_ids: current_active_document_ids)
|
|
178
|
-
orphan_result = apply_orphan_policy(graph: base_graph)
|
|
231
|
+
orphan_result = apply_orphan_policy(graph: graph)
|
|
179
232
|
orphaned_documents = orphan_result.fetch(:removed_documents)
|
|
180
|
-
return
|
|
181
|
-
graph: orphan_result.fetch(:graph)
|
|
182
|
-
} if orphaned_documents.empty?
|
|
233
|
+
return graph if orphaned_documents.empty?
|
|
183
234
|
|
|
184
|
-
remove_documents!(
|
|
185
|
-
current_active_document_ids: current_active_document_ids,
|
|
186
|
-
documents: orphaned_documents
|
|
187
|
-
)
|
|
235
|
+
graph.remove_documents!(orphaned_documents)
|
|
188
236
|
removed_documents.concat(orphaned_documents)
|
|
189
237
|
end
|
|
190
238
|
end
|
|
191
239
|
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
site: @engine.site,
|
|
196
|
-
configuration: @configuration,
|
|
197
|
-
registry: @engine.registry,
|
|
198
|
-
data_path: @engine.data_path,
|
|
199
|
-
debug_logger: @engine.debug_logger,
|
|
200
|
-
active_document_ids: active_document_ids
|
|
201
|
-
)
|
|
202
|
-
tree_graph.build!
|
|
203
|
-
tree_graph
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Builds one stable lookup of which documents each tree rule may prune.
|
|
207
|
-
def eligible_document_ids_by_rule(graph:)
|
|
208
|
-
root_distances = root_distances_for(graph)
|
|
209
|
-
@configuration.tree_prune_rules.each_with_object({}) do |rule, lookup|
|
|
210
|
-
lookup[rule.object_id] = graph.documents_for(rule.subject_collection).each_with_object({}) do |document, eligible_documents|
|
|
211
|
-
next unless depth_selected?(depth: rule.depth, root_distance: root_distances[document.object_id])
|
|
212
|
-
|
|
213
|
-
eligible_documents[document.object_id] = true
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# Runs the configured tree prune rules against the current graph while only
|
|
219
|
-
# allowing the documents selected by the initial depth snapshot to be pruned.
|
|
220
|
-
def prune_tree_rules(graph:, eligible_document_ids_by_rule:)
|
|
240
|
+
# Runs the configured tree prune rules against the current graph, re-reading
|
|
241
|
+
# depth from the mutable graph view each time candidates are evaluated.
|
|
242
|
+
def prune_tree_rules(graph:)
|
|
221
243
|
RulePruner.new(
|
|
222
244
|
graph: TreeGraphView.new(tree_graph: graph),
|
|
223
245
|
rules: @configuration.tree_prune_rules,
|
|
224
246
|
subject_documents_resolver: lambda do |rule, mutable_graph|
|
|
225
|
-
eligible_document_ids = eligible_document_ids_by_rule.fetch(rule.object_id)
|
|
226
247
|
mutable_graph.documents_for(rule.subject_collection).select do |document|
|
|
227
|
-
|
|
248
|
+
depth = rule.depth
|
|
249
|
+
depth.nil? || depth_selected?(depth: depth, root_distance: mutable_graph.root_distance_for(document))
|
|
228
250
|
end
|
|
229
251
|
end
|
|
230
252
|
).prune!
|
|
231
253
|
end
|
|
232
254
|
|
|
233
|
-
# Calculates the shortest distance from every node to any root in the current tree.
|
|
234
|
-
def root_distances_for(graph)
|
|
235
|
-
root_distances = {}
|
|
236
|
-
queue = graph.tree_documents.select do |document|
|
|
237
|
-
graph.parent_documents_for(document).empty?
|
|
238
|
-
end.sort_by(&:relative_path).map do |root_document|
|
|
239
|
-
[root_document, 0]
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
until queue.empty?
|
|
243
|
-
document, distance = queue.shift
|
|
244
|
-
existing_distance = root_distances[document.object_id]
|
|
245
|
-
next if !existing_distance.nil? && existing_distance <= distance
|
|
246
|
-
|
|
247
|
-
root_distances[document.object_id] = distance
|
|
248
|
-
graph.child_documents_for(document).each do |child_document|
|
|
249
|
-
queue << [child_document, distance + 1]
|
|
250
|
-
end
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
root_distances
|
|
254
|
-
end
|
|
255
|
-
|
|
256
255
|
# Returns true when one root distance is eligible under one configured depth.
|
|
257
256
|
def depth_selected?(depth:, root_distance:)
|
|
258
257
|
return false if root_distance.nil?
|
|
@@ -264,21 +263,6 @@ class TreePhase
|
|
|
264
263
|
end
|
|
265
264
|
end
|
|
266
265
|
|
|
267
|
-
# Removes many documents from the current active set in place.
|
|
268
|
-
def remove_documents!(current_active_document_ids:, documents:)
|
|
269
|
-
Array(documents).each do |document|
|
|
270
|
-
current_active_document_ids.delete(document.object_id)
|
|
271
|
-
@current_active_document_ids.delete(document.object_id)
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
# Builds one fast active-document lookup for the current build state.
|
|
276
|
-
def active_document_lookup(active_document_ids)
|
|
277
|
-
active_document_ids.each_with_object({}) do |document_id, active_ids|
|
|
278
|
-
active_ids[document_id] = true
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
|
|
282
266
|
# Applies the configured orphan policy to one current tree graph.
|
|
283
267
|
def apply_orphan_policy(graph:)
|
|
284
268
|
removed_documents = []
|
|
@@ -307,30 +291,31 @@ class TreePhase
|
|
|
307
291
|
end.sort_by(&:relative_path)
|
|
308
292
|
end
|
|
309
293
|
|
|
310
|
-
# Attempts to reconnect one orphan to its surviving original
|
|
294
|
+
# Attempts to reconnect one orphan to its nearest surviving original ancestors.
|
|
311
295
|
def reattach_orphan!(graph:, document:)
|
|
312
|
-
|
|
296
|
+
nearest_surviving_ancestor_candidates_for(graph: graph, document: document).each do |candidate|
|
|
313
297
|
graph.add_edge(
|
|
314
298
|
parent_document: candidate.fetch(:document),
|
|
315
299
|
child_document: document,
|
|
316
300
|
tree_settings: candidate.fetch(:tree_settings),
|
|
317
301
|
definition: candidate.fetch(:definition),
|
|
318
|
-
source_description: 'pruning
|
|
302
|
+
source_description: 'pruning ancestors'
|
|
319
303
|
)
|
|
320
304
|
end
|
|
321
305
|
|
|
322
306
|
graph.parent_documents_for(document).any?
|
|
323
307
|
end
|
|
324
308
|
|
|
325
|
-
# Returns
|
|
326
|
-
def
|
|
309
|
+
# Returns the first surviving ancestor on every original parent lineage.
|
|
310
|
+
def nearest_surviving_ancestor_candidates_for(graph:, document:)
|
|
327
311
|
@provenance.original_parent_entries_for(document).each_with_object({}) do |parent_entry, candidates|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
312
|
+
nearest_surviving_ancestors_for(
|
|
313
|
+
graph: graph,
|
|
314
|
+
document: parent_entry.fetch(:document),
|
|
315
|
+
visited_document_ids: {}
|
|
316
|
+
).each do |ancestor_document|
|
|
317
|
+
candidates[ancestor_document.object_id] ||= {
|
|
318
|
+
document: ancestor_document,
|
|
334
319
|
definition: parent_entry.fetch(:definition),
|
|
335
320
|
tree_settings: parent_entry.fetch(:tree_settings)
|
|
336
321
|
}
|
|
@@ -338,9 +323,28 @@ class TreePhase
|
|
|
338
323
|
end.values
|
|
339
324
|
end
|
|
340
325
|
|
|
341
|
-
#
|
|
342
|
-
def
|
|
343
|
-
|
|
326
|
+
# Walks removed provenance nodes until each lineage reaches an active document.
|
|
327
|
+
def nearest_surviving_ancestors_for(graph:, document:, visited_document_ids:)
|
|
328
|
+
return [] if visited_document_ids.key?(document.object_id)
|
|
329
|
+
return [document] if graph.active_document?(document)
|
|
330
|
+
|
|
331
|
+
lineage_visited_document_ids = visited_document_ids.merge(document.object_id => true)
|
|
332
|
+
@provenance.original_parent_entries_for(document).each_with_object({}) do |parent_entry, ancestors|
|
|
333
|
+
nearest_surviving_ancestors_for(
|
|
334
|
+
graph: graph,
|
|
335
|
+
document: parent_entry.fetch(:document),
|
|
336
|
+
visited_document_ids: lineage_visited_document_ids
|
|
337
|
+
).each do |ancestor_document|
|
|
338
|
+
ancestors[ancestor_document.object_id] ||= ancestor_document
|
|
339
|
+
end
|
|
340
|
+
end.values
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Returns one document array with later duplicates removed by object id.
|
|
344
|
+
def unique_documents(documents)
|
|
345
|
+
Array(documents).each_with_object({}) do |document, unique_documents|
|
|
346
|
+
unique_documents[document.object_id] ||= document
|
|
347
|
+
end.values
|
|
344
348
|
end
|
|
345
349
|
end
|
|
346
350
|
|