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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jekyll-relationships/configuration/debug_setting.rb +72 -12
  3. data/lib/jekyll-relationships/configuration/defaults.rb +3 -1
  4. data/lib/jekyll-relationships/configuration/frontmatter.rb +28 -1
  5. data/lib/jekyll-relationships/configuration/parser.rb +20 -3
  6. data/lib/jekyll-relationships/configuration/prune_rule_settings.rb +33 -5
  7. data/lib/jekyll-relationships/configuration/tree_frontmatter.rb +8 -2
  8. data/lib/jekyll-relationships/configuration.rb +43 -8
  9. data/lib/jekyll-relationships/debug_logger.rb +94 -3
  10. data/lib/jekyll-relationships/definitions/normal_relationship.rb +13 -2
  11. data/lib/jekyll-relationships/definitions/prune_rule.rb +19 -3
  12. data/lib/jekyll-relationships/definitions/tree_relationship.rb +8 -2
  13. data/lib/jekyll-relationships/documents/registry.rb +182 -33
  14. data/lib/jekyll-relationships/engine/normal_seed.rb +222 -0
  15. data/lib/jekyll-relationships/engine/persisted_links.rb +365 -0
  16. data/lib/jekyll-relationships/engine/raw_path_state.rb +46 -116
  17. data/lib/jekyll-relationships/engine/relationship_state.rb +67 -56
  18. data/lib/jekyll-relationships/engine/session.rb +159 -19
  19. data/lib/jekyll-relationships/engine/write_back.rb +3 -42
  20. data/lib/jekyll-relationships/engine.rb +168 -46
  21. data/lib/jekyll-relationships/pruning/rule_pruner.rb +7 -7
  22. data/lib/jekyll-relationships/pruning/tree_phase.rb +128 -124
  23. data/lib/jekyll-relationships/pruning/tree_provenance.rb +1 -1
  24. data/lib/jekyll-relationships/references/accumulator.rb +87 -9
  25. data/lib/jekyll-relationships/references/template.rb +46 -11
  26. data/lib/jekyll-relationships/resolvers/base.rb +59 -2
  27. data/lib/jekyll-relationships/support/frontmatter_matcher.rb +76 -0
  28. data/lib/jekyll-relationships/support/placeholders.rb +1 -0
  29. data/lib/jekyll-relationships/trees/edge_builder.rb +43 -11
  30. data/lib/jekyll-relationships/trees/graph.rb +135 -17
  31. data/lib/jekyll-relationships/trees/root_distances.rb +44 -0
  32. data/lib/jekyll-relationships/version.rb +1 -1
  33. data/lib/jekyll-relationships.rb +1 -0
  34. data/readme.md +185 -26
  35. metadata +6 -2
@@ -12,13 +12,14 @@ module Definitions
12
12
  # and which frontmatter, maximum, URL, and primary-key rules should be used to
13
13
  # resolve references for this concrete relationship definition.
14
14
  class TreeRelationship
15
- attr_reader :from_collection, :to_collection, :primary_path, :tree_settings, :sequence
15
+ attr_reader :from_collection, :to_collection, :primary_path, :scope_fields, :tree_settings, :sequence
16
16
 
17
17
  # Captures the allowed parent-child directions for one tree definition.
18
- def initialize(from_collection:, to_collection:, primary_path:, parent_child_pairs:, tree_settings:, debug:, sequence:)
18
+ def initialize(from_collection:, to_collection:, primary_path:, scope_fields: [], parent_child_pairs:, tree_settings:, debug:, sequence:)
19
19
  @from_collection = from_collection
20
20
  @to_collection = to_collection
21
21
  @primary_path = primary_path
22
+ @scope_fields = scope_fields.freeze
22
23
  @parent_child_pairs = parent_child_pairs
23
24
  @tree_settings = tree_settings
24
25
  @debug = debug
@@ -35,6 +36,11 @@ class TreeRelationship
35
36
  @debug.enabled?(area)
36
37
  end
37
38
 
39
+ # Returns true when one event's related IDs satisfy this definition's debug filter.
40
+ def debug_ids_match?(ids)
41
+ @debug.matches_ids?(ids)
42
+ end
43
+
38
44
  # Returns true when one parent-child direction is permitted.
