philiprehberger-dependency_graph 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9091598b68332edaee82a0a8a55bd23d3d00278c78466b88ac86b0e25651390
4
- data.tar.gz: '0889cc1cdd1d277dc63b4c08228b6e091bacccbcb3670833d5015c4aec02cba9'
3
+ metadata.gz: 2fc52e7f3c4c62be66dc6d19901b9d82c953fa8c5ea32f22b3cdada283c02fd5
4
+ data.tar.gz: d334350bfb017e0172ec0145718904875c94fd1568e6114f3a113b8f1a124ae1
5
5
  SHA512:
6
- metadata.gz: c9ecc7105f38fe9d6dcb1a63e2456d91883f26bb114b2097ea2bada656f80dfd77065c5143dadf579a9f29b07b069fc3c2fb98d39ddca7be71c65c1537275d9c
7
- data.tar.gz: 37f0dc97583b44d80f9c046dd68375fc2749ac14f360905b0de8c64e3f630645814471d012f053434b91318ec9ab77ea9f6af0b3cb839846ed760bbc31d1ef1b
6
+ metadata.gz: bf8205242047d62b202affb156583a13d89cdeb3db6d59fb7c4eeec1d9fb0b06ea23ffe5da925f37d97dafe0fb1b1cec628deba7bd24b33ac975e049add9758a
7
+ data.tar.gz: 5116b0ec4782836b7b74b0f7135b842af9774b2108a9820212f0a73466840769f623688b5384084ca78597fd5299aa4ebb9b9a1dfdf3d4be5c7b5cd5b2c1a11d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-04-09
11
+
12
+ ### Added
13
+ - `Graph#reverse` — return a new graph with all edges flipped (useful for dependent analysis)
14
+ - `Graph#all_dependents_of(item)` — transitive closure of items depending on a node
15
+ - `Graph#independent?(a, b)` — check whether two nodes are mutually unreachable
16
+
10
17
  ## [0.2.0] - 2026-04-03
11
18
 
12
19
  ### Added
data/README.md CHANGED
@@ -128,6 +128,9 @@ graph.resolve # => [:a, :b, :c]
128
128
  | `Graph#roots` | Nodes with no dependencies |
129
129
  | `Graph#leaves` | Nodes with no dependents |
130
130
  | `Graph#depth(item)` | Maximum dependency depth of a node |
131
+ | `Graph#reverse` | Return a new graph with all edges flipped |
132
+ | `Graph#all_dependents_of(item)` | All transitive dependents of a node |
133
+ | `Graph#independent?(a, b)` | Whether two nodes are mutually unreachable |
131
134
 
132
135
  ## Development
133
136
 
@@ -208,6 +208,110 @@ module Philiprehberger
208
208
  @nodes.keys.reject { |node| depended_on.include?(node) }
209
209
  end
210
210
 
211
+ # Merge another graph into this one, combining nodes and dependencies
212
+ #
213
+ # @param other [Graph] another graph to merge
214
+ # @return [self]
215
+ def merge(other)
216
+ raise Error, 'Can only merge Graph instances' unless other.is_a?(self.class)
217
+
218
+ other.nodes.each do |node, deps|
219
+ @nodes[node] ||= []
220
+ deps.each do |dep|
221
+ @nodes[node] << dep unless @nodes[node].include?(dep)
222
+ end
223
+ end
224
+ self
225
+ end
226
+
227
+ # Remove a node and all edges referencing it
228
+ #
229
+ # @param item [Object] the item to remove
230
+ # @return [Boolean] true if the node existed, false otherwise
231
+ def remove(item)
232
+ return false unless @nodes.key?(item)
233
+
234
+ @nodes.delete(item)
235
+ @nodes.each_value { |deps| deps.delete(item) }
236
+ true
237
+ end
238
+
239
+ # Total number of nodes in the graph
240
+ #
241
+ # @return [Integer]
242
+ def size
243
+ @nodes.size
244
+ end
245
+
246
+ # Whether the graph has no nodes
247
+ #
248
+ # @return [Boolean]
249
+ def empty?
250
+ @nodes.empty?
251
+ end
252
+
253
+ # Return a new graph with all edges reversed (dependents become dependencies)
254
+ #
255
+ # @return [Graph] a new graph where each edge direction is flipped
256
+ def reverse
257
+ new_graph = self.class.new
258
+ @nodes.each_key { |node| new_graph.instance_variable_get(:@nodes)[node] ||= [] }
259
+ @nodes.each do |node, deps|
260
+ deps.each { |dep| new_graph.add(dep, depends_on: [node]) }
261
+ end
262
+ new_graph
263
+ end
264
+
265
+ # Return all transitive dependents of an item (direct + indirect)
266
+ #
267
+ # @param item [Object] the item to query
268
+ # @return [Array] all items that depend on this item, directly or transitively
269
+ def all_dependents_of(item)
270
+ return [] unless @nodes.key?(item)
271
+
272
+ visited = {}
273
+ queue = dependents_of(item)
274
+ result = []
275
+
276
+ until queue.empty?
277
+ dep = queue.shift
278
+ next if visited[dep]
279
+
280
+ visited[dep] = true
281
+ result << dep
282
+ queue.concat(dependents_of(dep))
283
+ end
284
+
285
+ result
286
+ end
287
+
288
+ # Check whether two nodes are independent (neither depends on the other transitively)
289
+ #
290
+ # @param node_a [Object]
291
+ # @param node_b [Object]
292
+ # @return [Boolean] true if neither node is reachable from the other
293
+ def independent?(node_a, node_b)
294
+ return false if node_a == node_b
295
+ return false unless @nodes.key?(node_a) && @nodes.key?(node_b)
296
+
297
+ !all_dependencies_of(node_a).include?(node_b) &&
298
+ !all_dependencies_of(node_b).include?(node_a)
299
+ end
300
+
301
+ # Export the graph in Graphviz DOT format
302
+ #
303
+ # @param name [String] the digraph name
304
+ # @return [String] DOT source
305
+ def to_dot(name: 'G')
306
+ lines = ["digraph #{name} {"]
307
+ @nodes.each_key { |node| lines << " #{dot_quote(node)};" }
308
+ @nodes.each do |node, deps|
309
+ deps.each { |dep| lines << " #{dot_quote(node)} -> #{dot_quote(dep)};" }
310
+ end
311
+ lines << '}'
312
+ lines.join("\n")
313
+ end
314
+
211
315
  # Calculate maximum dependency depth for a node (longest path from any root to this node)
212
316
  #
213
317
  # @param item [Object] the item to query
@@ -221,6 +325,10 @@ module Philiprehberger
221
325
 
222
326
  private
223
327
 
328
+ def dot_quote(node)
329
+ %("#{node.to_s.gsub('"', '\"')}")
330
+ end
331
+
224
332
  def build_path(visited, from, to)
225
333
  path = [to]
226
334
  current = to
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module DependencyGraph
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-dependency_graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-04 00:00:00.000000000 Z
11
+ date: 2026-04-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Build and resolve dependency graphs using topological sort, detect cycles,
14
14
  generate parallel execution batches, query dependencies and dependents, find shortest