gempilot 0.2.4 → 0.3.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.
@@ -0,0 +1,163 @@
1
+ # Dev version bump semantics — design
2
+
3
+ **Issue:** `2360FFA4-9388-11F1-8474-FE6CB9572C2F` — "rake version:bump[dev] goes backward rather than forward"
4
+
5
+ **Date:** 2026-08-09
6
+
7
+ ## Problem
8
+
9
+ `Gempilot::Project::Version#bump(:dev)` appends `.dev1` to the *current* version
10
+ (`0.2.0` → `0.2.0.dev1`). RubyGems treats any version with a letter segment as a
11
+ **prerelease of the version it names** (`Gem::Version` rdoc: "Prereleases sort
12
+ between real releases (newest to oldest): 1.0 > 1.0.b1 > 1.0.a.2 > 0.9"), so
13
+ `0.2.0.dev1 < 0.2.0` — the bump moves the project *backward*. Two related gaps:
14
+
15
+ 1. `bump(:patch)` from `X.Y.Z.devN` produces `X.Y.(Z+1)`, overshooting the
16
+ `X.Y.Z` the dev cycle was working toward.
17
+ 2. There is no way to bump a fourth integer (`0.2.0` → `0.2.0.1`), the
18
+ rubygems-legal *non-prerelease* shape for tiny follow-up fixes on a released
19
+ version (the Rails security-release pattern, `6.0.3.1`).
20
+
21
+ ## Decisions
22
+
23
+ - **Dev versions preview the next patch.** `bump(:dev)` from a release version
24
+ yields the next patch's first dev prerelease (`0.2.0` → `0.2.1.dev1`); from a
25
+ dev version it increments the counter (`0.2.1.dev1` → `0.2.1.dev2`). This is
26
+ the only reading consistent with rubygems ordering, and matches ecosystem
27
+ practice (Rails `7.1.0.beta1` precedes `7.1.0`; Bundler's in-repo `.dev`).
28
+ - **One rule governs every bump: move to the smallest version of the requested
29
+ shape that is strictly greater than the current version.** Consequences:
30
+ numeric bumps from a dev version *finalize* the cycle (`0.2.1.dev3` +
31
+ `patch` → `0.2.1`, not `0.2.2`), and `minor`/`major` redirect it
32
+ (`0.2.1.dev3` + `minor` → `0.3.0`).
33
+ - **New `tiny` segment for the fourth integer.** `bump(:tiny)`: `0.2.0` →
34
+ `0.2.0.1` → `0.2.0.2`. Name chosen by elimination: semver's "build metadata"
35
+ is precedence-ignored (and the `Gem::Version` rdoc uses "build" for the third
36
+ integer); `micro` is Python's third digit; `patchlevel` is Ruby-historical but
37
+ long and confusable with `patch`. `tiny` reads as "smaller than patch", which
38
+ is where it sorts. (Old Rails used TINY for the third digit; accepted.)
39
+ - **No dev target argument** (`bump[dev,minor]` → `0.3.0.dev1`) — YAGNI. The
40
+ shape rule already finalizes hand-edited `M.m.0.devN` / `M.0.0.devN` states
41
+ correctly, so a targeted form can be layered on later without breaking
42
+ anything.
43
+ - **Dev GitHub releases are prereleases.** `release:github` passes
44
+ `--prerelease` to `gh release create` when the tag names a prerelease
45
+ version, so `v0.2.1.dev2` does not become the repo's "Latest" release.
46
+ - **Strict version parsing.** `Version#bump` recognizes exactly three forms —
47
+ `M.m.p`, `M.m.p.N`, `M.m.p.devN` — and raises `ArgumentError` for anything
48
+ else, instead of today's silent first-three-integers coercion.
49
+
50
+ ## Semantics
51
+
52
+ Accepted version forms: release `M.m.p`, tiny release `M.m.p.N`, dev prerelease
53
+ `M.m.p.devN` (parse: `/\A(\d+)\.(\d+)\.(\d+)(?:\.(\d+)|\.dev(\d+))?\z/`).
54
+
55
+ Full transition table (rows marked * arise only by hand-editing; the rule is
56
+ total over all three forms regardless of how the version got there):
57
+
58
+ | from ↓ bump → | `major` | `minor` | `patch` | `tiny` | `dev` |
59
+ | ------------- | ------- | ------- | ------- | --------- | ------------ |
60
+ | `1.2.3` | `2.0.0` | `1.3.0` | `1.2.4` | `1.2.3.1` | `1.2.4.dev1` |
61
+ | `1.2.3.1` | `2.0.0` | `1.3.0` | `1.2.4` | `1.2.3.2` | `1.2.4.dev1` |
62
+ | `1.2.4.dev2` | `2.0.0` | `1.3.0` | `1.2.4` | `1.2.4.1` | `1.2.4.dev3` |
63
+ | `1.3.0.dev2`* | `2.0.0` | `1.3.0` | `1.3.0` | `1.3.0.1` | `1.3.0.dev3` |
64
+ | `2.0.0.dev2`* | `2.0.0` | `2.0.0` | `2.0.0` | `2.0.0.1` | `2.0.0.dev3` |
65
+
66
+ All 25 transitions verified strictly increasing under `Gem::Version` (checked
67
+ 2026-08-09 against ruby's installed rubygems).
68
+
69
+ Branch logic per segment, where the numeric core is `M.m.p` and `dev?` means
70
+ the current version is a dev prerelease:
71
+
72
+ - `major`: `dev? && m == 0 && p == 0` → `M.0.0` (finalize); else `(M+1).0.0`
73
+ - `minor`: `dev? && p == 0` → `M.m.0` (finalize); else `M.(m+1).0`
74
+ - `patch`: `dev?` → `M.m.p` (finalize); else `M.m.(p+1)`
75
+ - `tiny`: `M.m.p.(N+1)` where `N` is the current fourth integer, else 0.
76
+ (From a dev version this yields `M.m.p.1` — it implicitly finalizes the
77
+ target and adds a tiny level; forward-moving, documented edge case.)
78
+ - `dev`: `dev?` → `M.m.p.dev(N+1)`; else `M.m.(p+1).dev1`
79
+
80
+ ## Components
81
+
82
+ ### `Gempilot::Project::Version` (modified — `lib/gempilot/project/version.rb`)
83
+
84
+ - Stays `Data.define(:path, :value)` — `write_version!`, `VersionTag`, and all
85
+ call sites are untouched. Parsing is internal to `bump`.
86
+ - `bump(segment)` accepts `:major, :minor, :patch, :tiny, :dev` (symbol or
87
+ string), implements the table above. Unknown segment → `ArgumentError`
88
+ naming the valid segments (as today, plus `tiny`). Unparseable value →
89
+ `ArgumentError` naming the value.
90
+ - `next_version` alias retained; its behavior follows the new `patch` default
91
+ (`0.0.4.dev3` → `0.0.4`, previously `0.0.5`).
92
+
93
+ ### `Gempilot::GithubRelease` (modified — `lib/gempilot/github_release.rb`)
94
+
95
+ - `#create` appends `--prerelease` when the tag (sans leading `v`) parses as a
96
+ prerelease per `Gem::Version#prerelease?`. Self-contained — the
97
+ `initialize(tag)` signature and all call sites are unchanged. `#destroy` /
98
+ `#list` untouched.
99
+
100
+ ### `Gempilot::VersionTask` (modified — `lib/gempilot/version_task.rb`)
101
+
102
+ - Behavior unchanged (it forwards the segment). Only the `version:bump` desc
103
+ string updates: "Bump version (patch default; segments: major, minor, patch,
104
+ tiny, dev)".
105
+
106
+ ### `Gempilot::CLI::Commands::Bump` (modified — `lib/gempilot/cli/commands/bump.rb`)
107
+
108
+ - Allowed segments become `%w[patch minor major tiny dev]`; error message,
109
+ argument desc, command description, and `examples` updated to match.
110
+
111
+ ## Testing (TDD — written before implementation)
112
+
113
+ **`spec/gempilot/project/version_spec.rb` (rewritten)**
114
+
115
+ - The full 5×5 transition table above, each cell an example.
116
+ - A monotonicity property spec: for every table cell,
117
+ `Gem::Version.new(new) > Gem::Version.new(old)` — the direct regression spec
118
+ for this issue.
119
+ - Segment as string; unknown segment `ArgumentError`; unparseable value
120
+ (`"1.2"`, `"1.2.3.beta1"`) `ArgumentError`; `next_version` follows patch
121
+ semantics (`0.0.4.dev3` → `0.0.4`).
122
+
123
+ **`spec/gempilot/version_task_spec.rb` (expectations updated)**
124
+
125
+ Fixture starts at `1.0.0.dev3`:
126
+ - `version:bump` (default patch) → `1.0.0` (finalize; was `1.0.1`).
127
+ - `version:bump[dev]` → `1.0.0.dev4` (unchanged).
128
+ - `version:bump[tiny]` → `1.0.0.1` (new example).
129
+ - `version:release[dev]` → commit "Bump version to 1.0.0.dev4", tag
130
+ `v1.0.0.dev4` (unchanged).
131
+
132
+ **`spec/gempilot/github_release_spec.rb` (extended)**
133
+
134
+ - `create` with `v1.2.4.dev1` includes `--prerelease`; with `v1.2.4` and
135
+ `v1.2.3.1` it does not.
136
+
137
+ **`test/gempilot/cli/bump_command_test.rb` (extended)**
138
+
139
+ - `tiny` accepted as a segment; unknown-segment error message lists it.
140
+
141
+ ## Docs
142
+
143
+ - `README.md` — `gempilot bump` section gains `tiny` and `dev` examples with a
144
+ one-line explanation of dev semantics; the rake tasks table row for
145
+ `version:bump` mentions the segments.
146
+ - `CLAUDE.md` — version lifecycle bullet: "(patch default, or
147
+ minor/major/tiny/dev)".
148
+
149
+ ## Migration
150
+
151
+ A repo sitting on an old-style dev version (e.g. `0.2.0.dev1` created *after*
152
+ `0.2.0` shipped) is already in the broken state this fixes; `bump(:patch)`
153
+ would finalize to the already-released `0.2.0` (and `version:tag` would then
154
+ fail on the existing tag). Recovery is a one-time hand edit of `version.rb`
155
+ past the last published version. Noted in the README dev-example line.
156
+
157
+ ## Out of scope
158
+
159
+ - Targeted dev cycles (`bump[dev,minor]`) — compatible later extension.
160
+ - Other prerelease markers (`pre`, `rc`, `beta`) — `dev` sorts before all of
161
+ them, so they remain addable later.
162
+ - RubyGems prerelease yanking; `data/templates/gem/Rakefile.erb` (no change
163
+ needed — it just instantiates `Gempilot::VersionTask`).
data/issues.rec CHANGED
@@ -95,7 +95,7 @@ Id: 4EB6E51C-4322-11F1-B1B2-FE6CB9572C2F
95
95
  Updated: Tue, 28 Apr 2026 12:50:05 -0400
96
96
  Title: Existing git repo on gempilot create needs new commit message
97
97
  Description: Currently it commits "Initial commit". Inappropriate for a git repo that already has a history
98
- Status: open
98
+ Status: closed
99
99
 
100
100
  Id: 0411D4DA-4323-11F1-847C-FE6CB9572C2F
101
101
  Updated: Tue, 28 Apr 2026 12:55:09 -0400
@@ -112,19 +112,19 @@ Description: e.g.
112
112
  + end
113
113
  +
114
114
  +
115
- Status: open
115
+ Status: closed
116
116
 
117
117
  Id: 5618BB20-4325-11F1-BF5A-FE6CB9572C2F
118
118
  Updated: Tue, 28 Apr 2026 13:11:46 -0400
119
119
  Title: Move zeitwerk check task into gempilot
120
120
  Description: Can't iterate on it when its not managed by gempilot. A new feature needs to be added
121
- Status: open
121
+ Status: closed
122
122
 
123
123
  Id: 6BE168E4-4325-11F1-BE0F-FE6CB9572C2F
124
124
  Updated: Tue, 28 Apr 2026 13:12:22 -0400
125
125
  Title: Add zeitwerk:all task
126
126
  Description: Add rake task that runs LOADER.all_expected_cpaths and prints in a nice table, similar to rails routes
127
- Status: open
127
+ Status: closed
128
128
 
129
129
  Id: 56DB9634-4327-11F1-8D0F-FE6CB9572C2F
130
130
  Updated: Fri, 05 Jun 2026 00:00:00 -0400
@@ -155,13 +155,13 @@ Description: When doing `gempilot new Reversal::Server`, in a project called "re
155
155
  + create test/reversal/store_test.rb
156
156
  +
157
157
  + The test file is incorrectly named. Should be server_test.rb
158
- Status: open
158
+ Status: closed
159
159
 
160
160
  Id: F5486B48-71DB-11F1-980F-FE6CB9572C2E
161
161
  Updated: Fri, 26 Jun 2026 23:54:55 -0400
162
162
  Title: Rubymine <=> Minitest usage causes "no reporters allowed error"
163
163
  Description: Has to do with how the ENV var RM_INFO is being interpreted
164
- Status: open
164
+ Status: closed
165
165
 
166
166
  Id: 8D847572-7273-11F1-B508-FE6CB9572C2E
167
167
  Updated: Fri, 03 Jul 2026 22:38:22 +0000
@@ -212,3 +212,91 @@ Description: the github release related tasks are invoked as follows:
212
212
  +
213
213
  + Also, there is an issue that prevents github releasing from working at all via the old tasks. I get an error every time saying that "tags have already been pushed" or something along those lines. Ensure that is fixed as well
214
214
  Status: closed
215
+
216
+ Id: 986E0100-88F4-11F1-B718-FE6CB9572C2D
217
+ Updated: Sun, 26 Jul 2026 09:19:13 -0400
218
+ Title: Make rbs and the other gems GP installs mri only
219
+ Description: This tool is also used with jruby, and it fails irb install due to rbs
220
+ Status: open
221
+
222
+ Id: 3FFE7616-88F5-11F1-8D3B-FE6CB9572C2D
223
+ Updated: Sun, 26 Jul 2026 09:23:54 -0400
224
+ Title: Add betterleaks to gem setup
225
+ Description: Ensure betterleaks is integrated into the commit and build pipelines, via commit hooks, rake tasks, and/or whatever the idiomatic way is to integrate betterleaks into a rubyproject
226
+ Status: open
227
+
228
+ Id: F6C6EB04-8DA4-11F1-91EF-FA41C3164F3A
229
+ Updated: Sat, 01 Aug 2026 12:31:47 +0000
230
+ Title: release:rubygem_push only handles one gem per version
231
+ Description: rake release:rubygems can publish only one gem per version. A project that publishes several -- platform gems each carrying a compiled executable, say -- either publishes one of them and silently leaves the rest, or, once it replaces the push task with a pkg glob, tries to re-push versions already on rubygems and stops halfway through the release.
232
+ +
233
+ + MECHANISM
234
+ +
235
+ + gempilot, lib/gempilot/release_tasks.rb: ReleaseTasks#define_rubygems_release wires release:rubygems to [build, release:guard_clean, release:source_control_push, release:rubygem_push], and, as the module's own rdoc says, expects bundler/gem_tasks to have supplied build, release:guard_clean and release:rubygem_push.
236
+ +
237
+ + bundler 4.0.17, lib/bundler/gem_helper.rb: GemHelper#install_tasks defines
238
+ +
239
+ + task "release:rubygem_push" => "build" do
240
+ + rubygem_push(built_gem_path) if gem_push?
241
+ + end
242
+ +
243
+ + built_gem_path is the single .gem that #build_gem produced. One build, one gem, one push. Nothing in that chain can carry a second gem, so a project publishing more than one has to replace both build and release:rubygem_push itself.
244
+ +
245
+ + There is no glob in gempilot's own source to go looking for. The glob is what a downstream project ends up writing once it takes over the push, and it is wrong in a way gempilot's own version:bump triggers.
246
+ +
247
+ + REPRODUCE
248
+ +
249
+ + 1. A gem publishing more than one gem per version. aws-session-manager on gempilot 0.3.0 publishes five: x86_64-linux, aarch64-linux, x86_64-darwin and arm64-darwin, each carrying a compiled session-manager-plugin executable, plus the plain ruby gem.
250
+ + 2. It replaces build, and replaces the push with the obvious implementation:
251
+ + task(:rubygem_push) { PACKAGES.glob("*.gem").each { sh "gem", "push", it.to_s } }
252
+ + 3. rake build && rake release:rubygems -- 0.0.1 publishes.
253
+ + 4. rake version:bump && rake build -- pkg/ now holds both 0.0.1 and 0.0.2 gems. Nothing empties pkg/ between versions; bundler never has, and gempilot's version tasks do not either.
254
+ + 5. rake release:rubygems -- gem push rejects the 0.0.1 gems ("Repushing of gem versions is not allowed"), sh raises on the non-zero status, and the release aborts with part of 0.0.2 published and the rest not. Re-running repeats the failure, because the stale gems are still there.
255
+ +
256
+ + FIX APPLIED DOWNSTREAM
257
+ +
258
+ + aws-session-manager stopped globbing and took the list from the specifications its build packages, so that gem:build and the push share one source of truth:
259
+ +
260
+ + PACKAGED = [*BUILDS.map { it.package.to_s }, GENERIC_PACKAGE].freeze
261
+ +
262
+ + namespace :gem do
263
+ + task build: PACKAGED
264
+ + end
265
+ +
266
+ + namespace :release do
267
+ + task(:rubygem_push) { PACKAGED.each { sh "gem", "push", it } }
268
+ + end
269
+ +
270
+ + FIX SUGGESTED HERE
271
+ +
272
+ + gempilot should own release:rubygem_push rather than inherit bundler's, and push every gem in pkg belonging to the version being released. It cannot know how a project builds, but it knows the name and the version, which is enough:
273
+ +
274
+ + def define_rubygem_push(project)
275
+ + clear_task "release:rubygem_push"
276
+ + task "release:rubygem_push" => "build" do
277
+ + packages(project).each { sh "gem", "push", it.to_s }
278
+ + end
279
+ + end
280
+ +
281
+ + def packages(project)
282
+ + stem = "#{project.name}-#{project.version_value}"
283
+ + project.root.glob("pkg/{#{stem}.gem,#{stem}-*.gem}")
284
+ + end
285
+ +
286
+ + Project#name, #version_value and #root already exist in lib/gempilot/project.rb.
287
+ +
288
+ + Two details worth keeping:
289
+ +
290
+ + - The brace glob matches gempilot-0.3.0.gem and gempilot-0.3.0-x86_64-linux.gem, and does not match gempilot-0.3.10.gem. A bare "#{stem}*.gem" would match the latter and push the wrong version.
291
+ + - An empty result should abort. Otherwise a project whose build writes gems elsewhere gets a release that reports success having published nothing, which is worse than the bug being fixed.
292
+ +
293
+ + Single-gem projects are unaffected: pkg holds one gem for the current version, and it gets pushed, as today. Multi-gem projects stop having to reinvent the push.
294
+ +
295
+ + If gempilot would rather not own the task, then the ReleaseTasks rdoc should state the contract instead: a project that replaces build must also replace release:rubygem_push, and must scope the push to the version being released rather than globbing pkg.
296
+ Status: open
297
+
298
+ Id: 2360FFA4-9388-11F1-8474-FE6CB9572C2F
299
+ Updated: Sat, 08 Aug 2026 20:20:34 -0400
300
+ Title: rake version:bump[dev] goes backward rather than forward
301
+ Description: rubygems treats 0.2.0.dev as an *older* version than 0.2.0. The way gempilot handles dev version needs to be rethought. Additionally, how does a user trigger a bump on the fourth integer, e.g. 0.2.0.1?
302
+ Status: open
@@ -8,18 +8,19 @@ module Gempilot
8
8
  include GemContext
9
9
 
10
10
  usage "[options] [SEGMENT]"
11
- description "Bump the gem version (patch by default, or minor/major/dev)"
11
+ description "Bump the gem version (patch by default, or minor/major/tiny/dev)"
12
12
 
13
13
  examples [
14
14
  "",
15
15
  "patch",
16
16
  "minor",
17
17
  "major",
18
+ "tiny",
18
19
  "dev",
19
20
  ]
20
21
 
21
22
  argument :segment, required: false,
22
- desc: "Version segment to bump: patch (default), minor, major, or dev"
23
+ desc: "Version segment to bump: patch (default), minor, major, tiny, or dev"
23
24
 
24
25
  def run(segment = "patch")
25
26
  detect_gem_context
@@ -31,9 +32,9 @@ module Gempilot
31
32
 
32
33
  def validate_segment(segment)
33
34
  segment = segment.downcase
34
- return segment if %w[patch minor major dev].include?(segment)
35
+ return segment if %w[patch minor major tiny dev].include?(segment)
35
36
 
36
- puts colors.red("Unknown segment '#{segment}'. Use patch, minor, major, or dev.")
37
+ puts colors.red("Unknown segment '#{segment}'. Use patch, minor, major, tiny, or dev.")
37
38
  exit 1
38
39
  end
39
40
 
@@ -97,9 +97,21 @@ module Gempilot
97
97
  cd @gem_name do
98
98
  sh "git", "init", "-q", "-b", @branch
99
99
  sh "git", "add", "."
100
- sh "git", "commit", "-q", "-m", "Initial commit."
100
+ sh "git", "commit", "-q", "-m", commit_message
101
101
  end
102
102
  end
103
+
104
+ # "Initial commit." is wrong when scaffolding into a directory that is
105
+ # already a repository with history (e.g. a cloned remote), so name the
106
+ # commit after the gem in that case.
107
+ def commit_message
108
+ existing_history? ? "Add #{@gem_name} gem scaffolding." : "Initial commit."
109
+ end
110
+
111
+ def existing_history?
112
+ system("git", "rev-parse", "--verify", "--quiet", "HEAD",
113
+ out: File::NULL, err: File::NULL)
114
+ end
103
115
  end
104
116
  end
105
117
  end
@@ -30,18 +30,14 @@ module Gempilot
30
30
 
31
31
  ## Path to the constant's source file, e.g. +lib/my_gem/services/auth.rb+.
32
32
  def lib_path
33
- "#{File.join("lib", *path_segments)}.rb"
33
+ path_for("lib", ".rb")
34
34
  end
35
35
 
36
36
  ## Path to the constant's test file for +framework+ (+:rspec+ or
37
- ## +:minitest+); correct for multi-segment (hyphenated) gem modules.
37
+ ## +:minitest+). Mirrors +lib_path+ so the test always tracks the source,
38
+ ## including for multi-segment (hyphenated) gem modules.
38
39
  def test_path(framework)
39
- rest = path_segments.drop(require_path.split("/").length)
40
- if framework == :rspec
41
- "#{File.join("spec", require_path, *rest)}_spec.rb"
42
- else
43
- "#{File.join("test", require_path, *rest)}_test.rb"
44
- end
40
+ framework == :rspec ? path_for("spec", "_spec.rb") : path_for("test", "_test.rb")
45
41
  end
46
42
 
47
43
  private
@@ -57,5 +53,9 @@ module Gempilot
57
53
  def path_segments
58
54
  parts.map(&:underscore)
59
55
  end
56
+
57
+ def path_for(root, suffix)
58
+ "#{File.join(root, *path_segments)}#{suffix}"
59
+ end
60
60
  end
61
61
  end
@@ -1,5 +1,6 @@
1
1
  module Gempilot
2
- ## Manages GitHub releases for a version tag.
2
+ ## Manages GitHub releases for a version tag. Tags naming a prerelease
3
+ ## version (e.g. +v1.2.4.dev1+) are created as GitHub prereleases.
3
4
  class GithubRelease
4
5
  include StrictShell
5
6
 
@@ -12,6 +13,7 @@ module Gempilot
12
13
  def create
13
14
  sh "gh", "release", "create",
14
15
  "--generate-notes", "--fail-on-no-commits",
16
+ *prerelease_flag,
15
17
  tag
16
18
  end
17
19
 
@@ -24,5 +26,11 @@ module Gempilot
24
26
  def list
25
27
  sh "gh", "release", "list"
26
28
  end
29
+
30
+ private
31
+
32
+ def prerelease_flag
33
+ Gem::Version.new(tag.delete_prefix("v")).prerelease? ? ["--prerelease"] : []
34
+ end
27
35
  end
28
36
  end
@@ -1,36 +1,15 @@
1
1
  module Gempilot
2
2
  class Project
3
+ ## The project's version file (+path+) and value. Bump arithmetic
4
+ ## delegates to SegmentedVersion, so every bump moves strictly forward
5
+ ## under RubyGems ordering.
3
6
  Version = Data.define(:path, :value) do
4
7
  def tag
5
8
  "v#{value}"
6
9
  end
7
10
 
8
11
  def bump(segment = :patch)
9
- case segment.to_sym
10
- when :major, :minor, :patch then bump_semver(segment)
11
- when :dev then bump_dev
12
- else raise ArgumentError, "Unknown segment #{segment.inspect}. Use :major, :minor, :patch, or :dev"
13
- end
14
- end
15
-
16
- private
17
-
18
- def bump_semver(segment)
19
- major, minor, patch = value.split(".").first(3).map(&:to_i)
20
- new_value = case segment.to_sym
21
- when :major then "#{major + 1}.0.0"
22
- when :minor then "#{major}.#{minor + 1}.0"
23
- when :patch then "#{major}.#{minor}.#{patch + 1}"
24
- end
25
- with(value: new_value)
26
- end
27
-
28
- def bump_dev
29
- if value.match?(/\.dev\d+\z/)
30
- with(value: value.sub(/\d+\z/) { it.to_i + 1 })
31
- else
32
- with(value: "#{value}.dev1")
33
- end
12
+ with(value: SegmentedVersion.parse(value).bump(segment).to_s)
34
13
  end
35
14
 
36
15
  alias_method :next_version, :bump
@@ -33,8 +33,16 @@ module Gempilot
33
33
  project_segments.join("-")
34
34
  end
35
35
 
36
+ def require_path
37
+ project_segments.join("/")
38
+ end
39
+
40
+ def module_name
41
+ project_segments.map(&:camelize).join("::")
42
+ end
43
+
36
44
  def klass
37
- Object.const_get(project_segments.map(&:camelize).join("::"))
45
+ Object.const_get(module_name)
38
46
  end
39
47
 
40
48
  def version
@@ -0,0 +1,64 @@
1
+ module Gempilot
2
+ ## Arithmetic over the three version shapes gempilot accepts: release
3
+ ## (+1.2.3+), tiny release (+1.2.3.1+), and dev prerelease (+1.2.3.dev1+).
4
+ ##
5
+ ## Every bump returns the smallest version of the requested shape that is
6
+ ## strictly greater than the current version under RubyGems ordering, so
7
+ ## bumping always moves a project forward: a dev bump previews the next
8
+ ## patch (+1.2.3+ to +1.2.4.dev1+) and numeric bumps finalize a dev cycle
9
+ ## (+1.2.4.dev2+ to +1.2.4+ for +:patch+).
10
+ SegmentedVersion = Data.define(:major, :minor, :patch, :tiny, :dev) do
11
+ def self.parse(string)
12
+ format = /\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:\.(?<tiny>\d+)|\.dev(?<dev>\d+))?\z/
13
+ match = format.match(string)
14
+ raise ArgumentError, "cannot parse version: #{string.inspect}" unless match
15
+
16
+ new(**match.named_captures.to_h { |name, digits| [name.to_sym, digits&.to_i] })
17
+ end
18
+
19
+ def bump(segment)
20
+ case segment.to_sym
21
+ when :major then bump_major
22
+ when :minor then bump_minor
23
+ when :patch then bump_patch
24
+ when :tiny then bump_tiny
25
+ when :dev then bump_dev
26
+ else raise ArgumentError, "Unknown segment #{segment.inspect}. Use :major, :minor, :patch, :tiny, or :dev"
27
+ end
28
+ end
29
+
30
+ def to_s
31
+ [major, minor, patch, tiny, ("dev#{dev}" if dev)].compact.join(".")
32
+ end
33
+
34
+ private
35
+
36
+ def bump_major
37
+ return finalize if dev && minor.zero? && patch.zero?
38
+
39
+ with(major: major + 1, minor: 0, patch: 0, tiny: nil, dev: nil)
40
+ end
41
+
42
+ def bump_minor
43
+ return finalize if dev && patch.zero?
44
+
45
+ with(minor: minor + 1, patch: 0, tiny: nil, dev: nil)
46
+ end
47
+
48
+ def bump_patch
49
+ dev ? finalize : with(patch: patch + 1, tiny: nil)
50
+ end
51
+
52
+ def bump_tiny
53
+ with(tiny: (tiny || 0) + 1, dev: nil)
54
+ end
55
+
56
+ def bump_dev
57
+ dev ? with(dev: dev + 1) : with(patch: patch + 1, tiny: nil, dev: 1)
58
+ end
59
+
60
+ def finalize
61
+ with(tiny: nil, dev: nil)
62
+ end
63
+ end.freeze
64
+ end
@@ -1,3 +1,3 @@
1
1
  module Gempilot
