jekyll-relationships 0.1.1.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/defaults.rb +1 -0
- 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.rb +43 -8
- data/lib/jekyll-relationships/definitions/normal_relationship.rb +8 -2
- data/lib/jekyll-relationships/definitions/prune_rule.rb +19 -3
- data/lib/jekyll-relationships/definitions/tree_relationship.rb +3 -2
- data/lib/jekyll-relationships/documents/registry.rb +182 -33
- data/lib/jekyll-relationships/engine/normal_seed.rb +9 -4
- data/lib/jekyll-relationships/engine/raw_path_state.rb +44 -22
- data/lib/jekyll-relationships/engine/relationship_state.rb +12 -0
- data/lib/jekyll-relationships/engine/session.rb +89 -15
- data/lib/jekyll-relationships/engine/write_back.rb +1 -0
- data/lib/jekyll-relationships/engine.rb +1 -1
- data/lib/jekyll-relationships/pruning/rule_pruner.rb +7 -7
- data/lib/jekyll-relationships/pruning/tree_phase.rb +29 -11
- data/lib/jekyll-relationships/pruning/tree_provenance.rb +1 -1
- data/lib/jekyll-relationships/references/accumulator.rb +18 -6
- data/lib/jekyll-relationships/references/template.rb +46 -11
- data/lib/jekyll-relationships/resolvers/base.rb +3 -0
- 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 +30 -13
- data/lib/jekyll-relationships/version.rb +1 -1
- data/lib/jekyll-relationships.rb +1 -0
- data/readme.md +154 -23
- metadata +3 -2
|
@@ -79,7 +79,7 @@ class Engine
|
|
|
79
79
|
# Reads and stores one concrete direct relationship definition from frontmatter.
|
|
80
80
|
def seed_definition_from_frontmatter(accumulators:, document:, definition:)
|
|
81
81
|
definition.foreign_paths.each do |path|
|
|
82
|
-
raw_state = raw_path_state(document, path)
|
|
82
|
+
raw_state = raw_path_state(document, path, definition)
|
|
83
83
|
debug_relationship_event(
|
|
84
84
|
document: document,
|
|
85
85
|
definition: definition,
|
|
@@ -94,7 +94,9 @@ class Engine
|
|
|
94
94
|
|
|
95
95
|
resolved_entries = raw_state.resolved_entries_for(
|
|
96
96
|
primary_path: definition.primary_path,
|
|
97
|
+
scope_fields: definition.scope_fields,
|
|
97
98
|
registry: @registry,
|
|
99
|
+
relationship: "#{definition.from_collection} -> #{definition.to_collection}",
|
|
98
100
|
active_document_checker: proc { |resolved_document| active_document?(resolved_document) }
|
|
99
101
|
)
|
|
100
102
|
debug_relationship_event(
|
|
@@ -144,6 +146,7 @@ class Engine
|
|
|
144
146
|
).add(
|
|
145
147
|
document: entry.fetch(:document),
|
|
146
148
|
key: entry.fetch(:key),
|
|
149
|
+
scope: entry.fetch(:scope),
|
|
147
150
|
metadata: entry.fetch(:metadata),
|
|
148
151
|
count: entry.fetch(:count)
|
|
149
152
|
)
|
|
@@ -157,6 +160,7 @@ class Engine
|
|
|
157
160
|
).add(
|
|
158
161
|
document: document,
|
|
159
162
|
key: @registry.key_for(document, primary_path: definition.primary_path),
|
|
163
|
+
scope: @registry.scope_for(document, scope_fields: definition.scope_fields, relationship: "#{definition.from_collection} -> #{definition.to_collection}"),
|
|
160
164
|
metadata: entry.fetch(:metadata),
|
|
161
165
|
count: entry.fetch(:count)
|
|
162
166
|
)
|
|
@@ -183,13 +187,14 @@ class Engine
|
|
|
183
187
|
end
|
|
184
188
|
|
|
185
189
|
# Returns the cached raw-path parser for one document/path pair.
|
|
186
|
-
def raw_path_state(document, path)
|
|
187
|
-
@raw_path_states[[document.object_id, path]] ||= RawPathState.new(
|
|
190
|
+
def raw_path_state(document, path, definition)
|
|
191
|
+
@raw_path_states[[document.object_id, path, definition.object_id]] ||= RawPathState.new(
|
|
188
192
|
document: document,
|
|
189
193
|
path: path,
|
|
190
194
|
data_path: @data_path,
|
|
191
195
|
string_array: @string_array,
|
|
192
|
-
reference_template: @configuration.reference_template
|
|
196
|
+
reference_template: @configuration.reference_template,
|
|
197
|
+
reference_context: "Relationship `#{definition.from_collection} -> #{definition.to_collection}` reference on document `#{document.relative_path}`"
|
|
193
198
|
)
|
|
194
199
|
end
|
|
195
200
|
|
|
@@ -18,16 +18,17 @@ class Engine
|
|
|
18
18
|
# reuse them without reparsing frontmatter.
|
|
19
19
|
class LocationState
|
|
20
20
|
# Builds one concrete-location cache.
|
|
21
|
-
def initialize(match:, string_array:, reference_template:)
|
|
21
|
+
def initialize(match:, string_array:, reference_template:, reference_context: nil)
|
|
22
22
|
@string_array = string_array
|
|
23
23
|
@reference_template = reference_template
|
|
24
|
+
@reference_context = reference_context
|
|
24
25
|
@entries = parse_entries(match.value)
|
|
25
26
|
@resolution_cache = {}
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
# Resolves every parsed entry for one
|
|
29
|
-
def resolved_entries_for(primary_path:, &resolver)
|
|
30
|
-
signature =
|
|
29
|
+
# Resolves every parsed entry for one complete identity scheme.
|
|
30
|
+
def resolved_entries_for(primary_path:, scope_fields:, &resolver)
|
|
31
|
+
signature = identity_signature(primary_path: primary_path, scope_fields: scope_fields)
|
|
31
32
|
@resolution_cache[signature] ||= @entries.map do |entry|
|
|
32
33
|
resolver.call(entry)
|
|
33
34
|
end
|
|
@@ -38,20 +39,20 @@ class Engine
|
|
|
38
39
|
# Parses one concrete raw value into individual reference entries.
|
|
39
40
|
def parse_entries(raw_value)
|
|
40
41
|
@string_array.interpret(raw_value, split: -1, flatten: true).map do |entry|
|
|
41
|
-
@reference_template.parse(entry)
|
|
42
|
+
@reference_template.parse(entry, context: @reference_context)
|
|
42
43
|
end.compact
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
# Normalises the cache key for one primary-key scheme.
|
|
46
|
-
def
|
|
47
|
-
primary_path.nil? ? '__relative_path__' : primary_path
|
|
46
|
+
# Normalises the cache key for one primary-key and scope scheme.
|
|
47
|
+
def identity_signature(primary_path:, scope_fields:)
|
|
48
|
+
[primary_path.nil? ? '__relative_path__' : primary_path, scope_fields.map(&:signature)]
|
|
48
49
|
end
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
attr_reader :document, :path, :raw_value
|
|
52
53
|
|
|
53
54
|
# Builds one raw-path cache for one document and path.
|
|
54
|
-
def initialize(document:, path:, data_path:, string_array:, reference_template:)
|
|
55
|
+
def initialize(document:, path:, data_path:, string_array:, reference_template:, reference_context: nil)
|
|
55
56
|
@document = document
|
|
56
57
|
@path = path
|
|
57
58
|
@data_path = data_path
|
|
@@ -64,7 +65,8 @@ class Engine
|
|
|
64
65
|
LocationState.new(
|
|
65
66
|
match: match,
|
|
66
67
|
string_array: @string_array,
|
|
67
|
-
reference_template: @reference_template
|
|
68
|
+
reference_template: @reference_template,
|
|
69
|
+
reference_context: reference_context
|
|
68
70
|
)
|
|
69
71
|
end
|
|
70
72
|
end
|
|
@@ -79,14 +81,16 @@ class Engine
|
|
|
79
81
|
@read_result.array_traversed?
|
|
80
82
|
end
|
|
81
83
|
|
|
82
|
-
# Resolves every parsed entry for one
|
|
83
|
-
def resolved_entries_for(primary_path:, registry:, active_document_checker: nil)
|
|
84
|
+
# Resolves every parsed entry for one complete identity scheme.
|
|
85
|
+
def resolved_entries_for(primary_path:, scope_fields: [], registry:, relationship: nil, active_document_checker: nil)
|
|
84
86
|
@location_states.flat_map do |location_state|
|
|
85
|
-
location_state.resolved_entries_for(primary_path: primary_path) do |entry|
|
|
87
|
+
location_state.resolved_entries_for(primary_path: primary_path, scope_fields: scope_fields) do |entry|
|
|
86
88
|
resolve_entry(
|
|
87
89
|
entry: entry,
|
|
88
90
|
primary_path: primary_path,
|
|
91
|
+
scope_fields: scope_fields,
|
|
89
92
|
registry: registry,
|
|
93
|
+
relationship: relationship,
|
|
90
94
|
active_document_checker: active_document_checker
|
|
91
95
|
)
|
|
92
96
|
end
|
|
@@ -96,21 +100,39 @@ class Engine
|
|
|
96
100
|
private
|
|
97
101
|
|
|
98
102
|
# Resolves one parsed entry to a document and canonical key.
|
|
99
|
-
def resolve_entry(entry:, primary_path:, registry:, active_document_checker:)
|
|
103
|
+
def resolve_entry(entry:, primary_path:, scope_fields:, registry:, relationship:, active_document_checker:)
|
|
104
|
+
effective_scope = registry.effective_scope_for(
|
|
105
|
+
reference_scope: entry.scope,
|
|
106
|
+
referring_document: @document,
|
|
107
|
+
scope_fields: scope_fields,
|
|
108
|
+
relationship: relationship
|
|
109
|
+
)
|
|
100
110
|
document = if entry.document?
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
registry.validate_scope_match!(
|
|
112
|
+
document: entry.page,
|
|
113
|
+
scope: effective_scope,
|
|
114
|
+
scope_fields: scope_fields,
|
|
115
|
+
relationship: relationship,
|
|
116
|
+
referring_document: @document
|
|
117
|
+
)
|
|
118
|
+
entry.page
|
|
119
|
+
else
|
|
120
|
+
registry.lookup(
|
|
121
|
+
key: entry.key,
|
|
122
|
+
primary_path: primary_path,
|
|
123
|
+
collection: entry.collection.nil? ? nil : entry.collection.to_s,
|
|
124
|
+
scope_fields: scope_fields,
|
|
125
|
+
scope: effective_scope,
|
|
126
|
+
relationship: relationship,
|
|
127
|
+
referring_document: @document
|
|
128
|
+
)
|
|
129
|
+
end
|
|
109
130
|
return nil if active_document_checker && !active_document_checker.call(document)
|
|
110
131
|
|
|
111
132
|
{
|
|
112
133
|
document: document,
|
|
113
134
|
key: registry.key_for(document, primary_path: primary_path),
|
|
135
|
+
scope: effective_scope,
|
|
114
136
|
metadata: entry.metadata,
|
|
115
137
|
count: entry.count
|
|
116
138
|
}
|
|
@@ -75,6 +75,9 @@ class Engine
|
|
|
75
75
|
target_document = @engine.resolve_reference_document(
|
|
76
76
|
reference,
|
|
77
77
|
primary_path: @definition.primary_path,
|
|
78
|
+
scope_fields: @definition.scope_fields,
|
|
79
|
+
referring_document: @document,
|
|
80
|
+
relationship: relationship_description,
|
|
78
81
|
collection_hint: @definition.to_collection
|
|
79
82
|
)
|
|
80
83
|
unless @engine.active_document?(target_document)
|
|
@@ -99,6 +102,7 @@ class Engine
|
|
|
99
102
|
result = @links.add_detailed_result(
|
|
100
103
|
document: target_document,
|
|
101
104
|
key: @engine.registry.key_for(target_document, primary_path: @definition.primary_path),
|
|
105
|
+
scope: @engine.registry.scope_for(target_document, scope_fields: @definition.scope_fields, relationship: relationship_description),
|
|
102
106
|
metadata: sanitised_metadata(metadata),
|
|
103
107
|
count: count
|
|
104
108
|
)
|
|
@@ -145,6 +149,9 @@ class Engine
|
|
|
145
149
|
target_document = @engine.resolve_reference_document(
|
|
146
150
|
reference,
|
|
147
151
|
primary_path: @definition.primary_path,
|
|
152
|
+
scope_fields: @definition.scope_fields,
|
|
153
|
+
referring_document: @document,
|
|
154
|
+
relationship: relationship_description,
|
|
148
155
|
collection_hint: @definition.to_collection
|
|
149
156
|
)
|
|
150
157
|
@engine.clear_persisted_link(source_state: self, target_document: target_document) if clear_persisted
|
|
@@ -231,6 +238,11 @@ class Engine
|
|
|
231
238
|
"`#{@document.relative_path}` (#{@definition.from_collection} -> #{@definition.to_collection})"
|
|
232
239
|
end
|
|
233
240
|
|
|
241
|
+
# Returns the concrete relationship label used in validation diagnostics.
|
|
242
|
+
def relationship_description
|
|
243
|
+
"#{@definition.from_collection} -> #{@definition.to_collection}"
|
|
244
|
+
end
|
|
245
|
+
|
|
234
246
|
# Emits one debug event for this state when the definition enables it.
|
|
235
247
|
def debug(area, event, details)
|
|
236
248
|
@engine.debug_logger.relationship_event(
|
|
@@ -36,6 +36,7 @@ class Engine
|
|
|
36
36
|
@relationship_states = {}
|
|
37
37
|
@document_resolution_states = {}
|
|
38
38
|
@initial_links_seeded = false
|
|
39
|
+
@defer_bidirectional_mirror_resolvers = false
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
# Returns true when the document is active in this session.
|
|
@@ -71,6 +72,8 @@ class Engine
|
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
ensure_initial_links_seeded!
|
|
75
|
+
return state.current_references if defer_bidirectional_mirror_resolver_for?(state)
|
|
76
|
+
|
|
74
77
|
resolve_state(state)
|
|
75
78
|
state.current_references
|
|
76
79
|
end
|
|
@@ -94,16 +97,41 @@ class Engine
|
|
|
94
97
|
end
|
|
95
98
|
|
|
96
99
|
# Resolves one helper or resolver reference to a document, even if inactive.
|
|
97
|
-
def resolve_reference_document(reference, primary_path:, collection_hint: nil)
|
|
98
|
-
|
|
100
|
+
def resolve_reference_document(reference, primary_path:, scope_fields: [], referring_document: nil, relationship: nil, collection_hint: nil)
|
|
101
|
+
if reference.is_a?(Jekyll::Document)
|
|
102
|
+
@registry.scope_for(reference, scope_fields: scope_fields, relationship: relationship)
|
|
103
|
+
return reference
|
|
104
|
+
end
|
|
99
105
|
|
|
100
|
-
parsed_reference = @configuration.reference_template.parse(
|
|
101
|
-
|
|
106
|
+
parsed_reference = @configuration.reference_template.parse(
|
|
107
|
+
reference,
|
|
108
|
+
context: reference_context(relationship: relationship, document: referring_document)
|
|
109
|
+
)
|
|
110
|
+
effective_scope = @registry.effective_scope_for(
|
|
111
|
+
reference_scope: parsed_reference.scope,
|
|
112
|
+
referring_document: referring_document,
|
|
113
|
+
scope_fields: scope_fields,
|
|
114
|
+
relationship: relationship
|
|
115
|
+
)
|
|
116
|
+
if parsed_reference.document?
|
|
117
|
+
@registry.validate_scope_match!(
|
|
118
|
+
document: parsed_reference.page,
|
|
119
|
+
scope: effective_scope,
|
|
120
|
+
scope_fields: scope_fields,
|
|
121
|
+
relationship: relationship,
|
|
122
|
+
referring_document: referring_document
|
|
123
|
+
)
|
|
124
|
+
return parsed_reference.page
|
|
125
|
+
end
|
|
102
126
|
|
|
103
127
|
@registry.lookup(
|
|
104
128
|
key: parsed_reference.key,
|
|
105
129
|
primary_path: primary_path,
|
|
106
|
-
collection: collection_hint || (parsed_reference.collection.nil? ? nil : parsed_reference.collection.to_s)
|
|
130
|
+
collection: collection_hint || (parsed_reference.collection.nil? ? nil : parsed_reference.collection.to_s),
|
|
131
|
+
scope_fields: scope_fields,
|
|
132
|
+
scope: effective_scope,
|
|
133
|
+
relationship: relationship,
|
|
134
|
+
referring_document: referring_document
|
|
107
135
|
)
|
|
108
136
|
end
|
|
109
137
|
|
|
@@ -198,16 +226,12 @@ class Engine
|
|
|
198
226
|
|
|
199
227
|
# Resolves every configured normal relationship state once.
|
|
200
228
|
def resolve_all_relationships!
|
|
201
|
-
@
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
resolve_relationships(document, definition.to_collection)
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
229
|
+
@defer_bidirectional_mirror_resolvers = true
|
|
230
|
+
resolve_all_relationships_pass!
|
|
231
|
+
@defer_bidirectional_mirror_resolvers = false
|
|
232
|
+
resolve_deferred_bidirectional_mirror_resolvers!
|
|
233
|
+
ensure
|
|
234
|
+
@defer_bidirectional_mirror_resolvers = false
|
|
211
235
|
end
|
|
212
236
|
|
|
213
237
|
# Writes the current session's resolved relationships back to frontmatter.
|
|
@@ -217,6 +241,14 @@ class Engine
|
|
|
217
241
|
|
|
218
242
|
private
|
|
219
243
|
|
|
244
|
+
# Builds consistent relationship and source-document details for reference parsing errors.
|
|
245
|
+
def reference_context(relationship:, document:)
|
|
246
|
+
parts = []
|
|
247
|
+
parts << "Relationship `#{relationship}`" unless relationship.nil? || relationship.to_s.empty?
|
|
248
|
+
parts << "reference on document `#{document.relative_path}`" if document
|
|
249
|
+
parts.join(' ')
|
|
250
|
+
end
|
|
251
|
+
|
|
220
252
|
# Seeds every normal relationship state before any resolver mutates the graph.
|
|
221
253
|
#
|
|
222
254
|
# Bidirectional removals can target reverse states that have not been
|
|
@@ -244,6 +276,48 @@ class Engine
|
|
|
244
276
|
state.resolve!
|
|
245
277
|
state
|
|
246
278
|
end
|
|
279
|
+
|
|
280
|
+
# Resolves every configured normal relationship state in collection and sequence order.
|
|
281
|
+
def resolve_all_relationships_pass!
|
|
282
|
+
@configuration.collections.each do |collection|
|
|
283
|
+
definitions = @configuration.normal_relationships_for(collection)
|
|
284
|
+
next if definitions.empty?
|
|
285
|
+
|
|
286
|
+
documents_for(collection).each do |document|
|
|
287
|
+
definitions.each do |definition|
|
|
288
|
+
resolve_relationships(document, definition.to_collection)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Runs deferred resolvers for inverse bidirectional mirrors after forward resolution settles.
|
|
295
|
+
def resolve_deferred_bidirectional_mirror_resolvers!
|
|
296
|
+
@configuration.collections.each do |collection|
|
|
297
|
+
definitions = @configuration.normal_relationships_for(collection).select do |definition|
|
|
298
|
+
deferred_bidirectional_mirror_definition?(definition)
|
|
299
|
+
end
|
|
300
|
+
next if definitions.empty?
|
|
301
|
+
|
|
302
|
+
documents_for(collection).each do |document|
|
|
303
|
+
definitions.each do |definition|
|
|
304
|
+
resolve_relationships(document, definition.to_collection)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Returns true when one definition should run in the deferred mirror-resolver pass.
|
|
311
|
+
def deferred_bidirectional_mirror_definition?(definition)
|
|
312
|
+
definition.bidirectional_mirror? && !definition.resolver_classes.empty?
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Returns true when this state should expose current links now but defer running its resolver.
|
|
316
|
+
def defer_bidirectional_mirror_resolver_for?(state)
|
|
317
|
+
return false unless @defer_bidirectional_mirror_resolvers
|
|
318
|
+
|
|
319
|
+
deferred_bidirectional_mirror_definition?(state.definition)
|
|
320
|
+
end
|
|
247
321
|
end
|
|
248
322
|
end
|
|
249
323
|
|
|
@@ -43,7 +43,7 @@ class Engine
|
|
|
43
43
|
return if @configuration.collections.empty?
|
|
44
44
|
|
|
45
45
|
@registry.validate_collections!
|
|
46
|
-
@registry.
|
|
46
|
+
@registry.validate_identity_schemes!(identity_schemes: @configuration.identity_schemes)
|
|
47
47
|
initial_active_document_ids = all_active_document_ids
|
|
48
48
|
initial_tree_seed_graph = build_tree_graph(active_document_ids: initial_active_document_ids)
|
|
49
49
|
normal_seed = NormalSeed.new(engine: self, active_document_ids: initial_active_document_ids)
|
|
@@ -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
|
|
@@ -291,30 +291,31 @@ class TreePhase
|
|
|
291
291
|
end.sort_by(&:relative_path)
|
|
292
292
|
end
|
|
293
293
|
|
|
294
|
-
# Attempts to reconnect one orphan to its surviving original
|
|
294
|
+
# Attempts to reconnect one orphan to its nearest surviving original ancestors.
|
|
295
295
|
def reattach_orphan!(graph:, document:)
|
|
296
|
-
|
|
296
|
+
nearest_surviving_ancestor_candidates_for(graph: graph, document: document).each do |candidate|
|
|
297
297
|
graph.add_edge(
|
|
298
298
|
parent_document: candidate.fetch(:document),
|
|
299
299
|
child_document: document,
|
|
300
300
|
tree_settings: candidate.fetch(:tree_settings),
|
|
301
301
|
definition: candidate.fetch(:definition),
|
|
302
|
-
source_description: 'pruning
|
|
302
|
+
source_description: 'pruning ancestors'
|
|
303
303
|
)
|
|
304
304
|
end
|
|
305
305
|
|
|
306
306
|
graph.parent_documents_for(document).any?
|
|
307
307
|
end
|
|
308
308
|
|
|
309
|
-
# Returns
|
|
310
|
-
def
|
|
309
|
+
# Returns the first surviving ancestor on every original parent lineage.
|
|
310
|
+
def nearest_surviving_ancestor_candidates_for(graph:, document:)
|
|
311
311
|
@provenance.original_parent_entries_for(document).each_with_object({}) do |parent_entry, candidates|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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,
|
|
318
319
|
definition: parent_entry.fetch(:definition),
|
|
319
320
|
tree_settings: parent_entry.fetch(:tree_settings)
|
|
320
321
|
}
|
|
@@ -322,6 +323,23 @@ class TreePhase
|
|
|
322
323
|
end.values
|
|
323
324
|
end
|
|
324
325
|
|
|
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
|
+
|
|
325
343
|
# Returns one document array with later duplicates removed by object id.
|
|
326
344
|
def unique_documents(documents)
|
|
327
345
|
Array(documents).each_with_object({}) do |document, unique_documents|
|
|
@@ -10,7 +10,7 @@ module Pruning
|
|
|
10
10
|
#
|
|
11
11
|
# Tree orphan repair needs stable provenance even after many prune rounds. This
|
|
12
12
|
# helper snapshots the first fully built tree graph before any pruning so later
|
|
13
|
-
# orphan handling can reattach surviving
|
|
13
|
+
# orphan handling can reattach nearest surviving ancestors in deterministic order.
|
|
14
14
|
class TreeProvenance
|
|
15
15
|
# Builds one immutable provenance index from one tree graph.
|
|
16
16
|
def initialize(tree_graph:)
|
|
@@ -24,26 +24,27 @@ class Accumulator
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# Adds one resolved relationship entry.
|
|
27
|
-
def add(document:, key:, metadata: nil, count: 1)
|
|
28
|
-
add_result(document: document, key: key, metadata: metadata, count: count) != :ignored
|
|
27
|
+
def add(document:, key:, scope: nil, metadata: nil, count: 1)
|
|
28
|
+
add_result(document: document, key: key, scope: scope, metadata: metadata, count: count) != :ignored
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# Adds one resolved relationship entry and returns how it changed the set.
|
|
32
|
-
def add_result(document:, key:, metadata: nil, count: 1)
|
|
32
|
+
def add_result(document:, key:, scope: nil, metadata: nil, count: 1)
|
|
33
33
|
add_detailed_result(
|
|
34
34
|
document: document,
|
|
35
35
|
key: key,
|
|
36
|
+
scope: scope,
|
|
36
37
|
metadata: metadata,
|
|
37
38
|
count: count
|
|
38
39
|
).fetch(:action)
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
# Adds one resolved relationship entry and returns the stored entry as well.
|
|
42
|
-
def add_detailed_result(document:, key:, metadata: nil, count: 1)
|
|
43
|
+
def add_detailed_result(document:, key:, scope: nil, metadata: nil, count: 1)
|
|
43
44
|
if @multiple_settings.keep?
|
|
44
45
|
return {
|
|
45
46
|
action: :added,
|
|
46
|
-
entry: append_entry(document: document, key: key, metadata: metadata, count: 1)
|
|
47
|
+
entry: append_entry(document: document, key: key, scope: scope, metadata: metadata, count: 1)
|
|
47
48
|
}
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -68,6 +69,7 @@ class Accumulator
|
|
|
68
69
|
entry = append_entry(
|
|
69
70
|
document: document,
|
|
70
71
|
key: key,
|
|
72
|
+
scope: scope,
|
|
71
73
|
metadata: metadata,
|
|
72
74
|
count: counted_mode? ? normalise_count(count) : 1
|
|
73
75
|
)
|
|
@@ -157,10 +159,11 @@ class Accumulator
|
|
|
157
159
|
end
|
|
158
160
|
|
|
159
161
|
# Appends one entry while recording its first-seen order.
|
|
160
|
-
def append_entry(document:, key:, metadata:, count:)
|
|
162
|
+
def append_entry(document:, key:, scope:, metadata:, count:)
|
|
161
163
|
entry = {
|
|
162
164
|
document: document,
|
|
163
165
|
key: key,
|
|
166
|
+
scope: duplicate_scope(scope),
|
|
164
167
|
metadata: normalise_metadata(metadata),
|
|
165
168
|
count: count,
|
|
166
169
|
first_seen_index: @next_position
|
|
@@ -184,6 +187,7 @@ class Accumulator
|
|
|
184
187
|
{
|
|
185
188
|
document: entry.fetch(:document),
|
|
186
189
|
key: entry.fetch(:key),
|
|
190
|
+
scope: duplicate_scope(entry.fetch(:scope)),
|
|
187
191
|
metadata: normalise_metadata(entry.fetch(:metadata)),
|
|
188
192
|
count: entry.fetch(:count),
|
|
189
193
|
first_seen_index: entry.fetch(:first_seen_index)
|
|
@@ -195,11 +199,19 @@ class Accumulator
|
|
|
195
199
|
@reference_template.build(
|
|
196
200
|
document: entry.fetch(:document),
|
|
197
201
|
key: entry.fetch(:key),
|
|
202
|
+
scope: entry.fetch(:scope),
|
|
198
203
|
metadata: entry.fetch(:metadata),
|
|
199
204
|
count: entry.fetch(:count)
|
|
200
205
|
)
|
|
201
206
|
end
|
|
202
207
|
|
|
208
|
+
# Returns a shallow copy of one complete scope hash while preserving scalar values exactly.
|
|
209
|
+
def duplicate_scope(scope)
|
|
210
|
+
return nil if scope.nil?
|
|
211
|
+
|
|
212
|
+
normalise_metadata(scope)
|
|
213
|
+
end
|
|
214
|
+
|
|
203
215
|
# Merges later metadata under the first-seen metadata.
|
|
204
216
|
def merge_metadata_under(existing_metadata:, incoming_metadata:)
|
|
205
217
|
Jekyll::Plugins::Relationships::Support.hash_deep_merge(
|