kinship 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f7d5ced07f37ed098f40ef418d6964cbc6b6c91655365720b14150eb80435242
4
+ data.tar.gz: af7ec88de5e10093501c056b37e6ba1f3e5b7ed861cad54efba00a3c525682ee
5
+ SHA512:
6
+ metadata.gz: 57573a41df7c60c1c2b83bf0522a6fa21d496c6246c5039983ad6b5ae25d20f801a1eb7e229b5f1de1ffb35f4b9b319e82ded6fd5b801c6613fffd2712573f16
7
+ data.tar.gz: a33cde8b13290fc2b5a518b1df9160684e839ca31fb636f96fe73dafc310a30b2b0f99e30acccf8fd1541381b040691f3fc876ae301498a0b7db458ddacbdfca
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-01-07
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Kurt Tamulonis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # Kinship
2
+
3
+ **Schema‑inferred relationship graphs for Ruby & Rails**
4
+
5
+ Kinship discovers relationships between your models **automatically** by inspecting columns like `user_id`, `project_id`, etc. From that single fact, it builds a full graph of your domain — parents, children, families, paths — without `has_many`, `belongs_to`, or configuration.
6
+
7
+ It exists to solve a very real problem:
8
+
9
+ > *Rails knows your schema, but not your graph.*
10
+
11
+ Kinship makes the graph explicit.
12
+
13
+ ---
14
+
15
+ ## Why Kinship exists
16
+
17
+ Most Rails apps:
18
+
19
+ * Define associations manually
20
+ * Forget to preload
21
+ * Accidentally ship N+1 queries
22
+ * Duplicate schema knowledge in code
23
+
24
+ Example of a **very common bad query**:
25
+
26
+ ```ruby
27
+ User.all.each do |user|
28
+ user.projects.each do |project|
29
+ project.tasks.each do |task|
30
+ task.comments.each do |comment|
31
+ comment.reactions.each do |reaction|
32
+ reaction.kind
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ ```
39
+
40
+ Rails *allows* this. It fails silently. Performance collapses.
41
+
42
+ Kinship’s philosophy:
43
+
44
+ > **If the database already encodes relationships, your application should be able to reason about them automatically.**
45
+
46
+ ---
47
+
48
+ ## What Kinship gives you
49
+
50
+ From nothing more than foreign keys, Kinship builds:
51
+
52
+ * `parents(model)` – one layer up
53
+ * `children(model)` – one layer down
54
+ * `families` – connected components
55
+ * `path(from, to)` – shortest relationship path
56
+ * `to_dot` – visual graph output
57
+
58
+ No macros. No DSL. No annotations.
59
+
60
+ ---
61
+
62
+ ## Installation
63
+
64
+ ### Ruby / Rails
65
+
66
+ ```bash
67
+ gem install kinship
68
+ ```
69
+
70
+ or in your Gemfile:
71
+
72
+ ```ruby
73
+ gem "kinship"
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Rails usage (recommended setup)
79
+
80
+ Create an initializer:
81
+
82
+ ```ruby
83
+ # config/initializers/kinship.rb
84
+ Rails.application.config.after_initialize do
85
+ Rails.application.eager_load!
86
+
87
+ KINSHIP = Kinship.build(
88
+ models: ApplicationRecord.descendants,
89
+ attribute_provider: ->(model) { model.column_names }
90
+ )
91
+ end
92
+ ```
93
+
94
+ That’s it.
95
+
96
+ Kinship now understands your entire domain graph.
97
+
98
+ ---
99
+
100
+ ## Example domain
101
+
102
+ Given these tables:
103
+
104
+ * `users`
105
+ * `projects` (`user_id`)
106
+ * `tasks` (`project_id`)
107
+ * `comments` (`task_id`)
108
+ * `reactions` (`comment_id`)
109
+
110
+ No associations defined.
111
+
112
+ ---
113
+
114
+ ## Core API
115
+
116
+ ### Parents
117
+
118
+ ```ruby
119
+ KINSHIP.parents(Project)
120
+ # => { user: User }
121
+ ```
122
+
123
+ ### Children
124
+
125
+ ```ruby
126
+ KINSHIP.children(Project)
127
+ # => { tasks: Task }
128
+ ```
129
+
130
+ ### Families
131
+
132
+ ```ruby
133
+ KINSHIP.families
134
+ # => [[User, Project, Task, Comment, Reaction]]
135
+ ```
136
+
137
+ ### Path discovery
138
+
139
+ ```ruby
140
+ KINSHIP.path(User, Reaction)
141
+ # => [User, Project, Task, Comment, Reaction]
142
+ ```
143
+
144
+ This is **the missing abstraction** Rails never gave you.
145
+
146
+ ---
147
+
148
+ ## Graph visualization
149
+
150
+ ```ruby
151
+ puts KINSHIP.to_dot
152
+ ```
153
+
154
+ Output:
155
+
156
+ ```dot
157
+ digraph Kinship {
158
+ "User";
159
+ "Project";
160
+ "Task";
161
+ "Comment";
162
+ "Reaction";
163
+ "User" -> "Project";
164
+ "Project" -> "Task";
165
+ "Task" -> "Comment";
166
+ "Comment" -> "Reaction";
167
+ }
168
+ ```
169
+
170
+ You can render this with Graphviz to **see your data model**.
171
+
172
+ ---
173
+
174
+ ## What Kinship intentionally does *not* do (yet)
175
+
176
+ * Execute SQL
177
+ * Replace ActiveRecord
178
+ * Magically rewrite queries
179
+
180
+ Instead, it provides the **missing layer of understanding** required to:
181
+
182
+ * detect N+1 queries
183
+ * generate preload plans
184
+ * reason about deep filters
185
+ * visualize data flow
186
+
187
+ Kinship is the **map**, avoiding wrong turns.
188
+
189
+ ---
190
+
191
+ ## Framework integrations
192
+
193
+ Kinship is framework‑agnostic.
194
+
195
+ It can be embedded into:
196
+
197
+ * Rails (today)
198
+ * Jetski (in progress)
199
+ * Custom ORMs
200
+ * Data tooling
201
+
202
+ Once embedded, *all downstream users benefit* automatically.
203
+
204
+ ---
205
+
206
+ ## Roadmap (vision)
207
+
208
+ * Declarative query intent
209
+ * Automatic preload inference
210
+ * N+1 detection hooks
211
+ * Query planning helpers
212
+ * Console inspection tools
213
+
214
+ Kinship is designed to cover **the 80% of relationship problems that cause 95% of performance bugs**.
215
+
216
+ ---
217
+
218
+ ## Philosophy
219
+
220
+ > Databases already encode relationships.
221
+ >
222
+ > Kinship makes them visible, navigable, and reason‑able.
223
+
224
+ ---
225
+
226
+ ## License
227
+
228
+ MIT
229
+
230
+ ---
231
+
232
+ **Built by Kurt Tamulonis**
233
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,145 @@
1
+ module Kinship
2
+ class Graph
3
+ attr_reader :models, :parents, :children
4
+
5
+ def initialize(models:, attribute_provider:)
6
+ @models = models
7
+ @attribute_provider = attribute_provider
8
+
9
+ @parents = Hash.new { |h, k| h[k] = {} }
10
+ @children = Hash.new { |h, k| h[k] = {} }
11
+
12
+ build!
13
+ end
14
+
15
+ def parents(model)
16
+ @parents[model] || {}
17
+ end
18
+
19
+ def children(model)
20
+ @children[model] || {}
21
+ end
22
+
23
+ def families
24
+ visited = {}
25
+ groups = []
26
+
27
+ @models.each do |model|
28
+ next if visited[model]
29
+ groups << bfs(model, visited)
30
+ end
31
+
32
+ groups
33
+ end
34
+
35
+ def path(from, to)
36
+ return [from] if from == to
37
+
38
+ queue = [[from]]
39
+ seen = { from => true }
40
+
41
+ until queue.empty?
42
+ current = queue.shift
43
+ node = current.last
44
+
45
+ neighbors(node).each do |neighbor|
46
+ next if seen[neighbor]
47
+
48
+ path = current + [neighbor]
49
+ return path if neighbor == to
50
+
51
+ seen[neighbor] = true
52
+ queue << path
53
+ end
54
+ end
55
+
56
+ nil
57
+ end
58
+
59
+ def to_dot
60
+ lines = []
61
+ lines << "digraph Kinship {"
62
+
63
+ @models.each do |model|
64
+ lines << " \"#{model.name}\";"
65
+ end
66
+
67
+ @parents.each do |child, parents|
68
+ parents.each_value do |parent|
69
+ lines << " \"#{parent.name}\" -> \"#{child.name}\";"
70
+ end
71
+ end
72
+
73
+ lines << "}"
74
+ lines.join("\n")
75
+ end
76
+
77
+ private
78
+
79
+ def build!
80
+ lookup = @models.to_h { |m| [simple_name(m), m] }
81
+
82
+ @models.each do |child|
83
+ attributes_for(child).each do |attr|
84
+ next unless attr.end_with?("_id")
85
+
86
+ parent_key = attr.sub(/_id$/, "")
87
+ parent = lookup[parent_key]
88
+ next unless parent
89
+
90
+ @parents[child][parent_key.to_sym] = parent
91
+ @children[parent][pluralize(child).to_sym] = child
92
+ end
93
+ end
94
+ end
95
+
96
+ def attributes_for(model)
97
+ attrs =
98
+ if @attribute_provider.respond_to?(:call)
99
+ @attribute_provider.call(model)
100
+ else
101
+ @attribute_provider[model]
102
+ end
103
+
104
+ raise ArgumentError, "attribute_provider must return Array<String>" unless attrs.is_a?(Array)
105
+ attrs
106
+ end
107
+
108
+ def neighbors(model)
109
+ (parents(model).values + children(model).values).uniq
110
+ end
111
+
112
+ def bfs(start, visited)
113
+ queue = [start]
114
+ group = []
115
+ visited[start] = true
116
+
117
+ until queue.empty?
118
+ node = queue.shift
119
+ group << node
120
+
121
+ neighbors(node).each do |n|
122
+ next if visited[n]
123
+ visited[n] = true
124
+ queue << n
125
+ end
126
+ end
127
+
128
+ group
129
+ end
130
+
131
+ def simple_name(model)
132
+ model.name
133
+ .split("::")
134
+ .last
135
+ .gsub(/([a-z0-9])([A-Z])/, '\1_\2')
136
+ .downcase
137
+ end
138
+
139
+ def pluralize(model)
140
+ n = simple_name(model)
141
+ n.end_with?("s") ? n : "#{n}s"
142
+ end
143
+ end
144
+ end
145
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kinship
4
+ VERSION = "0.0.1"
5
+ end
data/lib/kinship.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "kinship/version"
4
+ require_relative "kinship/graph"
5
+
6
+ module Kinship
7
+ def self.build(models:, attribute_provider:)
8
+ Graph.new(models: models, attribute_provider: attribute_provider)
9
+ end
10
+ end
11
+
data/sig/kinship.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Kinship
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kinship
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - hackliteracy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-01-09 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Kinship is a schema-inferred relationship graph for Ruby applications.
15
+ It automatically discovers parent/child relationships between models
16
+ by inspecting attributes (e.g. user_id, post_id) and builds a complete
17
+ in-memory graph with zero configuration.
18
+
19
+ Kinship enables deep relationship traversal, automatic join planning,
20
+ and eliminates common N+1 query patterns without requiring has_many or
21
+ belongs_to declarations. It is framework-agnostic and works with Rails,
22
+ Jetski, and custom ORMs.
23
+ email:
24
+ - hackliteracy@gmail.com
25
+ executables: []
26
+ extensions: []
27
+ extra_rdoc_files: []
28
+ files:
29
+ - CHANGELOG.md
30
+ - LICENSE.txt
31
+ - README.md
32
+ - Rakefile
33
+ - lib/kinship.rb
34
+ - lib/kinship/graph.rb
35
+ - lib/kinship/version.rb
36
+ - sig/kinship.rbs
37
+ homepage: https://github.com/ktamulonis/kinship
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ homepage_uri: https://github.com/ktamulonis/kinship
42
+ source_code_uri: https://github.com/ktamulonis/kinship
43
+ changelog_uri: https://github.com/ktamulonis/kinship/blob/main/CHANGELOG.md
44
+ rubygems_mfa_required: 'true'
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.5.22
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Schema-inferred relationship graphs for Ruby applications
64
+ test_files: []