philiprehberger-dependency_graph 0.3.0 → 0.5.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: 2fc52e7f3c4c62be66dc6d19901b9d82c953fa8c5ea32f22b3cdada283c02fd5
4
- data.tar.gz: d334350bfb017e0172ec0145718904875c94fd1568e6114f3a113b8f1a124ae1
3
+ metadata.gz: b91c3f882d45402ff3d62528249734aaa0ff45b1f56051cf3775a9b765538ba3
4
+ data.tar.gz: 7533cce9223239659948ad073b5c2f5e9b358c53b8c3d44d80760efb274f3335
5
5
  SHA512:
6
- metadata.gz: bf8205242047d62b202affb156583a13d89cdeb3db6d59fb7c4eeec1d9fb0b06ea23ffe5da925f37d97dafe0fb1b1cec628deba7bd24b33ac975e049add9758a
7
- data.tar.gz: 5116b0ec4782836b7b74b0f7135b842af9774b2108a9820212f0a73466840769f623688b5384084ca78597fd5299aa4ebb9b9a1dfdf3d4be5c7b5cd5b2c1a11d
6
+ metadata.gz: 561ce49d415c0c222b6813489a5f8e454690431a37447ee333ee83ebca3e8a90f662a9fcec2dc17faa3bbfb6a3bfb64aac4a50c9cb3e83e843bb5af9aee7d2cd
7
+ data.tar.gz: ebfb34c46ab091ff1f998100ac8141719c0bcc2e8b261db9e4d5ba375747f3fa7511ed4261eb52e0ed3d972b9d84df32c4b0f9a99c6ba9174334b72066102db6
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-05-01
11
+
12
+ ### Added
13
+ - `Graph#in_degree(item)` — count of direct dependents (how many items depend on this one)
14
+ - `Graph#out_degree(item)` — count of direct dependencies for an item
15
+
16
+ ## [0.4.0] - 2026-04-21
17
+
18
+ ### Added
19
+ - `Graph#to_dot` — Graphviz DOT export for visualization
20
+
21
+ ### Fixed
22
+ - `bug_report.yml` — require Ruby version; add Gem version input per guide
23
+
10
24
  ## [0.3.0] - 2026-04-09
11
25
 
12
26
  ### Added
data/README.md CHANGED
@@ -77,6 +77,9 @@ graph.add(:d, depends_on: [:b, :c])
77
77
  graph.dependencies_of(:d) # => [:b, :c]
78
78
  graph.all_dependencies_of(:d) # => [:b, :c, :a]
79
79
  graph.dependents_of(:b) # => [:c, :d]
80
+
81
+ graph.out_degree(:d) # => 2
82
+ graph.in_degree(:b) # => 2
80
83
  ```
81
84
 
82
85
  ### Path Finding
@@ -110,6 +113,23 @@ graph.add(:a).add(:b, depends_on: [:a]).add(:c, depends_on: [:b])
110
113
  graph.resolve # => [:a, :b, :c]
111
114
  ```
112
115
 
116
+ ### Graphviz Export
117
+
118
+ ```ruby
119
+ graph = Philiprehberger::DependencyGraph.new
120
+ graph.add(:a)
121
+ graph.add(:b, depends_on: [:a])
122
+ graph.add(:c, depends_on: [:b])
123
+
124
+ puts graph.to_dot
125
+ # digraph dependencies {
126
+ # "b" -> "a";
127
+ # "c" -> "b";
128
+ # }
129
+
130
+ graph.to_dot(name: 'MyDeps') # Customize the digraph name
131
+ ```
132
+
113
133
  ## API
114
134
 
115
135
  | Method | Description |
@@ -123,6 +143,8 @@ graph.resolve # => [:a, :b, :c]
123
143
  | `Graph#dependencies_of(item)` | Direct dependencies of an item |
124
144
  | `Graph#all_dependencies_of(item)` | All transitive dependencies |
125
145
  | `Graph#dependents_of(item)` | Items that directly depend on an item |
