quo 1.0.0.beta2 → 2.0.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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +17 -0
  3. data/.devcontainer/compose.yml +10 -0
  4. data/.devcontainer/devcontainer.json +12 -0
  5. data/Appraisals +4 -12
  6. data/CHANGELOG.md +112 -1
  7. data/CLAUDE.md +19 -0
  8. data/Gemfile +7 -1
  9. data/LICENSE.txt +1 -1
  10. data/README.md +496 -203
  11. data/Rakefile +66 -6
  12. data/UPGRADING.md +216 -0
  13. data/badges/coverage_badge_total.svg +35 -0
  14. data/badges/rubycritic_badge_score.svg +35 -0
  15. data/claude-skill/README.md +100 -0
  16. data/claude-skill/SKILL.md +442 -0
  17. data/claude-skill/references/API_REFERENCE.md +462 -0
  18. data/claude-skill/references/COMPOSITION.md +396 -0
  19. data/claude-skill/references/PAGINATION.md +396 -0
  20. data/claude-skill/references/QUERY_TYPES.md +297 -0
  21. data/claude-skill/references/TRANSFORMERS.md +282 -0
  22. data/context/01-core-architecture.md +247 -0
  23. data/context/02-query-types-implementation.md +355 -0
  24. data/context/03-composition-transformation.md +441 -0
  25. data/context/04-pagination-results.md +485 -0
  26. data/context/05-testing-configuration.md +491 -0
  27. data/context/06-advanced-patterns-examples.md +153 -0
  28. data/gemfiles/rails_8.0.gemfile +10 -5
  29. data/gemfiles/rails_8.1.gemfile +20 -0
  30. data/lib/generators/quo/install/USAGE +21 -0
  31. data/lib/generators/quo/install/install_generator.rb +63 -0
  32. data/lib/quo/collection_backed_query.rb +21 -15
  33. data/lib/quo/collection_results.rb +1 -0
  34. data/lib/quo/composed_collection_backed_query.rb +42 -0
  35. data/lib/quo/composed_instance.rb +144 -0
  36. data/lib/quo/composed_query.rb +43 -178
  37. data/lib/quo/composed_relation_backed_query.rb +42 -0
  38. data/lib/quo/composing/base_strategy.rb +22 -0
  39. data/lib/quo/composing/class_strategy.rb +86 -0
  40. data/lib/quo/composing/class_strategy_registry.rb +31 -0
  41. data/lib/quo/composing/query_classes_strategy.rb +38 -0
  42. data/lib/quo/composing.rb +81 -0
  43. data/lib/quo/engine.rb +1 -0
  44. data/lib/quo/minitest/helpers.rb +14 -24
  45. data/lib/quo/preloadable.rb +1 -0
  46. data/lib/quo/query.rb +22 -5
  47. data/lib/quo/relation_backed_query.rb +24 -18
  48. data/lib/quo/relation_backed_query_specification.rb +44 -25
  49. data/lib/quo/relation_results.rb +1 -0
  50. data/lib/quo/results.rb +31 -2
  51. data/lib/quo/rspec/helpers.rb +15 -26
  52. data/lib/quo/testing/collection_backed_fake.rb +1 -0
  53. data/lib/quo/testing/fake_helpers.rb +30 -0
  54. data/lib/quo/testing/relation_backed_fake.rb +1 -0
  55. data/lib/quo/version.rb +1 -1
  56. data/lib/quo/wrapped_collection_backed_query.rb +21 -0
  57. data/lib/quo/wrapped_relation_backed_query.rb +21 -0
  58. data/lib/quo.rb +8 -0
  59. data/quo.png +0 -0
  60. data/sig/generated/quo/collection_backed_query.rbs +10 -4
  61. data/sig/generated/quo/collection_results.rbs +1 -0
  62. data/sig/generated/quo/composed_collection_backed_query.rbs +25 -0
  63. data/sig/generated/quo/composed_instance.rbs +61 -0
  64. data/sig/generated/quo/composed_query.rbs +23 -56
  65. data/sig/generated/quo/composed_relation_backed_query.rbs +25 -0
  66. data/sig/generated/quo/composing/base_strategy.rbs +16 -0
  67. data/sig/generated/quo/composing/class_strategy.rbs +38 -0
  68. data/sig/generated/quo/composing/class_strategy_registry.rbs +16 -0
  69. data/sig/generated/quo/composing/query_classes_strategy.rbs +24 -0
  70. data/sig/generated/quo/composing.rbs +40 -0
  71. data/sig/generated/quo/engine.rbs +1 -0
  72. data/sig/generated/quo/minitest/helpers.rbs +12 -0
  73. data/sig/generated/quo/preloadable.rbs +1 -0
  74. data/sig/generated/quo/query.rbs +15 -4
  75. data/sig/generated/quo/relation_backed_query.rbs +15 -5
  76. data/sig/generated/quo/relation_backed_query_specification.rbs +47 -39
  77. data/sig/generated/quo/relation_results.rbs +1 -0
  78. data/sig/generated/quo/results.rbs +11 -0
  79. data/sig/generated/quo/rspec/helpers.rbs +12 -0
  80. data/sig/generated/quo/testing/collection_backed_fake.rbs +1 -0
  81. data/sig/generated/quo/testing/fake_helpers.rbs +14 -0
  82. data/sig/generated/quo/testing/relation_backed_fake.rbs +1 -0
  83. data/sig/generated/quo/wrapped_collection_backed_query.rbs +13 -0
  84. data/sig/generated/quo/wrapped_relation_backed_query.rbs +13 -0
  85. data/sig/generated/quo.rbs +1 -0
  86. data/website/.gitignore +6 -0
  87. data/website/.nojekyll +0 -0
  88. data/website/404.html +26 -0
  89. data/website/Gemfile +24 -0
  90. data/website/_config.yml +50 -0
  91. data/website/_data/navigation.yml +8 -0
  92. data/website/_data/sidebar.yml +2 -0
  93. data/website/_data/social_links.yml +3 -0
  94. data/website/_docs/api.md +261 -0
  95. data/website/_docs/get-started.md +289 -0
  96. data/website/assets/quo.png +0 -0
  97. data/website/index.md +141 -0
  98. metadata +70 -13
  99. data/gemfiles/rails_7.0.gemfile +0 -15
  100. data/gemfiles/rails_7.1.gemfile +0 -15
  101. data/gemfiles/rails_7.2.gemfile +0 -15
