philiprehberger-dependency_graph 0.1.7 → 0.2.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: e7adfe2ea19f84682a7098e4b2721e0da82302a2820b795910d7eed48b8ab2ba
4
- data.tar.gz: d7440b81ffe5f4a9a7b84ef2c70bd39b6a6f31115213df1dcef51ebce7c05669
3
+ metadata.gz: a9091598b68332edaee82a0a8a55bd23d3d00278c78466b88ac86b0e25651390
4
+ data.tar.gz: '0889cc1cdd1d277dc63b4c08228b6e091bacccbcb3670833d5015c4aec02cba9'
5
5
  SHA512:
6
- metadata.gz: 6372378bf705c2d620a0971b47a5fd52c3664664ec11388f983b318cc3f5548557fe5395a744d9b440deb85cb1144425009ec63211cfc525ea93c9e38ce39f78
7
- data.tar.gz: c038fa5661134768db05a5833ae52d912cb19e3bf3d53ffc998a665375d40f6dca1bb6ccb9fa0cbd8f08a00c61c0ce15163f0c627548c99cec9b8e4dd889900a
6
+ metadata.gz: c9ecc7105f38fe9d6dcb1a63e2456d91883f26bb114b2097ea2bada656f80dfd77065c5143dadf579a9f29b07b069fc3c2fb98d39ddca7be71c65c1537275d9c
7
+ data.tar.gz: 37f0dc97583b44d80f9c046dd68375fc2749ac14f360905b0de8c64e3f630645814471d012f053434b91318ec9ab77ea9f6af0b3cb839846ed760bbc31d1ef1b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-04-03
11
+
12
+ ### Added
13
+ - `Graph#dependencies_of(item)` — returns direct dependencies of a node
14
+ - `Graph#all_dependencies_of(item)` — returns transitive closure of all dependencies
15
+ - `Graph#dependents_of(item)` — reverse lookup of items that depend on a node
16
+ - `Graph#path(from, to)` — BFS shortest dependency path between two nodes
17
+ - `Graph#subgraph(*items)` — extract a new graph containing only specified nodes and their edges
18
+ - `Graph#roots` — nodes with no dependencies
19
+ - `Graph#leaves` — nodes with no dependents
20
+ - `Graph#depth(item)` — maximum dependency depth for a node
21
+
22
+ ## [0.1.8] - 2026-03-31
23
+
24
+ ### Added
25
+ - Add GitHub issue templates, dependabot config, and PR template
26
+
10
27
  ## [0.1.7] - 2026-03-31
11
28
 
12
29
  ### Changed
data/README.md CHANGED
@@ -65,6 +65,43 @@ graph.cycle? # => true
65
65
  graph.cycles # => [[:a, :b, :a]]
66
66
  ```
67
67
 
68
+ ### Dependency Queries
69
+
70
+ ```ruby
71
+ graph = Philiprehberger::DependencyGraph.new
72
+ graph.add(:a)
73
+ graph.add(:b, depends_on: [:a])
74
+ graph.add(:c, depends_on: [:b])
75
+ graph.add(:d, depends_on: [:b, :c])
76
+
77
+ graph.dependencies_of(:d) # => [:b, :c]
78
+ graph.all_dependencies_of(:d) # => [:b, :c, :a]
79
+ graph.dependents_of(:b) # => [:c, :d]
80
+ ```
81
+
82
+ ### Path Finding
83
+
84
+ ```ruby
85
+ graph.path(:d, :a) # => [:d, :b, :a]
86
+ graph.path(:a, :d) # => nil (no path in that direction)
87
+ ```
88
+
89
+ ### Subgraph Extraction
90
+
91
+ ```ruby
92
+ sub = graph.subgraph(:a, :b, :c)
93
+ sub.resolve # => [:a, :b, :c]
94
+ # Edges to nodes outside the subgraph are excluded
95
+ ```
96
+
97
+ ### Roots, Leaves, and Depth
98
+
99
+ ```ruby
100
+ graph.roots # => [:a] (no dependencies)
101
+ graph.leaves # => [:d] (nothing depends on it)
102
+ graph.depth(:d) # => 2 (longest path from a root)
103
+ ```
104
+
68
105
  ### Chaining
69
106
 
70
107
  ```ruby
@@ -83,6 +120,14 @@ graph.resolve # => [:a, :b, :c]
83
120
  | `Graph#parallel_batches` | Group into parallel execution batches |
84
121
  | `Graph#cycle?` | Check if the graph contains cycles |
85
122
  | `Graph#cycles` | List all detected cycles |
123
+ | `Graph#dependencies_of(item)` | Direct dependencies of an item |
124
+ | `Graph#all_dependencies_of(item)` | All transitive dependencies |
125
+ | `Graph#dependents_of(item)` | Items that directly depend on an item |
126
+ | `Graph#path(from, to)` | Shortest dependency path (BFS), or nil |
127
+ | `Graph#subgraph(*items)` | Extract a new graph with specified nodes |
128
+ | `Graph#roots` | Nodes with no dependencies |
129
+ | `Graph#leaves` | Nodes with no dependents |
130
+ | `Graph#depth(item)` | Maximum dependency depth of a node |
86
131
 
87
132
  ## Development
88
133
 
@@ -105,8 +105,145 @@ module Philiprehberger
105
105
  found_cycles
106
106
  end
107
107
 