2
- VERSION = "0.2.4".freeze
2
+ VERSION = "0.3.1".freeze
3
3
  end
@@ -50,7 +50,7 @@ module Gempilot
50
50
  end
51
51
 
52
52
  def define_bump_task(project)
53
- desc "Bump version (patch default, or rake version:bump[minor])"
53
+ desc "Bump version (patch default; segments: major, minor, patch, tiny, dev)"
54
54
  task :bump, [:segment] do |_t, args|
55
55
  segment = (args[:segment] || :patch).to_sym
56
56
  old_version = project.version
@@ -0,0 +1,61 @@
1
+ require "rake/tasklib"
2
+ require_relative "../gempilot"
3
+
4
+ module Gempilot
5
+ ## Rake tasks for validating and inspecting a gem's Zeitwerk loader.
6
+ ##
7
+ ## Owned by gempilot and consumed by generated gems via
8
+ ## <tt>require "gempilot/zeitwerk_task"; Gempilot::ZeitwerkTask.new</tt>, so
9
+ ## the logic rolls forward on a gempilot bump instead of being copied into
10
+ ## every gem's Rakefile. Each task boots a clean child process so eager
11
+ ## loading surfaces naming errors without polluting the Rake process.
12
+ class ZeitwerkTask < Rake::TaskLib
13
+ attr_reader :project
14
+
15
+ def initialize(root: Dir.pwd)
16
+ super()
17
+ @project = Project.new(root)
18
+ define_tasks
19
+ end
20
+
21
+ private
22
+
23
+ def define_tasks
24
+ namespace :zeitwerk do
25
+ define_validate_task
26
+ define_all_task
27
+ end
28
+ end
29
+
30
+ def define_validate_task
31
+ desc "Verify all files follow Zeitwerk naming conventions"
32
+ task(:validate) { ruby "-Ilib", "-e", validate_script }
33
+ end
34
+
35
+ def define_all_task
36
+ desc "List every constant Zeitwerk manages and the file it expects"
37
+ task(:all) { ruby "-Ilib", "-e", all_script }
38
+ end
39
+
40
+ def loader
41
+ "#{project.module_name}::LOADER"
42
+ end
43
+
44
+ def validate_script
45
+ <<~RUBY
46
+ require '#{project.require_path}'
47
+ #{loader}.eager_load(force: true)
48
+ puts 'Zeitwerk: All files loaded successfully.'
49
+ RUBY
50
+ end
51
+
52
+ def all_script
53
+ <<~RUBY
54
+ require '#{project.require_path}'
55
+ rows = #{loader}.all_expected_cpaths.sort_by(&:last)
56
+ width = rows.map { |_path, cpath| cpath.length }.max || 0
57
+ rows.each { |path, cpath| puts format("%-\#{width}s %s", cpath, path) }
58
+ RUBY
59
+ end
60
+ end
61
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gempilot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Gillis
@@ -72,6 +72,8 @@ executables:
72
72
  extensions: []