data/Rakefile CHANGED
@@ -1,14 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "rake/testtask"
5
-
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/*_test.rb"]
4
+ desc "Run tests"
5
+ task :test do
6
+ sh "bin/test"
10
7
  end
11
8
 
12
9
  require "standard/rake"
13
10
 
14
11
  task default: %i[test standard]
12
+
13
+ # Add RubyCritic task with badge generation
14
+ begin
15
+ require "rubycritic_small_badge"
16
+ require "rubycritic/rake_task"
17
+
18
+ RubyCriticSmallBadge.configure do |config|
19
+ config.minimum_score = 90
20
+ end
21
+
22
+ RubyCritic::RakeTask.new do |task|
23
+ task.paths = FileList["lib/**/*.rb"]
24
+
25
+ task.options = %(--custom-format RubyCriticSmallBadge::Report
26
+ --minimum-score #{RubyCriticSmallBadge.config.minimum_score}
27
+ --coverage-path coverage/.resultset.json
28
+ --no-browser)
29
+ end
30
+
31
+ desc "Run tests with coverage and then RubyCritic"
32
+ task rubycritic_with_coverage: [:coverage, :rubycritic]
33
+ rescue LoadError
34
+ desc "Run RubyCritic (not available)"
35
+ task :rubycritic do
36
+ puts "RubyCritic is not available"
37
+ end
38
+ end
39
+
40
+ desc "Run code coverage"
41
+ task :coverage do
42
+ ENV["COVERAGE"] = "1"
43
+ Rake::Task["test"].invoke
44
+ end
45
+
46
+ namespace :website do
47
+ desc "Build the documentation website"
48
+ task :build do
49
+ Dir.chdir("website") do
50
+ puts "Building documentation website..."
51
+ system "bundle install"
52
+ system "bundle exec jekyll build"
53
+ puts "Website built in website/_site/"
54
+ end
55
+ end
56
+
57
+ desc "Serve the documentation website locally"
58
+ task :serve do
59
+ Dir.chdir("website") do
60
+ puts "Starting local documentation server..."
61
+ puts "View the website at http://localhost:4000/"
62
+ system "bundle install"
63
+ system "bundle exec jekyll serve"
64
+ end
65
+ end
66
+
67
+ desc "Clean the documentation website build"
68
+ task :clean do
69
+ Dir.chdir("website") do
70
+ puts "Cleaning website build..."
71
+ system "bundle exec jekyll clean"
72
+ end
73
+ end
74
+ end
data/UPGRADING.md ADDED
@@ -0,0 +1,216 @@
1
+ # Upgrading Quo
2
+
3
+ ## Upgrading from 1.x to 2.0
4
+
5
+ Quo 2.0 is a structural rework of query composition. Most code continues
6
+ to work unchanged. There are two intentional behavioural changes worth
7
+ knowing about before you bump, plus a new value-form API you'll want to
8
+ adopt for hot paths.
9
+
10
+ ### TL;DR
11
+
12
+ - Composition still uses `+` / `compose` / `merge`. Same API.
13
+ - **Class composition** (`SomeClass + OtherClass`) — unchanged. Returns a Class.
14
+ - **Instance composition** (`some_instance + other_instance`) — now returns a
15
+ concrete value (`Quo::ComposedRelationBackedQuery` or
16
+ `Quo::ComposedCollectionBackedQuery`), not an anonymous class. Allocates
17
+ no new classes per call.
18
+ - New: `Quo::RelationBackedQuery.from(relation)` and
19
+ `Quo::CollectionBackedQuery.from(enumerable)` — value-form constructors,
20
+ for use instead of `wrap(rel).new` on hot paths.
21
+ - 1.x's "prop fan-out from a synthesised parent class" is gone. Each
22
+ operand keeps its own constructor-time props. Two subtle consequences
23
+ documented below.
24
+ - ~2× faster instance composition, with **zero anonymous classes per call**.
25
+
26
+ ### Why
27
+
28
+ 1.x had a single composition path. Both `Q1 + Q2` (between classes) and
29
+ `q1 + q2` (between instances) went through the same machinery, which
30
+ allocated a fresh anonymous class on every call. For instances, the
31
+ class was then immediately instantiated once and discarded — pure
32
+ waste, paid per request.
33
+
34
+ 2.0 splits the two cases. Class composition keeps the class-allocation
35
+ machinery (it's the right shape for *defining a new type* once at
36
+ code-load time). Instance composition produces a value instead — a
37
+ small struct that holds the two operands and walks them at unwrap
38
+ time. No anonymous classes, no `prop` re-registration.
39
+
40
+ ### What's the same
41
+
42
+ - The `+` / `compose` / `merge` API at both class and instance levels.
43
+ - The leaf query class API: `prop`, `query`, `collection`, `transform`,
44
+ `wrap`, `with_specification`, etc.
45
+ - Pagination and transformer surfaces on every Quo::Query.
46
+ - Specifications (`order(...)`, `joins(...)`, `where(...)`, `distinct`,
47
+ etc.) on relation-backed queries, including when applied to a composed
48
+ instance.
49
+ - `Quo::ComposedQuery` is still the marker module used by class
50
+ composition. Existing `kind_of?(Quo::ComposedQuery)` checks on
51
+ class-composed results keep working.
52
+ - Composed and wrapped value-form instances are subclasses of the
53
+ configured base classes (`Quo.relation_backed_query_base_class`,
54
+ `Quo.collection_backed_query_base_class` — typically
55
+ `ApplicationRelationQuery` / `ApplicationCollectionQuery`). Anything
56
+ defined on those base classes is available on `.from`-constructed and
57
+ instance-composed values. The base classes are resolved at autoload
58
+ time, so configure `Quo.relation_backed_query_base_class = ...`
59
+ in an initializer (it runs before eager load / first reference in
60
+ Rails apps).
61
+
62
+ ### What's intentionally different
63
+
64
+ #### 1. Prop fan-out at instance composition is gone
65
+
66
+ In 1.x, `(Q1.new(score: 0.5) + Q2.new(score: 0.7))` produced a merged
67
+ class whose effective `score` was 0.7 (right won) and that value was
68
+ fanned to every child at unwrap time.
69
+
70
+ In 2.x, each operand keeps its own constructor-time props. The merged
71
+ relation is the AND of both operands' filters, evaluated independently:
72
+
73
+ ```ruby
74
+ # 1.x: filter = (score < 0.7 AND score < 0.7) -> score < 0.7
75
+ # 2.x: filter = (score < 0.5 AND score < 0.7) -> score < 0.5
76
+ ```
77
+
78
+ If you relied on 1.x's "right wins everywhere" behaviour, you now need
79
+ to construct the leaf with the value you actually want:
80
+
81
+ ```ruby
82
+ # Equivalent to 1.x's behaviour
83
+ Q1.new(score: 0.7) + Q2.new(score: 0.7)
84
+ ```
85
+
86
+ #### 2. Pagination inherits as a coupled (page, page_size) pair
87
+
88
+ In 1.x, `page` and `page_size` were fanned independently, so a composed
89
+ query could end up with `page: 2` from the left operand and
90
+ `page_size: 50` from the right, even though neither operand was
91
+ constructed with that pair.
92
+
93
+ In 2.x, pagination inherits *as a unit* from whichever operand is
94
+ paginated (i.e. has a non-nil `page`). Right wins if both are.
95
+ `page_size` alone doesn't make a query paginated, because every
96
+ Quo::Query has a default `page_size`.
97
+
98
+ If you previously relied on the cross-pollination behaviour, set
99
+ pagination explicitly on the composed instance:
100
+
101
+ ```ruby
102
+ (q1 + q2).copy(page: 1, page_size: 50).results
103
+ ```
104
+
105
+ #### 3. `wrap` is type-strict at definition time
106
+
107
+ `Quo::RelationBackedQuery.wrap(x)` now raises `ArgumentError` at call
108
+ time if `x` is not an `ActiveRecord::Relation` or a
109
+ `Quo::RelationBackedQuery` instance. Same for
110
+ `Quo::CollectionBackedQuery.wrap(x)` — `x` must be an `Enumerable` or a
111
+ `Quo::CollectionBackedQuery` instance. Previously these would silently
112
+ accept the wrong type and fail later at `.unwrap` / `.results` time,
113
+ often with a confusing error.
114
+
115
+ Block forms still defer the check to first call (the block body can
116
+ return anything until it's evaluated).
117
+
118
+ If you were relying on cross-type wrapping (e.g. handing an array to
119
+ `RelationBackedQuery.wrap`), use the matching constructor:
120
+
121
+ - in-memory collection → `Quo::CollectionBackedQuery.wrap(arr)` or
122
+ `Quo::CollectionBackedQuery.from(arr)`
123
+ - AR relation → `Quo::RelationBackedQuery.wrap(rel)` or
124
+ `Quo::RelationBackedQuery.from(rel)`
125
+
126
+ #### 4. Minimum Rails 8
127
+
128
+ Quo 2.x requires `activerecord >= 8.0` and `activesupport >= 8.0`. For
129
+ Rails 7.x, stay on Quo 1.x.
130
+
131
+ ### What's new
132
+
133
+ #### `.from` — value-form constructors
134
+
135
+ For wrapping a bare relation or enumerable as a Quo::Query at a call
136
+ site, use `.from` instead of `wrap(...).new`:
137
+
138
+ ```ruby
139
+ # 1.x — still works, but allocates a new class per call.
140
+ Quo::RelationBackedQuery.wrap(Comment.where(read: false)).new.results
141
+
142
+ # 2.x — value form, no Class.new per call.
143
+ Quo::RelationBackedQuery.from(Comment.where(read: false)).results
144
+
145
+ # Same for collections.
146
+ Quo::CollectionBackedQuery.from([1, 2, 3]).results
147
+ ```
148
+
149
+ `.from` returns an instance of `Quo::WrappedRelationBackedQuery` /
150
+ `Quo::WrappedCollectionBackedQuery`. They behave like any other
151
+ Quo::Query — composable, paginatable, transformable.
152
+
153
+ Keep using `wrap` when you want a *class* (constant assignment, block
154
+ form with typed props):
155
+
156
+ ```ruby
157
+ # Still right in 2.x — type-defining use.
158
+ RecentComments = Quo::RelationBackedQuery.wrap(props: {since: Time}) do
159
+ Comment.where("created_at > ?", since)
160
+ end
161
+ RecentComments.new(since: 1.day.ago).results
162
+ ```
163
+
164
+ The skill bundled with Quo (`bin/rails generate quo:install`) covers
165
+ the class-vs-instance composition cut and the new `.from` API.
166
+
167
+ ### Performance
168
+
169
+ Measured on a representative 3-leaf instance composition (5000
170
+ iterations of `(L.new + R.new + T.new).unwrap.to_sql`):
171
+
172
+ | metric | 1.0.0 | 2.0.0 | Δ |
173
+ |---------------------|-------------|-------------|-------------|
174
+ | wall | 3805 ms | 1740 ms | 2.2× faster |
175
+ | GC time | 238 ms | 61 ms | 4× less |
176
+ | total allocations | 8,000,091 | 4,755,036 | -41% |
177
+ | T_CLASS allocations | 753 | **0** | -100% |
178
+
179
+ The "zero T_CLASS per call" is the structural win — instance
180
+ composition no longer goes through `Class.new`.
181
+
182
+ ### Migrating
183
+
184
+ For most apps, just bumping the gem version and running tests is
185
+ sufficient. If tests fail, they'll fall into one of these categories:
186
+
187
+ 1. **Test asserted 1.x's prop-fan-out "right wins at unwrap time".**
188
+ Update the test to reflect the new behaviour (filter is the AND of
189
+ each operand's own filter) or restructure your call site to
190
+ construct each leaf with the values you want.
191
+
192
+ 2. **Test asserted the cross-pollinated pagination behaviour.** Set
193
+ pagination explicitly on the composed instance, not at the leaf
194
+ level.
195
+
196
+ 3. **Test calls `composed.copy(some_user_prop:)` and expects a single
197
+ prop value to be observed.** v2's copy applies the new value to
198
+ every operand that declares the prop (right-wins precedence on
199
+ reads when both have it). Behaviour matches if you wanted "this
200
+ value, everywhere". If you wanted "this value on one side only",
201
+ construct the new operand explicitly: `q.copy(left: q.left.copy(prop: x))`.
202
+
203
+ ### Performance opportunities
204
+
205
+ After upgrading, scan for these patterns and consider rewriting:
206
+
207
+ - `Quo::RelationBackedQuery.wrap(some_relation).new` → use
208
+ `Quo::RelationBackedQuery.from(some_relation)`.
209
+ - `(Q1 + Q2).new(prop: value)` at a call site → if `Q1` and `Q2` are
210
+ classes, this still allocates a new class per request. Either hoist
211
+ the composition to a constant (`MyType = Q1 + Q2`) and instantiate
212
+ `MyType.new(...)` per request, or switch to instance composition
213
+ (`Q1.new(prop: value) + Q2.new(...)` ).
214
+
215
+ Both patterns are clearly explained in the bundled skill — see
216
+ `.claude/skills/quo/SKILL.md` after running `bin/rails generate quo:install`.
@@ -0,0 +1,35 @@
1
+ <svg contentScriptType="text/ecmascript" contentStyleType="text/css" preserveAspectRatio="xMidYMid meet" version="1.0" height="20" width="120"
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ xmlns:xlink="http://www.w3.org/1999/xlink">
4
+
5
+
6
+ <linearGradient id="smooth" x2="0" y2="120">
7
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
8
+ <stop offset="1" stop-opacity=".1"/>
9
+ </linearGradient>
10
+ <clipPath id="round">
11
+ <rect height="20" width="120" rx="3" fill="#ffffcc"/>
12
+ </clipPath>
13
+ <g clip-path="url(#round)">
14
+ <rect height="20" width="60" fill="#555"/>
15
+ <rect x="60" height="20" width="60" fill="#cccc00"/>
16
+ <rect height="20" width="120" fill="url(#smooth)"/>
17
+ </g>
18
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11">
19
+ <text x="30" y="15" fill="#010101" fill-opacity="0.3">
20
+ scov total
21
+ </text>
22
+ <text x="30" y="14">
23
+ scov total
24
+ </text>
25
+ </g>
26
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11">
27
+ <text x="90" y="15" fill="#010101" fill-opacity="0.3">
28
+ 96%
29
+ </text>
30
+ <text x="90" y="14">
31
+ 96%
32
+ </text>
33
+ </g>
34
+
35
+ </svg>
@@ -0,0 +1,35 @@
1
+ <svg contentScriptType="text/ecmascript" contentStyleType="text/css" preserveAspectRatio="xMidYMid meet" version="1.0" height="20" width="200"
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ xmlns:xlink="http://www.w3.org/1999/xlink">
4
+
5
+
6
+ <linearGradient id="smooth" x2="0" y2="200">
7
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
8
+ <stop offset="1" stop-opacity=".1"/>
9
+ </linearGradient>
10
+ <clipPath id="round">
11
+ <rect height="20" width="200" rx="3" fill="#fff"/>
12
+ </clipPath>
13
+ <g clip-path="url(#round)">
14
+ <rect height="20" width="100" fill="#555"/>
15
+ <rect x="100" height="20" width="100" fill="#ff0000"/>
16
+ <rect height="20" width="200" fill="url(#smooth)"/>
17
+ </g>
18
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11">
19
+ <text x="50" y="15" fill="#010101" fill-opacity="0.3">
20
+ rubycritic score
21
+ </text>
22
+ <text x="50" y="14">
23
+ rubycritic score
24
+ </text>
25
+ </g>
26
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,sans-serif" font-size="11">
27
+ <text x="150" y="15" fill="#010101" fill-opacity="0.3">
28
+ 87.73/100.0
29
+ </text>
30
+ <text x="150" y="14">
31
+ 87.73/100.0
32
+ </text>
33
+ </g>
34
+
35
+ </svg>
@@ -0,0 +1,100 @@
1
+ # Claude Code skill: Quo
2
+
3
+ A [Claude Code](https://claude.com/claude-code) skill that teaches Claude
4
+ how to use the [Quo gem](https://github.com/stevegeek/quo) for building
5
+ composable, type-safe query objects in a Rails application.
6
+
7
+ ## What this skill provides
8
+
9
+ `SKILL.md` (loaded automatically by Claude Code) covers:
10
+
11
+ - The two composition modes (class vs instance) and when to use each
12
+ - Core query object patterns for ActiveRecord and collections
13
+ - Type-safe property declarations using Literal
14
+ - Pagination, transformer, and `wrap` patterns
15
+ - A short cookbook of common patterns
16
+
17
+ `references/` (loaded on demand by Claude when it needs depth):
18
+
19
+ | File | Read when… |
20
+ |---|---|
21
+ | `references/QUERY_TYPES.md` | Detail on RelationBackedQuery vs CollectionBackedQuery |
22
+ | `references/COMPOSITION.md` | Composition modes, merge strategies, joins, conditional building |
23
+ | `references/PAGINATION.md` | Page navigation, counts, unpaginated access |
24
+ | `references/TRANSFORMERS.md` | Result transformation, presenter patterns |
25
+ | `references/API_REFERENCE.md` | Method-by-method reference for queries and results |
26
+
27
+ ## Versioning
28
+
29
+ Each markdown file in this skill carries a banner near the top declaring
30
+ which Quo version it targets, e.g.:
31
+
32
+ > **Targets Quo `~> 2.0`.**
33
+
34
+ When you upgrade the gem, re-run the install generator with `--force` to
35
+ refresh the skill content:
36
+
37
+ ```bash
38
+ bin/rails generate quo:install --force
39
+ ```
40
+
41
+ ## Installation
42
+
43
+ The intended path is the bundled Rails generator (ships with Quo `~> 2.0`):
44
+
45
+ ```bash
46
+ bin/rails generate quo:install
47
+ ```
48
+
49
+ This copies the skill into your app's `.claude/skills/quo/` directory.
50
+ Claude Code picks it up automatically on the next session.
51
+
52
+ You can also install manually by copying this directory to
53
+ `.claude/skills/quo/` in your project root.
54
+
55
+ ### CLAUDE.md fragment
56
+
57
+ The generator can optionally append a "Quo" section to your project's
58
+ top-level `CLAUDE.md`, telling Claude that the project uses Quo and
59
+ where the skill lives. This is opt-in:
60
+
61
+ ```bash
62
+ bin/rails generate quo:install --with-claude-md
63
+ ```
64
+
65
+ If you prefer to manage `CLAUDE.md` by hand, just add a line like:
66
+
67
+ ```markdown
68
+ ## Quo
69
+
70
+ This project uses the Quo gem for query objects. See
71
+ `.claude/skills/quo/SKILL.md` for usage guidance.
72
+ ```
73
+
74
+ ## How it works
75
+
76
+ When Claude Code starts a session, it loads `SKILL.md` automatically.
77
+ That makes the core concepts and quick references immediately available
78
+ in context. When Claude needs depth on a specific topic, it reads the
79
+ relevant file from `references/` on demand.
80
+
81
+ The skill is organised around progressive disclosure: keep `SKILL.md`
82
+ short and grep-friendly; put the long detail in references.
83
+
84
+ ## Contributing
85
+
86
+ To improve this skill:
87
+
88
+ 1. Edit the relevant markdown file
89
+ 2. Keep `SKILL.md` concise — quick references and patterns
90
+ 3. Put detailed material in the `references/` files
91
+ 4. Update the version banner if the API surface you describe changed
92
+ 5. Use the `Post` / `Author` / `Comment` fixture models from the gem's
93
+ own test suite for examples — they're verifiable against the gem's
94
+ tests and avoid project-specific terminology
95
+
96
+ ## Related
97
+
98
+ - Quo source: <https://github.com/stevegeek/quo>
99
+ - Quo documentation site: <https://quo-gem.diaconou.com/>
100
+ - Literal (type system): <https://github.com/joeldrapper/literal>