108
+ # Return direct dependencies of an item
109
+ #
110
+ # @param item [Object] the item to query
111
+ # @return [Array] direct dependencies, or empty array if item is unknown
112
+ def dependencies_of(item)
113
+ (@nodes[item] || []).dup
114
+ end
115
+
116
+ # Return all transitive dependencies of an item (direct + indirect)
117
+ #
118
+ # @param item [Object] the item to query
119
+ # @return [Array] all dependencies in no particular order
120
+ def all_dependencies_of(item)
121
+ return [] unless @nodes.key?(item)
122
+
123
+ visited = {}
124
+ queue = @nodes[item].dup
125
+ result = []
126
+
127
+ until queue.empty?
128
+ dep = queue.shift
129
+ next if visited[dep]
130
+
131
+ visited[dep] = true
132
+ result << dep
133
+ queue.concat(@nodes[dep] || [])
134
+ end
135
+
136
+ result
137
+ end
138
+
139
+ # Return items that directly depend on the given item (reverse lookup)
140
+ #
141
+ # @param item [Object] the item to query
142
+ # @return [Array] direct dependents
143
+ def dependents_of(item)
144
+ @nodes.each_with_object([]) do |(node, deps), acc|
145
+ acc << node if deps.include?(item)
146
+ end
147
+ end
148
+
149
+ # Find shortest dependency path between two nodes using BFS
150
+ #
151
+ # @param from [Object] the starting node
152
+ # @param to [Object] the target node
153
+ # @return [Array, nil] array of nodes forming the path, or nil if no path exists
154
+ def path(from, to)
155
+ return nil unless @nodes.key?(from) && @nodes.key?(to)
156
+ return [from] if from == to
157
+
158
+ visited = { from => nil }
159
+ queue = [from]
160
+
161
+ until queue.empty?
162
+ current = queue.shift
163
+ (@nodes[current] || []).each do |dep|
164
+ next if visited.key?(dep)
165
+
166
+ visited[dep] = current
167
+ if dep == to
168
+ return build_path(visited, from, to)
169
+ end
170
+
171
+ queue << dep
172
+ end
173
+ end
174
+
175
+ nil
176
+ end
177
+
178
+ # Extract a subgraph containing only the specified nodes and edges between them
179
+ #
180
+ # @param items [Array<Object>] nodes to include
181
+ # @return [Graph] a new graph with only the specified nodes
182
+ def subgraph(*items)
183
+ item_set = items.flatten.to_h { |i| [i, true] }
184
+ new_graph = self.class.new
185
+
186
+ items.flatten.each do |item|
187
+ next unless @nodes.key?(item)
188
+
189
+ matching_deps = (@nodes[item] || []).select { |dep| item_set[dep] }
190
+ new_graph.add(item, depends_on: matching_deps)
191
+ end
192
+
193
+ new_graph
194
+ end
195
+
196
+ # Return nodes that have no dependencies
197
+ #
198
+ # @return [Array] root nodes
199
+ def roots
200
+ @nodes.select { |_node, deps| deps.empty? }.keys
201
+ end
202
+
203
+ # Return nodes with no dependents (no other node depends on them)
204
+ #
205
+ # @return [Array] leaf nodes
206
+ def leaves
207
+ depended_on = @nodes.values.flatten.uniq
208
+ @nodes.keys.reject { |node| depended_on.include?(node) }
209
+ end
210
+
211
+ # Calculate maximum dependency depth for a node (longest path from any root to this node)
212
+ #
213
+ # @param item [Object] the item to query
214
+ # @return [Integer] the depth, or 0 if the item is a root or unknown
215
+ def depth(item)
216
+ return 0 unless @nodes.key?(item)
217
+
218
+ memo = {}
219
+ compute_depth(item, memo)
220
+ end
221
+
108
222
  private
109
223
 
224
+ def build_path(visited, from, to)
225
+ path = [to]
226
+ current = to
227
+ while current != from
228
+ current = visited[current]
229
+ path.unshift(current)
230
+ end
231
+ path
232
+ end
233
+
234
+ def compute_depth(item, memo)
235
+ return memo[item] if memo.key?(item)
236
+
237
+ deps = @nodes[item] || []
238
+ memo[item] = if deps.empty?
239
+ 0
240
+ else
241
+ deps.map { |dep| compute_depth(dep, memo) }.max + 1
242
+ end
243
+
244
+ memo[item]
245
+ end
246
+
110
247
  def detect_cycles(node, visited, stack, path, found_cycles)
111
248
  visited[node] = true
112
249
  stack[node] = true
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module DependencyGraph
5
- VERSION = '0.1.7'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,17 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-dependency_graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.2.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-03-31 00:00:00.000000000 Z
11
+ date: 2026-04-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Build and resolve dependency graphs using topological sort, detect cycles,
14
- and generate parallel execution batches for concurrent task scheduling.
14
+ generate parallel execution batches, query dependencies and dependents, find shortest
15
+ paths, and extract subgraphs.
15
16
  email:
16
17
  - me@philiprehberger.com
17
18
  executables: []
@@ -24,11 +25,11 @@ files:
24
25
  - lib/philiprehberger/dependency_graph.rb
25
26
  - lib/philiprehberger/dependency_graph/graph.rb
26
27
  - lib/philiprehberger/dependency_graph/version.rb
27
- homepage: https://github.com/philiprehberger/rb-dependency-graph
28
+ homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-dependency_graph
28
29
  licenses:
29
30
  - MIT
30
31
  metadata:
31
- homepage_uri: https://github.com/philiprehberger/rb-dependency-graph
32
+ homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-dependency_graph
32
33
  source_code_uri: https://github.com/philiprehberger/rb-dependency-graph
33
34
  changelog_uri: https://github.com/philiprehberger/rb-dependency-graph/blob/main/CHANGELOG.md
34
35
  bug_tracker_uri: https://github.com/philiprehberger/rb-dependency-graph/issues