gempilot 0.2.2 → 0.2.3

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.
@@ -1,679 +0,0 @@
1
- # Address Code-Review Critiques Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Retire a class of namespace/path bugs and the `new`/`destroy` duplication by introducing a `GemConstant` value object, plus small polish to the version tasks, framework detection, and stale docs.
6
-
7
- **Architecture:** Extract the duplicated "constant within a gem" logic (qualify, namespaces, lib/test paths) out of `new.rb`, `destroy.rb`, and `gem_context.rb` into one immutable `Gempilot::GemConstant` (a `Data` subclass), mirroring the existing `Project::Version` value object. Commands build a `GemConstant` via a shared `GemContext#gem_constant` factory and read paths off it. Smaller, independent fixes round out the other critiques.
8
-
9
- **Tech Stack:** Ruby 4.0, CommandKit, Zeitwerk, RSpec 3.13 + Minitest, RuboCop (+ rubocop-rspec).
10
-
11
- ---
12
-
13
- ## Environment note
14
-
15
- `bundle exec` is broken on this box (vendored gems are macOS-built; see memory `gempilot-test-running-env`). Run tools directly against system gems:
16
- - RSpec: `rspec <path>` (full suite: `rspec`)
17
- - Minitest: `ruby -Itest -Ilib -e 'Dir["test/**/*_test.rb"].each { |f| require File.expand_path(f) }'`
18
- - RuboCop: `rubocop <paths>`
19
-
20
- In a normal environment these are `bundle exec rspec` / `bundle exec rake test` / `bundle exec rake rubocop`.
21
-
22
- ## File structure
23
-
24
- | File | Responsibility | Change |
25
- |---|---|---|
26
- | `lib/gempilot/gem_constant.rb` | Value object: a class/module constant within a gem (qualify + paths) | **Create** |
27
- | `spec/gempilot/gem_constant_spec.rb` | Unit spec for `GemConstant` | **Create** |
28
- | `lib/gempilot/cli/gem_context.rb` | Shared command context; `gem_constant` factory + framework detection | Modify |
29
- | `lib/gempilot/cli/commands/new.rb` | `new` command, now delegating constant logic to `GemConstant` | Modify |
30
- | `lib/gempilot/cli/commands/destroy.rb` | `destroy` command, same delegation | Modify |
31
- | `lib/gempilot/version_tag.rb` | Version git ops: consistent error + named constant | Modify |
32
- | `spec/gempilot/version_tag_spec.rb` | Update the non-bump assertion expectation | Modify |
33
- | `spec/gempilot/cli/commands/new_namespace_spec.rb` | Document unified non-interactive rooting | Modify |
34
- | `CLAUDE.md` | Fix stale architecture/feature notes | Modify |
35
-
36
- **Design decision (documented in `GemConstant`):** every class/module constant is rooted at the gem's module *by construction* — bare input is prefixed, input already starting with the gem's first segment is left as-is. This makes interactive and non-interactive behavior consistent and removes the half-enforced `validate_gem_root!` (no test depends on its rejection). Rooting matches on the first segment only, preserving extension-gem support (a `My::Gem` gem accepts any `My::…`).
37
-
38
- ---
39
-
40
- ## Part A — Version task polish
41
-
42
- ### Task A1: Make the non-bump guard `raise` instead of `abort`
43
-
44
- **Files:**
45
- - Modify: `lib/gempilot/version_tag.rb:50`
46
- - Test: `spec/gempilot/version_tag_spec.rb:109-118`
47
-
48
- - [ ] **Step 1: Update the spec to expect a `RuntimeError`** (consistent with `#create`'s dirty-staging error)
49
-
50
- In `spec/gempilot/version_tag_spec.rb`, replace the block at lines 109-118 with:
51
-
52
- ```ruby
53
- describe "#tag when last commit is not a version bump" do
54
- before do
55
- File.write("README.md", "init")
56
- system("git add README.md && git commit -m 'Not a version bump' --quiet")
57
- end
58
-
59
- it "raises an error" do
60
- expect { version_tag.tag }.to raise_error(RuntimeError, /does not appear to be a version bump/)
61
- end
62
- end
63
- ```
64
-
65
- - [ ] **Step 2: Run the spec, verify it fails**
66
-
67
- Run: `rspec spec/gempilot/version_tag_spec.rb -e "not a version bump"`
68
- Expected: FAIL — `expected RuntimeError, got #<SystemExit: exit>` (code still calls `abort`).
69
-
70
- - [ ] **Step 3: Change `abort` to `raise`**
71
-
72
- In `lib/gempilot/version_tag.rb`, in `assert_last_commit_is_bump!` (line 50), change:
73
-
74
- ```ruby
75
- abort "Last commit does not appear to be a version bump." unless message.start_with?("Bump version to ")
76
- ```
77
- to:
78
- ```ruby
79
- raise "Last commit does not appear to be a version bump." unless message.start_with?("Bump version to ")
80
- ```
81
-
82
- - [ ] **Step 4: Run the spec, verify it passes**
83
-
84
- Run: `rspec spec/gempilot/version_tag_spec.rb`
85
- Expected: PASS (all examples).
86
-
87
- - [ ] **Step 5: Commit**
88
-
89
- ```bash
90
- git add lib/gempilot/version_tag.rb spec/gempilot/version_tag_spec.rb
91
- git commit -m "Raise instead of abort in version bump guard"
92
- ```
93
-
94
- ### Task A2: Extract the bump-message prefix into a named constant
95
-
96
- **Files:**
97
- - Modify: `lib/gempilot/version_tag.rb`
98
-
99
- - [ ] **Step 1: Confirm current behavior is covered, run the spec**
100
-
101
- Run: `rspec spec/gempilot/version_tag_spec.rb`
102
- Expected: PASS. (`#create` asserts the commit message; the guard test asserts the prefix check — both cover this refactor.)
103
-
104
- - [ ] **Step 2: Add the constant and use it in both places**
105
-
106
- In `lib/gempilot/version_tag.rb`, add the constant just below `include StrictShell`:
107
-
108
- ```ruby
109
- include StrictShell
110
-
111
- ## Commit-message prefix written for a version bump; the guard below
112
- ## matches on it, so the two must stay in sync.
113
- BUMP_MESSAGE_PREFIX = "Bump version to ".freeze
114
- ```
115
-
116
- In `#create`, change the commit line to:
117
-
118
- ```ruby
119
- sh "git", "commit", "-m", "#{BUMP_MESSAGE_PREFIX}#{version.value}"
120
- ```
121
-
122
- In `assert_last_commit_is_bump!`, change the guard to:
123
-
124
- ```ruby
125
- raise "Last commit does not appear to be a version bump." unless message.start_with?(BUMP_MESSAGE_PREFIX)
126
- ```
127
-
128
- - [ ] **Step 3: Run the spec, verify still green**
129
-
130
- Run: `rspec spec/gempilot/version_tag_spec.rb`
131
- Expected: PASS (commit message is byte-identical: `"Bump version to <value>"`).
132
-
133
- - [ ] **Step 4: Lint and commit**
134
-
135
- ```bash
136
- rubocop lib/gempilot/version_tag.rb
137
- git add lib/gempilot/version_tag.rb
138
- git commit -m "Name the version bump commit-message prefix"
139
- ```
140
-
141
- ---
142
-
143
- ## Part B — `GemConstant` value object + de-duplicate new/destroy
144
-
145
- ### Task B1: Create the `GemConstant` value object
146
-
147
- **Files:**
148
- - Create: `lib/gempilot/gem_constant.rb`
149
- - Test: `spec/gempilot/gem_constant_spec.rb`
150
-
151
- - [ ] **Step 1: Write the unit spec**
152
-
153
- Create `spec/gempilot/gem_constant_spec.rb`:
154
-
155
- ```ruby
156
- require "spec_helper"
157
-
158
- RSpec.describe Gempilot::GemConstant do
159
- def constant(input, gem_module: "MyGem", require_path: "my_gem")
160
- described_class.new(input: input, gem_module: gem_module, require_path: require_path)
161
- end
162
-
163
- describe "#qualified" do
164
- it "prepends the gem module to a bare suffix" do
165
- expect(constant("Services::Auth").qualified).to eq("MyGem::Services::Auth")
166
- end
167
-
168
- it "leaves an already-rooted constant unchanged" do
169
- expect(constant("MyGem::Services::Auth").qualified).to eq("MyGem::Services::Auth")
170
- end
171
-
172
- it "matches on the root segment for multi-segment modules" do
173
- c = constant("My::Widget", gem_module: "My::Gem", require_path: "my/gem")
174
- expect(c.qualified).to eq("My::Widget")
175
- end
176
-
177
- it "prepends the full module to a bare suffix under a multi-segment module" do
178
- c = constant("Widget", gem_module: "My::Gem", require_path: "my/gem")
179
- expect(c.qualified).to eq("My::Gem::Widget")
180
- end
181
- end
182
-
183
- describe "#namespaces / #name" do
184
- it "splits the qualified constant", :aggregate_failures do
185
- c = constant("Services::Auth")
186
- expect(c.namespaces).to eq(%w[MyGem Services])
187
- expect(c.name).to eq("Auth")
188
- end
189
- end
190
-
191
- describe "#lib_path" do
192
- it "builds the underscored source path" do
193
- expect(constant("Services::Auth").lib_path).to eq("lib/my_gem/services/auth.rb")
194
- end
195
- end
196
-
197
- describe "#test_path" do
198
- it "builds the rspec path" do
199
- expect(constant("Services::Auth").test_path(:rspec)).to eq("spec/my_gem/services/auth_spec.rb")
200
- end
201
-
202
- it "builds the minitest path" do
203
- expect(constant("Services::Auth").test_path(:minitest)).to eq("test/my_gem/services/auth_test.rb")
204
- end
205
-
206
- it "does not duplicate segments for multi-segment (hyphenated) modules" do
207
- c = constant("Widget", gem_module: "My::Gem", require_path: "my/gem")
208
- expect(c.test_path(:minitest)).to eq("test/my/gem/widget_test.rb")
209
- end
210
- end
211
- end
212
- ```
213
-
214
- - [ ] **Step 2: Run the spec, verify it fails**
215
-
216
- Run: `rspec spec/gempilot/gem_constant_spec.rb`
217
- Expected: FAIL — `uninitialized constant Gempilot::GemConstant`.
218
-
219
- - [ ] **Step 3: Create the value object**
220
-
221
- Create `lib/gempilot/gem_constant.rb`:
222
-
223
- ```ruby
224
- module Gempilot
225
- ## A class or module constant resolved within a gem's namespace.
226
- ##
227
- ## Wraps raw user input (a bare suffix like +Services::Auth+ or a
228
- ## fully-qualified +MyGem::Services::Auth+) together with the gem's module
229
- ## and require path, and derives the qualified constant, file paths, and
230
- ## namespace pieces from a single parse.
231
- ##
232
- ## Every constant is rooted at the gem's module: bare input is prefixed with
233
- ## it, while input already starting with the gem's root segment is left as
234
- ## is. Rooting is matched on the first segment only, so an extension gem
235
- ## whose module is +My::Gem+ accepts any +My::...+ constant.
236
- class GemConstant < Data.define(:input, :gem_module, :require_path)
237
- using String::Inflectable
238
-
239
- ## The fully-qualified constant, rooted at the gem module.
240
- def qualified
241
- input.start_with?("#{root_segment}::") ? input : "#{gem_module}::#{input}"
242
- end
243
-
244
- ## Namespace segments preceding the final constant name.
245
- def namespaces
246
- parts[0...-1]
247
- end
248
-
249
- ## The final class or module name.
250
- def name
251
- parts.last
252
- end
253
-
254
- ## Path to the constant's source file, e.g. +lib/my_gem/services/auth.rb+.
255
- def lib_path
256
- "#{File.join("lib", *path_segments)}.rb"
257
- end
258
-
259
- ## Path to the constant's test file for +framework+ (+:rspec+ or
260
- ## +:minitest+); correct for multi-segment (hyphenated) gem modules.
261
- def test_path(framework)
262
- rest = path_segments.drop(require_path.split("/").length)
263
- if framework == :rspec
264
- "#{File.join("spec", require_path, *rest)}_spec.rb"
265
- else
266
- "#{File.join("test", require_path, *rest)}_test.rb"
267
- end
268
- end
269
-
270
- private
271
-
272
- def root_segment
273
- gem_module.split("::").first
274
- end
275
-
276
- def parts
277
- qualified.split("::")
278
- end
279
-
280
- def path_segments
281
- parts.map(&:underscore)
282
- end
283
- end
284
- end
285
- ```
286
-
287
- - [ ] **Step 4: Run the spec, verify it passes**
288
-
289
- Run: `rspec spec/gempilot/gem_constant_spec.rb`
290
- Expected: PASS (all examples). If you see `undefined method 'underscore'`, confirm `String::Inflectable` is loaded at boot (it is for `new.rb`); the file-top `using` activates it for the whole file.
291
-
292
- - [ ] **Step 5: Lint and commit**
293
-
294
- ```bash
295
- rubocop lib/gempilot/gem_constant.rb spec/gempilot/gem_constant_spec.rb
296
- git add lib/gempilot/gem_constant.rb spec/gempilot/gem_constant_spec.rb
297
- git commit -m "Add GemConstant value object for gem-rooted constants"
298
- ```
299
-
300
- ### Task B2: Add a `gem_constant` factory to `GemContext`
301
-
302
- **Files:**
303
- - Modify: `lib/gempilot/cli/gem_context.rb`
304
-
305
- - [ ] **Step 1: Add the factory method** (additive; `parse_constant`/`validate_gem_root!` stay for now so nothing breaks)
306
-
307
- In `lib/gempilot/cli/gem_context.rb`, add this method inside `module GemContext` (after `detect_gem_context`):
308
-
309
- ```ruby
310
- def gem_constant(input)
311
- GemConstant.new(input: input, gem_module: @gem_module, require_path: @require_path)
312
- end
313
- ```
314
-
315
- - [ ] **Step 2: Verify nothing is broken**
316
-
317
- Run: `rspec`
318
- Expected: PASS (additive change; existing suite unaffected).
319
-
320
- - [ ] **Step 3: Commit**
321
-
322
- ```bash
323
- git add lib/gempilot/cli/gem_context.rb
324
- git commit -m "Add GemContext#gem_constant factory"
325
- ```
326
-
327
- ### Task B3: Refactor `new` to use `GemConstant`
328
-
329
- **Files:**
330
- - Modify: `lib/gempilot/cli/commands/new.rb`
331
- - Test: `spec/gempilot/cli/commands/new_namespace_spec.rb`
332
-
333
- - [ ] **Step 1: Add a spec for unified non-interactive rooting** (documents the now-consistent behavior: a bare name passed as an argument is rooted, instead of being rejected)
334
-
335
- In `spec/gempilot/cli/commands/new_namespace_spec.rb`, add this context inside the top-level `describe` (the file already has the tmpdir `around` + `include FileUtils` + `my_gem` gemspec fixture):
336
-
337
- ```ruby
338
- describe "non-interactive rooting" do
339
- before { mkdir_p("lib/my_gem") }
340
-
341
- it "roots a bare class name under the gem module" do
342
- command.main(["class", "Services::Auth"])
343
-
344
- expect(File).to exist("lib/my_gem/services/auth.rb")
345
- end
346
- end
347
- ```
348
-
349
- - [ ] **Step 2: Run it, verify it fails**
350
-
351
- Run: `rspec spec/gempilot/cli/commands/new_namespace_spec.rb -e "roots a bare class name"`
352
- Expected: FAIL — current code calls `validate_gem_root!("Services")` and exits, so the file is not created (`SystemExit`).
353
-
354
- - [ ] **Step 3: Refactor `new.rb`**
355
-
356
- In `lib/gempilot/cli/commands/new.rb`:
357
-
358
- Replace `prompt_for_path` (it no longer prepends — `GemConstant#qualified` owns that):
359
-
360
- ```ruby
361
- def prompt_for_path(type)
362
- ask(colors.green(type), required: true)
363
- end
364
- ```
365
-
366
- Replace `dispatch_add` so class/module build a `GemConstant`:
367
-
368
- ```ruby
369
- def dispatch_add(type, path)
370
- case type
371
- when "class" then add_class(gem_constant(path))
372
- when "module" then add_module(gem_constant(path))
373
- when "command" then add_command(path)
374
- else
375
- puts colors.red("Unknown type '#{type}'. Use class, module, or command.")
376
- exit 1
377
- end
378
- end
379
- ```
380
-
381
- Delete `prepare_constant` and `class_test_path` entirely. Replace `add_class`, `add_module`, and `add_test_file` with:
382
-
383
- ```ruby
384
- def add_class(constant)
385
- print_adding_banner("class", constant.qualified)
386
- ensure_directory(File.dirname(constant.lib_path))
387
- source = build_nested_source(constant.namespaces, "class", constant.name)
388
- create_file(constant.lib_path, source)
389
- add_test_file(constant)
390
- end
391
-
392
- def add_module(constant)
393
- print_adding_banner("module", constant.qualified)
394
- ensure_directory(File.dirname(constant.lib_path))
395
- source = build_nested_source(constant.namespaces, "module", constant.name)
396
- create_file(constant.lib_path, source)
397
- end
398
-
399
- def add_test_file(constant)
400
- test_path = constant.test_path(@test_framework)
401
- ensure_directory(File.dirname(test_path))
402
- content = if @test_framework == :rspec
403
- rspec_class_content(constant.namespaces, constant.name)
404
- else
405
- minitest_class_content(constant.namespaces, constant.name)
406
- end
407
- create_file(test_path, content)
408
- end
409
- ```
410
-
411
- Leave `add_command`, `command_test_path`, `add_command_test_file`, `rspec_class_content`, `minitest_class_content`, `rspec_command_content`, `minitest_command_content`, `build_nested_source`, `build_namespace_lines`, `build_closing_lines`, `print_adding_banner`, and `ensure_directory` unchanged.
412
-
413
- - [ ] **Step 4: Run the new test, the command specs, and the minitest suite**
414
-
415
- Run: `rspec spec/gempilot/cli/commands/new_namespace_spec.rb spec/gempilot/cli/commands/new_interactive_spec.rb`
416
- Expected: PASS (interactive + hyphenated cases still green; new rooting test green).
417
-
418
- Run: `ruby -Itest -Ilib -e 'require File.expand_path("test/gempilot/cli/new_command_test.rb")'`
419
- Expected: PASS, 0 failures (full constants are unchanged by `qualified`).
420
-
421
- - [ ] **Step 5: Lint and commit**
422
-
423
- ```bash
424
- rubocop lib/gempilot/cli/commands/new.rb spec/gempilot/cli/commands/new_namespace_spec.rb
425
- git add lib/gempilot/cli/commands/new.rb spec/gempilot/cli/commands/new_namespace_spec.rb
426
- git commit -m "Refactor new command onto GemConstant"
427
- ```
428
-
429
- ### Task B4: Refactor `destroy` to use `GemConstant`
430
-
431
- **Files:**
432
- - Modify: `lib/gempilot/cli/commands/destroy.rb`
433
-
434
- - [ ] **Step 1: Run the destroy specs (baseline green)**
435
-
436
- Run: `rspec spec/gempilot/cli/commands/destroy_namespace_spec.rb spec/gempilot/cli/commands/destroy_interactive_spec.rb`
437
- Expected: PASS.
438
-
439
- - [ ] **Step 2: Refactor `destroy.rb`**
440
-
441
- In `lib/gempilot/cli/commands/destroy.rb`:
442
-
443
- Replace `prompt_for_path`:
444
-
445
- ```ruby
446
- def prompt_for_path(type)
447
- ask(colors.green(type), required: true)
448
- end
449
- ```
450
-
451
- Replace `dispatch_destroy`:
452
-
453
- ```ruby
454
- def dispatch_destroy(type, path)
455
- case type
456
- when "class" then destroy_class(gem_constant(path))
457
- when "module" then destroy_module(gem_constant(path))
458
- when "command" then destroy_command(path)
459
- else
460
- puts colors.red("Unknown type '#{type}'. Use class, module, or command.")
461
- exit 1
462
- end
463
- end
464
- ```
465
-
466
- Delete `test_path_for`. Replace `destroy_class` and `destroy_module`:
467
-
468
- ```ruby
469
- def destroy_class(constant)
470
- lib_path = constant.lib_path
471
- test_path = constant.test_path(@test_framework)
472
-
473
- remove_file(lib_path)
474
- remove_file(test_path)
475
- cleanup_empty_dirs(lib_path, test_path)
476
- end
477
-
478
- def destroy_module(constant)
479
- lib_path = constant.lib_path
480
-
481
- remove_file(lib_path)
482
- remove_empty_parents(File.dirname(lib_path), File.join("lib", @require_path))
483
- end
484
- ```
485
-
486
- Leave `destroy_command`, `cleanup_empty_dirs`, `remove_empty_parents`, `remove_file`, `print_remove`, and `print_skip` unchanged.
487
-
488
- - [ ] **Step 3: Run destroy specs + minitest**
489
-
490
- Run: `rspec spec/gempilot/cli/commands/destroy_namespace_spec.rb spec/gempilot/cli/commands/destroy_interactive_spec.rb`
491
- Expected: PASS.
492
-
493
- Run: `ruby -Itest -Ilib -e 'require File.expand_path("test/gempilot/cli/destroy_command_test.rb")'`
494
- Expected: PASS, 0 failures.
495
-
496
- - [ ] **Step 4: Lint and commit**
497
-
498
- ```bash
499
- rubocop lib/gempilot/cli/commands/destroy.rb
500
- git add lib/gempilot/cli/commands/destroy.rb
501
- git commit -m "Refactor destroy command onto GemConstant"
502
- ```
503
-
504
- ### Task B5: Remove the now-dead `parse_constant` and `validate_gem_root!`
505
-
506
- **Files:**
507
- - Modify: `lib/gempilot/cli/gem_context.rb`
508
-
509
- - [ ] **Step 1: Confirm they're unused**
510
-
511
- Run: `grep -rn "parse_constant\|validate_gem_root" lib`
512
- Expected: only the two definitions in `gem_context.rb` (no call sites remain after B3/B4).
513
-
514
- - [ ] **Step 2: Delete both methods**
515
-
516
- In `lib/gempilot/cli/gem_context.rb`, delete the entire `parse_constant` method and the entire `validate_gem_root!` method. Keep `using String::Inflectable` (still used by `detect_gem_context`'s `camelize`), `detect_gem_context`, and `gem_constant`.
517
-
518
- - [ ] **Step 3: Run the full suite + minitest**
519
-
520
- Run: `rspec`
521
- Expected: PASS (whole RSpec suite).
522
-
523
- Run: `ruby -Itest -Ilib -e 'Dir["test/**/*_test.rb"].each { |f| require File.expand_path(f) }'`
524
- Expected: only the known environmental `create_command_test` bundler-subprocess failure (see memory `gempilot-test-running-env`); 0 other failures.
525
-
526
- - [ ] **Step 4: Lint and commit**
527
-
528
- ```bash
529
- rubocop lib/gempilot/cli/gem_context.rb
530
- git add lib/gempilot/cli/gem_context.rb
531
- git commit -m "Drop parse_constant/validate_gem_root! superseded by GemConstant"
532
- ```
533
-
534
- ---
535
-
536
- ## Part C — Robust test-framework detection
537
-
538
- ### Task C1: Detect rspec by its config files, not a bare `spec/` directory
539
-
540
- **Files:**
541
- - Modify: `lib/gempilot/cli/gem_context.rb`
542
- - Test: `spec/gempilot/cli/commands/new_interactive_spec.rb`
543
-
544
- - [ ] **Step 1: Add a spec proving a stray `spec/` dir doesn't force rspec**
545
-
546
- In `spec/gempilot/cli/commands/new_interactive_spec.rb`, add inside `describe "interactive mode"`:
547
-
548
- ```ruby
549
- context "when a spec/ directory exists but there is no rspec config" do
550
- before { mkdir_p("spec") }
551
-
552
- it "still scaffolds a minitest test file" do
553
- generate("1\nServices::Auth\n")
554
-
555
- expect(File).to exist("test/my_gem/services/auth_test.rb")
556
- end
557
- end
558
- ```
559
-
560
- - [ ] **Step 2: Run it, verify it fails**
561
-
562
- Run: `rspec spec/gempilot/cli/commands/new_interactive_spec.rb -e "stray"` (or `-e "no rspec config"`)
563
- Expected: FAIL — current `File.directory?("spec")` returns true, so an rspec spec file is generated under `spec/` and the expected `test/...` file does not exist.
564
-
565
- - [ ] **Step 3: Replace the detection**
566
-
567
- In `lib/gempilot/cli/gem_context.rb`, change the last line of `detect_gem_context` from:
568
-
569
- ```ruby
570
- @test_framework = File.directory?("spec") ? :rspec : :minitest
571
- ```
572
- to:
573
- ```ruby
574
- @test_framework = detect_test_framework
575
- ```
576
-
577
- Add this private method (next to `gem_constant`):
578
-
579
- ```ruby
580
- # Detect rspec by its canonical config files rather than the mere
581
- # presence of a spec/ directory, which a minitest project may also have.
582
- def detect_test_framework
583
- return :rspec if File.exist?(".rspec") || File.exist?(File.join("spec", "spec_helper.rb"))
584
-
585
- :minitest
586
- end
587
- ```
588
-
589
- - [ ] **Step 4: Run the spec + full suite**
590
-
591
- Run: `rspec spec/gempilot/cli/commands/new_interactive_spec.rb`
592
- Expected: PASS.
593
-
594
- Run: `rspec`
595
- Expected: PASS (whole suite).
596
-
597
- - [ ] **Step 5: Lint and commit**
598
-
599
- ```bash
600
- rubocop lib/gempilot/cli/gem_context.rb spec/gempilot/cli/commands/new_interactive_spec.rb
601
- git add lib/gempilot/cli/gem_context.rb spec/gempilot/cli/commands/new_interactive_spec.rb
602
- git commit -m "Detect rspec by config files, not a bare spec/ dir"
603
- ```
604
-
605
- ---
606
-
607
- ## Part D — CLAUDE.md accuracy pass
608
-
609
- ### Task D1: Correct the stale architecture and feature notes
610
-
611
- **Files:**
612
- - Modify: `CLAUDE.md`
613
-
614
- - [ ] **Step 1: Fix the version-tasks note**
615
-
616
- In `CLAUDE.md`, under "Generated Gem Features", replace:
617
-
618
- ```
619
- - Version management rake tasks in `rakelib/version.rake` (current, bump, commit, revert, release:full)
620
- ```
621
- with:
622
- ```
623
- - Version lifecycle rake tasks installed via `Gempilot::VersionTask.new` (a `Rake::TaskLib`): `version:current/bump/commit/tag/untag/reset/revert`, composite `version:release`/`version:unrelease`, and `version:github:release/unrelease/list`
624
- ```
625
-
626
- - [ ] **Step 2: Fix the `exe/` note in the ISSUES section**
627
-
628
- In `CLAUDE.md`, update issue 1 to reflect reality — `exe/gempilot` keeps a *conditional* `bundler/setup` (only when a sibling `Gemfile` exists), it was not removed. Replace the issue body with:
629
-
630
- ```
631
- 1. RESOLVED — `exe/gempilot` previously had an unconditional `ENV["BUNDLE_GEMFILE"]` + `require "bundler/setup"`, breaking `gem install` usage. It now guards both behind `if File.exist?(gemfile)` (the Ronin pattern), so installed gems skip Bundler while in-repo runs still use it.
632
- ```
633
-
634
- - [ ] **Step 3: Note the constant model under Architecture**
635
-
636
- In `CLAUDE.md`, under "Architecture", add a bullet:
637
-
638
- ```
639
- - `GemConstant` value object (`lib/gempilot/gem_constant.rb`) owns constant→namespace/path resolution for `new`/`destroy`; constants are rooted at the gem module by construction
640
- ```
641
-
642
- - [ ] **Step 4: Commit**
643
-
644
- ```bash
645
- git add CLAUDE.md
646
- git commit -m "Refresh CLAUDE.md: version tasks, exe note, GemConstant"
647
- ```
648
-
649
- ---
650
-
651
- ## Final verification
652
-
653
- - [ ] **Full RSpec suite**
654
-
655
- Run: `rspec`
656
- Expected: PASS (all examples, including the new `gem_constant_spec` and added cases).
657
-
658
- - [ ] **Minitest suite**
659
-
660
- Run: `ruby -Itest -Ilib -e 'Dir["test/**/*_test.rb"].each { |f| require File.expand_path(f) }'`
661
- Expected: only the known environmental `create_command_test` bundler failure; 0 others.
662
-
663
- - [ ] **RuboCop (real source dirs)**
664
-
665
- Run: `rubocop lib spec test exe Rakefile`
666
- Expected: no offenses.
667
-
668
- - [ ] **Confirm net code reduction**
669
-
670
- Run: `git diff --stat master -- lib/gempilot/cli/commands/new.rb lib/gempilot/cli/commands/destroy.rb lib/gempilot/cli/gem_context.rb`
671
- Expected: `new.rb`/`destroy.rb`/`gem_context.rb` shrink (constant logic centralized in `gem_constant.rb`).
672
-
673
- ---
674
-
675
- ## Self-review notes (done while writing)
676
-
677
- - **Spec coverage:** each critique maps to a task — duplication + bug class → B1–B5; `abort`/`raise` → A1; magic string → A2; fragile framework detection → C1; CLAUDE.md drift → D1. The `validate_gem_root!` leniency is resolved as a documented design decision in B1.
678
- - **Type consistency:** `GemConstant#qualified/namespaces/name/lib_path/test_path(framework)` are used with the same names/signatures across B3/B4; the factory is `gem_constant(input)` everywhere.
679
- - **Behavior change called out:** B3 makes non-interactive bare names root under the gem (previously rejected). No existing test asserted the rejection; B3 Step 1 adds a test documenting the new behavior.