73
73
  extra_rdoc_files: []
74
74
  files:
75
+ - ".claude/memory/be-decisive-not-menus.md"
76
+ - ".claude/memory/flagship-shared-rubocop-config-state.md"
75
77
  - ".claude/skills/using-command-kit/SKILL.md"
76
78
  - ".claude/skills/using-command-kit/cli-example.rb"
77
79
  - ".claude/skills/using-command-kit/generator-pattern.rb"
@@ -107,7 +109,9 @@ files:
107
109
  - data/templates/new/.keep
108
110
  - data/templates/new/command.rb.erb
109
111
  - docs/superpowers/plans/2026-07-20-release-task-hierarchy.md
112
+ - docs/superpowers/plans/2026-08-09-dev-version-bump.md
110
113
  - docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md
114
+ - docs/superpowers/specs/2026-08-09-dev-version-bump-design.md
111
115
  - exe/gempilot
112
116
  - issues.rec
113
117
  - lib/core_ext/string/inflection_methods.rb
@@ -130,10 +134,12 @@ files:
130
134
  - lib/gempilot/project.rb
131
135
  - lib/gempilot/project/version.rb
132
136
  - lib/gempilot/release_tasks.rb
137
+ - lib/gempilot/segmented_version.rb
133
138
  - lib/gempilot/strict_shell.rb
134
139
  - lib/gempilot/version.rb
135
140
  - lib/gempilot/version_tag.rb
136
141
  - lib/gempilot/version_task.rb
142
+ - lib/gempilot/zeitwerk_task.rb
137
143
  - vendor/vendored.gemv
138
144
  homepage: https://github.com/gillisd/gempilot
139
145
  licenses:
@@ -155,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
161
  - !ruby/object:Gem::Version
156
162
  version: '0'
157
163
  requirements: []
158
- rubygems_version: 4.0.16
164
+ rubygems_version: 4.0.18
159
165
  specification_version: 4
160
166
  summary: A toolkit for creating, managing, and releasing your own rubygems
161
167
  test_files: []