39
45
  def allows_parent_child?(parent_collection:, child_collection:)
40
46
  @parent_child_pairs.include?([parent_collection, child_collection])
@@ -6,17 +6,18 @@ module Plugins
6
6
  module Relationships
7
7
  module Documents
8
8
 
9
- # Indexes participating documents by collection and primary key.
9
+ # Indexes participating documents by collection, primary key, and optional complete scope.
10
10
  #
11
- # Indices are built lazily per primary-key path so relationship definitions can
12
- # use different key schemes without rebuilding unrelated lookups.
11
+ # Indices are built lazily per complete identity scheme so relationship definitions can
12
+ # use different primary and scope paths without rebuilding unrelated lookups.
13
13
  class Registry
14
14
 
15
15
  # Builds the registry over the collections used by the configuration.
16
16
  def initialize(site:, collections:)
17
17
  @site = site
18
18
  @collections = collections
19
- @indices_by_primary_path = {}
19
+ @indices_by_identity = {}
20
+ @data_path = Jekyll::Plugins::Relationships::Support::DataPath.new
20
21
  end
21
22
 
22
23
  # Returns true when the document belongs to one of the tracked collections.
@@ -48,7 +49,19 @@ class Registry
48
49
  # Builds every requested primary-key index so duplicates fail eagerly.
49
50
  def validate_primary_paths!(primary_paths:)
50
51
  Array(primary_paths).uniq.each do |primary_path|
51
- index_for(primary_path)
52
+ index_for(primary_path: primary_path, scope_fields: [])
53
+ end
54
+ end
55
+
56
+ # Builds every requested identity index so missing scope values and duplicate identities fail eagerly.
57
+ def validate_identity_schemes!(identity_schemes:)
58
+ Array(identity_schemes).each do |scheme|
59
+ index_for(
60
+ primary_path: scheme.fetch(:primary_path),
61
+ scope_fields: scheme.fetch(:scope_fields),
62
+ collections: scheme.fetch(:collections),
63
+ relationship: scheme.fetch(:relationships).join(', ')
64
+ )
52
65
  end
53
66
  end
54
67
 
@@ -57,61 +70,157 @@ class Registry
57
70
  if primary_path.nil?
58
71
  default_relative_key(document)
59
72
  else
60
- value = Jekyll::Plugins::Relationships::Support::DataPath.new.read(document.data, primary_path)
73
+ value = @data_path.read(document.data, primary_path)
61
74
  raise ResolutionError, "Document `#{document.relative_path}` is missing primary key path `#{primary_path}`." if value.nil?
62
75
 
63
76
  value.to_s
64
77
  end
65
78
  end
66
79
 
67
- # Resolves one key to a document, optionally constrained to a collection.
68
- def lookup(key:, primary_path:, collection: nil)
69
- index = index_for(primary_path)
70
- if collection
71
- document = fetch_nested(index.fetch(:by_collection), collection, key.to_s)
72
- raise ResolutionError, "Could not resolve key `#{key}` in collection `#{collection}`." unless document
80
+ # Reads and validates a document's complete scope for one configured identity scheme.
81
+ def scope_for(document, scope_fields:, relationship: nil)
82
+ return nil if scope_fields.empty?
83
+
84
+ scope_fields.each_with_object({}) do |field, scope|
85
+ value = @data_path.read(document.data, field.path)
86
+ if value.nil?
87
+ raise ResolutionError, "#{relationship_prefix(relationship)}document `#{document.relative_path}` is missing required scope field `#{field.name}` at frontmatter path `#{field.path}`."
88
+ end
89
+
90
+ scope[field.name] = value
91
+ end
92
+ end
93
+
94
+ # Resolves reference overrides over the referring document's scope and validates every configured field.
95
+ def effective_scope_for(reference_scope:, referring_document:, scope_fields:, relationship: nil)
96
+ return nil if scope_fields.empty? && (reference_scope.nil? || reference_scope.empty?)
97
+
98
+ reference_scope ||= {}
99
+ unless reference_scope.is_a?(Hash)
100
+ raise ResolutionError, "#{relationship_prefix(relationship)}reference on document `#{referring_document.relative_path}` must provide scope as a hash."
101
+ end
102
+
103
+ configured_names = scope_fields.map(&:name)
104
+ unknown_names = reference_scope.keys.map(&:to_s) - configured_names
105
+ unless unknown_names.empty?
106
+ raise ResolutionError, "#{relationship_prefix(relationship)}reference on document `#{referring_document.relative_path}` uses unconfigured scope field `#{unknown_names.first}`."
107
+ end
108
+
109
+ scope_fields.each_with_object({}) do |field, effective_scope|
110
+ value = if hash_property?(reference_scope, field.name)
111
+ Jekyll::Plugins::Relationships::Support::FrontmatterPath.read_hash(reference_scope, field.name)
112
+ else
113
+ @data_path.read(referring_document.data, field.path)
114
+ end
115
+ if value.nil?
116
+ raise ResolutionError, "#{relationship_prefix(relationship)}reference on document `#{referring_document.relative_path}` has no effective value for scope field `#{field.name}`."
117
+ end
73
118
 