146
+ | `Graph#in_degree(item)` | Count of direct dependents for an item |
147
+ | `Graph#out_degree(item)` | Count of direct dependencies for an item |
126
148
  | `Graph#path(from, to)` | Shortest dependency path (BFS), or nil |
127
149
  | `Graph#subgraph(*items)` | Extract a new graph with specified nodes |
128
150
  | `Graph#roots` | Nodes with no dependencies |
@@ -131,6 +153,7 @@ graph.resolve # => [:a, :b, :c]
131
153
  | `Graph#reverse` | Return a new graph with all edges flipped |
132
154
  | `Graph#all_dependents_of(item)` | All transitive dependents of a node |
133
155
  | `Graph#independent?(a, b)` | Whether two nodes are mutually unreachable |
156
+ | `#to_dot(name:)` | Graphviz DOT representation |
134
157
 
135
158
  ## Development
136
159
 
@@ -146,6 +146,28 @@ module Philiprehberger
146
146
  end
147
147
  end
148
148
 
149
+ # Number of direct dependencies for an item.
150
+ #
151
+ # Returns 0 for unknown items, matching the defensive behavior of
152
+ # {#dependencies_of}.
153
+ #
154
+ # @param item [Object] the item to query
155
+ # @return [Integer] count of direct dependencies
156
+ def out_degree(item)
157
+ (@nodes[item] || []).size
158
+ end
159
+
160
+ # Number of direct dependents for an item (how many items depend on it).
161
+ #
162
+ # Returns 0 for unknown items, matching the defensive behavior of
163
+ # {#dependents_of}.
164
+ #
165
+ # @param item [Object] the item to query
166
+ # @return [Integer] count of direct dependents
167
+ def in_degree(item)
168
+ @nodes.count { |_node, deps| deps.include?(item) }
169
+ end
170
+
149
171
  # Find shortest dependency path between two nodes using BFS
150
172
  #
151
173
  # @param from [Object] the starting node
@@ -298,18 +320,33 @@ module Philiprehberger
298
320
  !all_dependencies_of(node_b).include?(node_a)
299
321
  end
300
322
 
301
- # Export the graph in Graphviz DOT format
323
+ # Export the graph in Graphviz DOT format.
302
324
  #
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)};" }
325
+ # Nodes are emitted in alphabetical order (cast to string for sort key),
326
+ # and edges within a node are sorted alphabetically for deterministic
327
+ # output. Nodes that participate in at least one edge (incoming or
328
+ # outgoing) are emitted implicitly via the edge lines; truly isolated
329
+ # nodes are declared explicitly so they still appear in the rendered
330
+ # graph. Works on graphs containing cycles.
331
+ #
332
+ # @param name [String] the digraph name used in the `digraph` header
333
+ # @return [String] Graphviz DOT source, terminated by a newline
334
+ def to_dot(name: 'dependencies')
335
+ output = "digraph #{name} {\n"
336
+ depended_on = @nodes.each_with_object({}) do |(_node, deps), acc|
337
+ deps.each { |dep| acc[dep] = true }
338
+ end
339
+ sorted_nodes = @nodes.keys.sort_by(&:to_s)
340
+ sorted_nodes.each do |node|
341
+ deps = (@nodes[node] || []).sort_by(&:to_s)
342
+ if deps.empty?
343
+ output << " #{dot_quote(node)};\n" unless depended_on[node]
344
+ else
345
+ deps.each { |dep| output << " #{dot_quote(node)} -> #{dot_quote(dep)};\n" }
346
+ end
310
347
  end
311
- lines << '}'
312
- lines.join("\n")
348
+ output << "}\n"
349
+ output
313
350
  end
314
351
 
315
352
  # Calculate maximum dependency depth for a node (longest path from any root to this node)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module DependencyGraph
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.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.3.0
4
+ version: 0.5.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-09 00:00:00.000000000 Z
11
+ date: 2026-05-02 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