74
- return document
119
+ effective_scope[field.name] = value
75
120
  end
121
+ end
122
+
123
+ # Ensures one already-selected target contains and exactly matches an effective scope.
124
+ def validate_scope_match!(document:, scope:, scope_fields:, relationship: nil, referring_document: nil)
125
+ return nil if scope_fields.empty?
76
126
 
77
- matches = index.fetch(:by_key).fetch(key.to_s, [])
78
- raise ResolutionError, "Could not resolve key-only reference `#{key}`." if matches.empty?
79
- return matches.first if index.fetch(:site_wide_unique)
80
- return matches.first if matches.length == 1
127
+ target_scope = scope_for(document, scope_fields: scope_fields, relationship: relationship)
128
+ scope_fields.each do |field|
129
+ next if target_scope.fetch(field.name) == scope.fetch(field.name)
81
130
 
82
- collections = matches.map { |document| collection_label(document) }.uniq.sort.join(', ')
83
- raise ResolutionError, "Key-only reference `#{key}` is ambiguous across collections: #{collections}."
131
+ raise ResolutionError, scope_mismatch_message(
132
+ document: document,
133
+ field: field,
134
+ expected_value: scope.fetch(field.name),
135
+ actual_value: target_scope.fetch(field.name),
136
+ relationship: relationship,
137
+ referring_document: referring_document
138
+ )
139
+ end
140
+
141
+ target_scope
142
+ end
143
+
144
+ # Resolves one key and complete scope to a document, optionally constrained to a collection.
145
+ def lookup(key:, primary_path:, collection: nil, scope_fields: [], scope: nil, relationship: nil, referring_document: nil)
146
+ index = index_for(primary_path: primary_path, scope_fields: scope_fields, relationship: relationship)
147
+ candidates = if collection
148
+ fetch_nested(index.fetch(:by_collection), collection, key.to_s) || []
149
+ else
150
+ index.fetch(:by_key).fetch(key.to_s, [])
151
+ end
152
+ if candidates.empty?
153
+ if collection
154
+ raise ResolutionError, "#{relationship_prefix(relationship)}could not resolve key `#{key}` in collection `#{collection}`#{source_suffix(referring_document)}."
155
+ end
156
+
157
+ raise ResolutionError, "#{relationship_prefix(relationship)}could not resolve key-only reference `#{key}`#{source_suffix(referring_document)}."
158
+ end
159
+
160
+ matches = if scope_fields.empty?
161
+ candidates
162
+ else
163
+ candidates.select { |entry| entry.fetch(:scope) == scope }
164
+ end
165
+ if matches.empty?
166
+ raise_scope_mismatch!(
167
+ candidates: candidates,
168
+ scope: scope,
169
+ scope_fields: scope_fields,
170
+ relationship: relationship,
171
+ referring_document: referring_document
172
+ )
173
+ end
174
+
175
+ matched_documents = matches.map { |entry| entry.fetch(:document) }
176
+ return matched_documents.first if collection
177
+
178
+ return matched_documents.first if matched_documents.length == 1
179
+
180
+ collections = matched_documents.map { |document| collection_label(document) }.uniq.sort.join(', ')
181
+ raise ResolutionError, "#{relationship_prefix(relationship)}key-only reference `#{key}` is ambiguous across collections: #{collections}#{source_suffix(referring_document)}."
84
182
  end
85
183
 
86
184
  private
87
185
 
88
- # Returns or builds the index for one primary-key scheme.
89
- def index_for(primary_path)
90
- signature = primary_path.nil? ? '__relative_path__' : primary_path
91
- @indices_by_primary_path[signature] ||= build_index(primary_path)
186
+ # Returns or builds the index for one complete identity scheme.
187
+ def index_for(primary_path:, scope_fields:, collections: nil, relationship: nil)
188
+ signature = [primary_path.nil? ? '__relative_path__' : primary_path, scope_fields.map(&:signature)]
189
+ @indices_by_identity[signature] ||= build_index(
190
+ primary_path: primary_path,
191
+ scope_fields: scope_fields,
192
+ collections: collections || @collections,
193
+ relationship: relationship
194
+ )
92
195
  end
93
196
 
94
- # Builds the collection and global indices for one scheme.
95
- def build_index(primary_path)
96
- by_collection = Hash.new { |hash, key| hash[key] = {} }
197
+ # Builds the collection and global indices for one complete identity scheme.
198
+ def build_index(primary_path:, scope_fields:, collections:, relationship: nil)
199
+ by_collection = Hash.new { |hash, key| hash[key] = Hash.new { |nested_hash, nested_key| nested_hash[nested_key] = [] } }
97
200
  by_key = Hash.new { |hash, key| hash[key] = [] }
98
201
 
99
- @collections.each do |collection|
202
+ collections.each do |collection|
100
203
  documents_for(collection).each do |document|
101
204
  key = key_for(document, primary_path: primary_path)
102
- if by_collection[collection].key?(key)
103
- raise ResolutionError, "Primary key `#{key}` is duplicated within collection `#{collection}`."
205
+ scope = scope_for(document, scope_fields: scope_fields, relationship: relationship)
206
+ existing_entry = by_collection[collection][key].find { |entry| entry.fetch(:scope) == scope }
207
+ if existing_entry
208
+ if scope_fields.empty?
209
+ raise ResolutionError, "Primary key `#{key}` is duplicated within collection `#{collection}`."
210
+ end
211
+
212
+ raise ResolutionError, "#{relationship_prefix(relationship)}primary key `#{key}` is duplicated within collection `#{collection}` and scope #{scope.inspect} by documents `#{existing_entry.fetch(:document).relative_path}` and `#{document.relative_path}`."
104
213
  end
105
214
 
106
- by_collection[collection][key] = document
107
- by_key[key] << document
215
+ entry = { document: document, scope: scope }
216
+ by_collection[collection][key] << entry
217
+ by_key[key] << entry
108
218
  end
109
219
  end
110
220
 
111
221
  {
112
222
  by_collection: by_collection,
113
- by_key: by_key,
114
- site_wide_unique: by_key.values.all? { |documents| documents.length <= 1 }
223
+ by_key: by_key
115
224
  }
116
225
  end
117
226
 
@@ -123,6 +232,46 @@ class Registry
123
232
  first_hash[second_key]
124
233
  end
125
234
 
235
+ # Raises a field-specific error when key candidates exist but none match the requested complete scope.
236
+ def raise_scope_mismatch!(candidates:, scope:, scope_fields:, relationship:, referring_document:)
237
+ candidate = candidates.first
238
+ field = scope_fields.find do |configured_field|
239
+ candidate.fetch(:scope).fetch(configured_field.name) != scope.fetch(configured_field.name)
240
+ end
241
+ raise ResolutionError, scope_mismatch_message(
242
+ document: candidate.fetch(:document),
243
+ field: field,
244
+ expected_value: scope.fetch(field.name),
245
+ actual_value: candidate.fetch(:scope).fetch(field.name),
246
+ relationship: relationship,
247
+ referring_document: referring_document
248
+ )
249
+ end
250
+
251
+ # Builds one detailed exact-match failure for a configured scope field.
252
+ def scope_mismatch_message(document:, field:, expected_value:, actual_value:, relationship:, referring_document:)
253
+ "#{relationship_prefix(relationship)}target document `#{document.relative_path}` has scope field `#{field.name}` value `#{actual_value.inspect}`, not the required `#{expected_value.inspect}`#{source_suffix(referring_document)}."
254
+ end
255
+
256
+ # Returns true when one literal scope key exists in string or symbol form.
257
+ def hash_property?(hash, property)
258
+ hash.key?(property) || hash.key?(property.to_s) || hash.key?(property.to_s.to_sym)
259
+ end
260
+
261
+ # Prefixes an error with its concrete relationship where available.
262
+ def relationship_prefix(relationship)
263
+ return '' if relationship.nil? || relationship.to_s.empty?
264
+
265
+ "Relationship `#{relationship}`: "
266
+ end
267
+
268
+ # Adds the referring document to lookup diagnostics where available.
269
+ def source_suffix(document)
270
+ return '' unless document
271
+
272
+ " from document `#{document.relative_path}`"
273
+ end
274
+
126
275
  # Implements the README's default relative-path key rule.
127
276
  def default_relative_key(document)
128
277
  document.relative_path.sub(/\A_/, '').sub(/#{Regexp.escape(document.extname)}\z/, '')
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module Plugins
5
+
6
+ module Relationships
7
+
8
+ class Engine
9
+
10
+ # Stores the source-derived normal relationship graph used to seed each round.
11
+ #
12
+ # The seed graph is built once from frontmatter, including deterministic
13
+ # bidirectional mirrors, and is later reduced only by document removals.
14
+ # Resolver output never flows back into this structure.
15
+ class NormalSeed
16
+ # Builds one normal seed graph for the current active document set.
17
+ def initialize(engine:, active_document_ids:)
18
+ @engine = engine
19
+ @configuration = engine.configuration
20
+ @registry = engine.registry
21
+ @data_path = engine.data_path
22
+ @debug_logger = engine.debug_logger
23
+ @active_document_ids = active_document_ids.each_with_object({}) do |document_id, active_ids|
24
+ active_ids[document_id] = true
25
+ end
26
+ @string_array = Jekyll::Plugins::Relationships::Support::StringArray.new
27
+ @raw_path_states = {}
28
+ @entries_by_state = {}
29
+ build!
30
+ end
31
+
32
+ # Returns the seed entries for one concrete document-to-collection pair.
33
+ def entries_for(document, to_collection)
34
+ duplicate_entries(@entries_by_state[[document.object_id, to_collection]])
35
+ end
36
+
37
+ # Permanently removes documents from the seed graph in both source and target positions.
38
+ def remove_documents!(documents)
39
+ removed_document_ids = Array(documents).each_with_object({}) do |document, lookup|
40
+ lookup[document.object_id] = true
41
+ end
42
+ return if removed_document_ids.empty?
43
+
44
+ @entries_by_state.delete_if do |(source_document_id, _to_collection), _entries|
45
+ removed_document_ids.key?(source_document_id)
46
+ end
47
+ @entries_by_state.each_value do |entries|
48
+ entries.reject! do |entry|
49
+ removed_document_ids.key?(entry.fetch(:document).object_id)
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Builds the full source-derived seed graph once.
57
+ def build!
58
+ accumulators = {}
59
+
60
+ @configuration.collections.each do |collection|
61
+ @configuration.normal_relationships_for(collection).each do |definition|
62
+ next unless definition.reads_frontmatter
63
+
64
+ documents_for(collection).each do |document|
65
+ seed_definition_from_frontmatter(
66
+ accumulators: accumulators,
67
+ document: document,
68
+ definition: definition
69
+ )
70
+ end
71
+ end
72
+ end
73
+
74
+ @entries_by_state = accumulators.each_with_object({}) do |(state_key, accumulator), entries_by_state|
75
+ entries_by_state[state_key] = accumulator.encounter_entries
76
+ end
77
+ end
78
+
79
+ # Reads and stores one concrete direct relationship definition from frontmatter.
80
+ def seed_definition_from_frontmatter(accumulators:, document:, definition:)
81
+ definition.foreign_paths.each do |path|
82
+ raw_state = raw_path_state(document, path, definition)
83
+ debug_relationship_event(
84
+ document: document,
85
+ definition: definition,
86
+ event: 'raw_path',
87
+ details: {
88
+ path: path,
89
+ present: raw_state.present?,
90
+ value: raw_state.raw_value
91
+ }
92
+ )
93
+ next unless raw_state.present?
94
+
95
+ resolved_entries = raw_state.resolved_entries_for(
96
+ primary_path: definition.primary_path,
97
+ scope_fields: definition.scope_fields,
98
+ registry: @registry,
99
+ relationship: "#{definition.from_collection} -> #{definition.to_collection}",
100
+ active_document_checker: proc { |resolved_document| active_document?(resolved_document) }
101
+ )
102
+ debug_relationship_event(
103
+ document: document,
104
+ definition: definition,
105
+ event: 'raw_path_resolved',
106
+ details: {
107
+ path: path,
108
+ entries: resolved_entries.compact
109
+ }
110
+ )
111
+
112
+ resolved_entries.each do |entry|
113
+ next unless entry
114
+ if entry.fetch(:document).collection.label != definition.to_collection
115
+ debug_relationship_event(
116
+ document: document,
117
+ definition: definition,
118
+ event: 'raw_path_skipped',
119
+ details: {
120
+ path: path,
121
+ target: entry.fetch(:document),
122
+ target_key: entry.fetch(:key),
123
+ actual_collection: entry.fetch(:document).collection.label,
124
+ expected_collection: definition.to_collection
125
+ }
126
+ )
127
+ next
128
+ end
129
+
130
+ add_direct_entry(
131
+ accumulators: accumulators,
132
+ document: document,
133
+ definition: definition,
134
+ entry: entry
135
+ )
136
+ end
137
+ end
138
+ end
139
+
140
+ # Adds one direct link and its deterministic bidirectional mirror when configured.
141
+ def add_direct_entry(accumulators:, document:, definition:, entry:)
142
+ accumulator_for(
143
+ accumulators: accumulators,
144
+ document: document,
145
+ to_collection: definition.to_collection
146
+ ).add(
147
+ document: entry.fetch(:document),
148
+ key: entry.fetch(:key),
149
+ scope: entry.fetch(:scope),
150
+ metadata: entry.fetch(:metadata),
151
+ count: entry.fetch(:count)
152
+ )
153
+
154
+ return unless definition.bidirectional
155
+
156
+ accumulator_for(
157
+ accumulators: accumulators,
158
+ document: entry.fetch(:document),
159
+ to_collection: definition.from_collection
160
+ ).add(
161
+ document: document,
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}"),
164
+ metadata: entry.fetch(:metadata),
165
+ count: entry.fetch(:count)
166
+ )
167
+ end
168
+
169
+ # Returns one reusable accumulator for one concrete source pair.
170
+ def accumulator_for(accumulators:, document:, to_collection:)
171
+ accumulators[[document.object_id, to_collection]] ||= Jekyll::Plugins::Relationships::References::Accumulator.new(
172
+ reference_template: @configuration.reference_template,
173
+ multiple_settings: @configuration.multiple_settings
174
+ )
175
+ end
176
+
177
+ # Returns every active document for one collection.
178
+ def documents_for(collection)
179
+ @registry.documents_for(collection).select do |document|
180
+ active_document?(document)
181
+ end
182
+ end
183
+
184
+ # Returns true when one document is still active in the seed graph.
185
+ def active_document?(document)
186
+ @active_document_ids.key?(document.object_id)
187
+ end
188
+
189
+ # Returns the cached raw-path parser for one document/path pair.
190
+ def raw_path_state(document, path, definition)
191
+ @raw_path_states[[document.object_id, path, definition.object_id]] ||= RawPathState.new(
192
+ document: document,
193
+ path: path,
194
+ data_path: @data_path,
195
+ string_array: @string_array,
196
+ reference_template: @configuration.reference_template,
197
+ reference_context: "Relationship `#{definition.from_collection} -> #{definition.to_collection}` reference on document `#{document.relative_path}`"
198
+ )
199
+ end
200
+
201
+ # Duplicates one stored seed-entry array defensively.
202
+ def duplicate_entries(entries)
203
+ Array(entries).map(&:dup)
204
+ end
205
+
206
+ # Emits one debug event while the seed graph is being built.
207
+ def debug_relationship_event(document:, definition:, event:, details:)
208
+ @debug_logger.relationship_event(
209
+ document: document,
210
+ definition: definition,
211
+ area: 'upgrading',
212
+ event: event,
213
+ details: details
214
+ )
215
+ end
216
+ end
217
+ end
218
+
219
+ end
220
+
221
+ end
222
+ end