develoz-rails 0.1.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 (172) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +31 -0
  3. data/.github/workflows/release.yml +459 -0
  4. data/.rubocop.yml +52 -0
  5. data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
  6. data/.tool-versions +2 -0
  7. data/CHANGELOG.md +28 -0
  8. data/CONTRIBUTING.md +120 -0
  9. data/README.md +76 -0
  10. data/Rakefile +18 -0
  11. data/config/generators.yml +105 -0
  12. data/docs/guide/installation.md +574 -0
  13. data/exe/develoz +6 -0
  14. data/install +98 -0
  15. data/lib/develoz/canonical_fetcher.rb +56 -0
  16. data/lib/develoz/cli.rb +84 -0
  17. data/lib/develoz/generators/base.rb +134 -0
  18. data/lib/develoz/manifest.rb +44 -0
  19. data/lib/develoz/options.rb +55 -0
  20. data/lib/develoz/version.rb +5 -0
  21. data/lib/develoz/version_resolver.rb +103 -0
  22. data/lib/develoz.rb +15 -0
  23. data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
  24. data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
  25. data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
  26. data/lib/generators/develoz/admin/admin_generator.rb +44 -0
  27. data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
  28. data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
  29. data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
  30. data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
  31. data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
  32. data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
  33. data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
  34. data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
  35. data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
  36. data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
  37. data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
  38. data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
  39. data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
  40. data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
  41. data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
  42. data/lib/generators/develoz/api/api_generator.rb +55 -0
  43. data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
  44. data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
  45. data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
  46. data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
  47. data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
  48. data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
  49. data/lib/generators/develoz/auth/auth_generator.rb +82 -0
  50. data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
  51. data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
  52. data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
  53. data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
  54. data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
  55. data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
  56. data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
  57. data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
  58. data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
  59. data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
  60. data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
  61. data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
  62. data/lib/generators/develoz/ci/ci_generator.rb +66 -0
  63. data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
  64. data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
  65. data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
  66. data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
  67. data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
  68. data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
  69. data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
  70. data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
  71. data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
  72. data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
  73. data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
  74. data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
  75. data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
  76. data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
  77. data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
  78. data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
  79. data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
  80. data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
  81. data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
  82. data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
  83. data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
  84. data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
  85. data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
  86. data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
  87. data/lib/generators/develoz/database/database_generator.rb +44 -0
  88. data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
  89. data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
  90. data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
  91. data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
  92. data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
  93. data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
  94. data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
  95. data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
  96. data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
  97. data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
  98. data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
  99. data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
  100. data/lib/generators/develoz/docker/docker_generator.rb +54 -0
  101. data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
  102. data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
  103. data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
  104. data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
  105. data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
  106. data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
  107. data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
  108. data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
  109. data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
  110. data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
  111. data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
  112. data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
  113. data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
  114. data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
  115. data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
  116. data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
  117. data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
  118. data/lib/generators/develoz/install/install_generator.rb +51 -0
  119. data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
  120. data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
  121. data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
  122. data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
  123. data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
  124. data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
  125. data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
  126. data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
  127. data/lib/generators/develoz/push/push_generator.rb +61 -0
  128. data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
  129. data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
  130. data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
  131. data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
  132. data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
  133. data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
  134. data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
  135. data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
  136. data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
  137. data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
  138. data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
  139. data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
  140. data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
  141. data/lib/generators/develoz/solid/solid_generator.rb +52 -0
  142. data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
  143. data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
  144. data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
  145. data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
  146. data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
  147. data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
  148. data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
  149. data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
  150. data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
  151. data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
  152. data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
  153. data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
  154. data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
  155. data/lib/generators/develoz/testing/testing_generator.rb +39 -0
  156. data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
  157. data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
  158. data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
  159. data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
  160. data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
  161. data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
  162. data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
  163. data/lib/generators/develoz/ui/ui_generator.rb +57 -0
  164. data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
  165. data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
  166. data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
  167. data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
  168. data/lib/tasks/canonical.rake +27 -0
  169. data/templates/.keep +0 -0
  170. data/templates/CANONICAL_SOURCES.md +93 -0
  171. data/templates/pull_request_template.md.tt +13 -0
  172. metadata +383 -0
@@ -0,0 +1,855 @@
1
+ # Develoz Rails Generator Pattern - Learnings
2
+
3
+ ## Working Generator Pattern (Task 10 - ToolingGenerator)
4
+
5
+ ### Key Success Factors
6
+
7
+ 1. **Source Root Override**: Use `def self.source_root` to override the base class method
8
+ - Pattern: `File.expand_path("templates", __dir__)`
9
+ - This avoids the Thor `source_paths` error that caused infinite loops in prior attempts
10
+ - Templates must be co-located in `lib/generators/develoz/<generator_name>/templates/`
11
+
12
+ 2. **Per-File Templates**: Use `template` method for each file, NOT Thor's `directory` method
13
+ - `template "vscode/settings.json.tt", ".vscode/settings.json"`
14
+ - `template "env.example.tt", ".env.example"`
15
+ - This gives fine-grained control and avoids directory-level issues
16
+
17
+ 3. **Destination Root Binding in Specs**: CRITICAL for test isolation
18
+ - Always pass `destination_root: tmp_dir` when instantiating the generator
19
+ - Assert `expect(gen.destination_root).to eq(tmp_dir)` to verify binding
20
+ - Without this, the generator writes to the real repo and corrupts it
21
+
22
+ 4. **Calling Generator Methods**: Do NOT use `invoke_all` in tests
23
+ - Reason: Thor treats all public methods as tasks, including helper methods like `inject_once`
24
+ - Instead: Call specific methods directly: `gen.create_vscode; gen.create_env_files; ...`
25
+ - This avoids calling base class helper methods as tasks
26
+
27
+ 5. **Template Content**: Use ERB syntax for dynamic values
28
+ - `<%= app_name %>` works because base class defines `app_name` method
29
+ - Templates are processed through Rails' template engine
30
+
31
+ 6. **Idempotency**: Use base class helpers with built-in guards
32
+ - `add_gem` checks if gem already exists before adding
33
+ - `ensure_gitignore` checks if pattern already exists
34
+ - `create_file` with conditional check for `.env`
35
+ - These prevent duplicate entries on re-runs
36
+
37
+ ### File Structure
38
+
39
+ ```
40
+ lib/generators/develoz/tooling/
41
+ ├── tooling_generator.rb
42
+ └── templates/
43
+ ├── vscode/
44
+ │ ├── settings.json.tt
45
+ │ ├── extensions.json.tt
46
+ │ └── tasks.json.tt
47
+ ├── env.example.tt
48
+ └── constants.rb.tt
49
+
50
+ spec/develoz/generators/
51
+ └── tooling_generator_spec.rb
52
+ ```
53
+
54
+ ### Generator Methods Pattern
55
+
56
+ ```ruby
57
+ class ToolingGenerator < Develoz::Generators::Base
58
+ def self.source_root
59
+ File.expand_path("templates", __dir__)
60
+ end
61
+
62
+ def create_vscode
63
+ template "vscode/settings.json.tt", ".vscode/settings.json"
64
+ # ... more templates
65
+ end
66
+
67
+ def create_env_files
68
+ template "env.example.tt", ".env.example"
69
+ create_file ".env", "" unless File.exist?(File.join(destination_root, ".env"))
70
+ ensure_gitignore(".env")
71
+ end
72
+
73
+ def add_dotenv
74
+ add_gem "dotenv-rails", group: %i[development test]
75
+ end
76
+ end
77
+ ```
78
+
79
+ ### Spec Pattern
80
+
81
+ - Use `with_tmp_dir` helper to create isolated temp directories
82
+ - Seed minimal Gemfile and .gitignore for base class helpers to work
83
+ - Call generator methods directly, not `invoke_all`
84
+ - Keep expectations to 3 per test (RuboCop rule)
85
+ - Avoid instance variables; use local variables and method parameters
86
+
87
+ ### Coverage Requirements
88
+
89
+ - Generator code: 100% line + branch coverage
90
+ - Full suite: 100% line + branch coverage (includes all lib files)
91
+ - Tests must reach all branches (e.g., both `.env` exists/not-exists paths)
92
+
93
+ ## Reuse for T11-T31
94
+
95
+ This pattern is stable and should be reused for all subsequent generators:
96
+ 1. Create generator in `lib/generators/develoz/<name>/<name>_generator.rb`
97
+ 2. Create templates in `lib/generators/develoz/<name>/templates/`
98
+ 3. Override `source_root` with `File.expand_path("templates", __dir__)`
99
+ 4. Use per-file `template` calls
100
+ 5. Create spec in `spec/develoz/generators/<name>_generator_spec.rb`
101
+ 6. Bind `destination_root` in all spec instantiations
102
+ 7. Call methods directly, not `invoke_all`
103
+ 8. Ensure 100% coverage with focused tests (max 3 expectations each)
104
+
105
+ ## SimpleCov + Parallel Test Collation (Task 11 - TestingGenerator)
106
+
107
+ The generated app's test setup uses SimpleCov with parallel_tests collation:
108
+
109
+ 1. **SimpleCov Configuration** (spec/spec_helper.rb):
110
+ - `enable_coverage :branch` for branch coverage tracking
111
+ - `minimum_coverage line: 100, branch: 100` enforces 100% coverage
112
+ - `coverage_dir "public/coverage"` outputs to public/coverage (not repo root)
113
+ - `SimpleCov.command_name "RSpec_#{ENV['TEST_ENV_NUMBER']}"` tags each parallel process
114
+ - `SimpleCov.use_merging true` merges coverage across parallel processes
115
+ - `add_filter %w[/spec/ /config/ /db/]` excludes test/config/db files from coverage
116
+
117
+ 2. **Parallel Test Collation**:
118
+ - When running `parallel_tests`, each process gets a unique `TEST_ENV_NUMBER`
119
+ - SimpleCov uses this to create separate coverage reports per process
120
+ - The `use_merging true` flag combines all reports into a single result
121
+ - This allows 100% coverage enforcement even with parallel test execution
122
+
123
+ 3. **Capybara + Selenium Configuration** (spec/rails_helper.rb):
124
+ - Registers `:headless_chrome` driver with Selenium WebDriver
125
+ - Chrome options: `--headless=new`, `--no-sandbox`, `--disable-gpu`
126
+ - Sets `Capybara.javascript_driver = :headless_chrome` for JS tests
127
+ - FactoryBot configured to load factories from `spec/factories`
128
+ - `ActiveRecord::Migration.maintain_test_schema!` keeps test DB in sync
129
+
130
+ 4. **RSpec Configuration**:
131
+ - `.rspec` and `.rspec_parallel` both use `--require spec_helper --format documentation`
132
+ - `spec/spec_helper.rb` loaded first (SimpleCov setup)
133
+ - `spec/rails_helper.rb` requires spec_helper, then adds Rails-specific config
134
+ - Support files auto-loaded from `spec/support/**/*.rb`
135
+
136
+ This approach is used by T20 (database setup) and T32 (CI/parallel execution).
137
+
138
+ ## Solid Queue/Cache/Cable Stack (Task 12 - SolidGenerator)
139
+
140
+ The solid generator adds Solid Queue, Solid Cache, Solid Cable, and Mission Control Jobs to a Rails app.
141
+
142
+ ### Canonical Sources
143
+
144
+ The generator sources configuration files from the ams2stats repo:
145
+ - **Verbatim files**: `config/queue.yml`, `config/cache.yml`, `config/recurring.yml`, `app/jobs/application_job.rb` are copied directly from ams2stats (SQLite-based).
146
+ - **Adapted files**: `config/cable.yml` is adapted from ams2stats (changes Redis URL handling for production). `config/initializers/mission_control.rb` is verbatim.
147
+ - **Generated files**: `config/initializers/solid.rb` is authored (sets queue_adapter, cache_store, and cable config).
148
+
149
+ ### Key Differences from ams2stats
150
+
151
+ ams2stats uses SQLite and runs on Windows without `fork()`. The develoz-rails template targets Postgres with multi-database support (T14 will define named connections `queue`, `cache`, `cable`). The YAML files are structured identically but will reference those named connections in production.
152
+
153
+ ### Generator Methods
154
+
155
+ 1. `add_solid_gems` - adds solid_queue, solid_cache, solid_cable, mission_control-jobs
156
+ 2. `create_queue_config` - generates config/queue.yml with dispatcher/worker config
157
+ 3. `create_cache_config` - generates config/cache.yml with store options
158
+ 4. `create_cable_config` - generates config/cable.yml with async/test/redis adapters
159
+ 5. `create_recurring_config` - generates config/recurring.yml with cleanup task
160
+ 6. `create_application_job` - generates app/jobs/application_job.rb base class
161
+ 7. `create_mission_control_initializer` - generates config/initializers/mission_control.rb with HTTP basic auth
162
+ 8. `create_solid_initializer` - generates config/initializers/solid.rb with Rails config
163
+ 9. `insert_mission_control_route` - idempotently mounts MissionControl::Jobs::Engine at /jobs
164
+
165
+ ### Idempotency
166
+
167
+ All methods use base class helpers (add_gem, insert_route, template) which guard against duplicates:
168
+ - `add_gem` checks if gem already exists in Gemfile
169
+ - `insert_route` checks if route already exists in routes.rb
170
+ - `template` overwrites files (idempotent by nature)
171
+
172
+ ### Spec Coverage
173
+
174
+ The spec file has 40+ tests covering:
175
+ - Gem additions (4 tests)
176
+ - Config file generation (queue, cache, cable, recurring)
177
+ - Initializer generation (mission_control, solid)
178
+ - Route insertion
179
+ - Idempotency (run twice, verify single entries)
180
+ - Content validation (structure, env vars, frozen_string_literal)
181
+
182
+ ## CI Generator (Task 13)
183
+
184
+ ### Pattern
185
+
186
+ The ci generator follows the same pattern as tooling/testing/solid generators:
187
+ 1. Generator class at `lib/generators/develoz/ci/ci_generator.rb`
188
+ 2. Templates co-located in `lib/generators/develoz/ci/templates/`
189
+ 3. Spec at `spec/develoz/generators/ci_generator_spec.rb`
190
+
191
+ ### Key Files Generated
192
+
193
+ - **bin/ci** - CI entry point (`require "active_support/continuous_integration"`, loads `config/ci.rb`)
194
+ - **config/ci.rb** - Step definitions using `CI.run do ... end` with setup, style (Ruby/JS/YAML/Markdown), security, and test steps
195
+ - **Linter configs**: `.rubocop.yml`, `.reek.yml`, `biome.json`, `.stylelintrc.json`, `.haml-lint.yml`, `.markdownlint.json`, `.yamllint`
196
+ - **Workflow**: `.github/workflows/ci.yml` (simple workflow running `bin/ci`)
197
+
198
+ ### Gem Additions
199
+
200
+ All added to `[:development, :test]` group:
201
+ - `rubocop-rails-omakase` (RuboCop with Rails defaults)
202
+ - `reek` (code smell detector)
203
+ - `flay` (code duplication analysis)
204
+ - `brakeman` (security static analysis)
205
+ - `bundler-audit` (gem vulnerability check)
206
+ - `haml_lint` (Haml template linting, require: false)
207
+
208
+ ### Canonical Sources
209
+
210
+ Linter configs are adapted from `LooperInsights/looper_core` (RuboCop, Reek, Biome, Stylelint, haml-lint, markdownlint, yamllint). `bin/ci` and `config/ci.rb` are adapted from `mauriciozaffari/ams2stats`. The CI workflow is authored.
211
+
212
+ ### Spec Structure
213
+
214
+ 55+ tests covering:
215
+ - 6 gem addition tests (one per gem)
216
+ - 7 gem idempotency tests (combined and individual)
217
+ - File existence for all 10 generated files
218
+ - Content validation for each file (structure, key settings)
219
+ - Idempotency for all file types
220
+ - 100% line + branch coverage verified
221
+
222
+ ## Database Generator (Task 14)
223
+
224
+ The database generator adds PostgreSQL and pg_search gems, generates database.yml with Postgres 18+ config, creates a pg_search initializer, and ensures .tool-versions includes postgres 18.
225
+
226
+ ### Key Decisions
227
+
228
+ 1. **database.yml structure**: Uses named connections (primary, queue, cache, cable) for all environments. Production uses DATABASE_URL pattern for the primary connection. Test env uses TEST_ENV_NUMBER suffix for parallel_tests.
229
+
230
+ 2. **pg_search initializer**: Minimal - just a comment noting that PgSearch is included via the searchable concern (T15). No global configuration needed at this stage.
231
+
232
+ 3. **.tool-versions handling**: Uses `inject_once` when the file exists (idempotent append), `create_file` when it doesn't. Both branches tested for 100% coverage.
233
+
234
+ ### Generator Methods
235
+
236
+ 1. `add_database_gems` - adds pg and pg_search gems
237
+ 2. `create_database_config` - generates config/database.yml with Postgres config
238
+ 3. `create_pg_search_initializer` - generates config/initializers/pg_search.rb
239
+ 4. `ensure_postgres_tool_version` - ensures .tool-versions has postgres 18
240
+
241
+ ### Idempotency
242
+
243
+ - `add_gem` guards against duplicate gem declarations
244
+ - `template` overwrites files (idempotent by nature)
245
+ - `inject_once` guards against duplicate content in .tool-versions
246
+ - `create_file` only called when .tool-versions doesn't exist
247
+
248
+ ### Testing ENV.fetch Patterns
249
+
250
+ When testing templates that use `ENV.fetch`, the ERB is processed at generation time. To verify the pattern works, set the env var before running the generator and assert the evaluated value appears in the output. Use `begin/ensure` to restore the env var after the test.
251
+
252
+ ## Concerns Generator (Task 15)
253
+
254
+ The concerns generator copies 4 concern files from `LooperInsights/looper_core` verbatim, plus 2 migrations and 4 app-level spec templates.
255
+
256
+ ### Canonical Sources
257
+
258
+ - **Verbatim concerns**: `searchable_concern.rb` (PgSearch), `optimized_finders.rb` (in-memory finders), `transitionable.rb` (state machine + JSONB history), `configurable.rb` (polymorphic config + global fallback). None reference an app name, so no ERB substitution is needed — the `.tt` files are byte-identical to the fixtures.
259
+ - **Migrations**: `add_status_transitions.rb.tt` is adapted from looper_core's `add_status_columns_to_merchandizing_pages` migration (generic `:records` table name). `create_configurations.rb.tt` is adapted from looper_core's `create_configurations` migration (UUID primary keys with pgcrypto, polymorphic columns, concurrent indexes).
260
+ - **App-level specs**: Authored templates modeled on looper_core's own specs. Each spec defines its own test models using `stub_const` and `ActiveRecord::Schema.define` with `before(:all)`/`after(:all)` to create/drop temporary tables. This makes them self-contained and runnable in any generated Rails app.
261
+
262
+ ### Transitionable Schema
263
+
264
+ The transitionable concern uses a `status` string column (enum) and a `status_transitions` JSONB column (default: `[]`) on the model itself — NOT a separate transitions table. Each entry is `{ "to_state" => "...", "created_at" => "<ISO8601>", "metadata" => {...} }`.
265
+
266
+ ### Configurable Schema
267
+
268
+ The configurable concern uses a polymorphic `configurations` table with UUID primary keys, `configurable_type`/`configurable_id` columns, `key`/`value` (jsonb), and `active` boolean. Global configurations have `configurable_type` and `configurable_id` set to nil.
269
+
270
+ ### RuboCop Notes
271
+
272
+ - `RSpec/DescribeClass` allows string describes that match `/^(?:(?:::)?[A-Z]\w*)+$/` — so `"Fidelity"` passes but `"Concerns Fidelity"` (with a space) does not. Use `"ConcernsFidelity"` instead.
273
+ - `Style/ExplicitBlockArgument` triggers when a helper method just wraps another method with `yield` and no other code. Use `# rubocop:disable`/`# rubocop:enable` or add content inside the block.
274
+
275
+ ### Testing Notes
276
+
277
+ - Concern logic is validated in the generated app (T32), not the gem suite. The gem suite only tests the generator code (file creation, idempotency, content checks).
278
+ - Fidelity specs render each `.tt` template with ERB and compare to the canonical fixture using `match_canonical`. Since the concern templates are verbatim (no ERB), the rendered output equals the fixture byte-for-byte.
279
+
280
+ ## Strict Loading Generator (Task 16)
281
+
282
+ The strict_loading generator creates a single initializer `config/initializers/strict_loading.rb` that configures ActiveRecord strict loading without editing `config/application.rb`.
283
+
284
+ ### Key Decisions
285
+
286
+ 1. **Initializer over application.rb injection**: The task explicitly preferred the initializer approach to avoid fragile `inject_once` into `config/application.rb`. A single `config/initializers/strict_loading.rb.tt` template sets all three config values in one file.
287
+
288
+ 2. **Per-env violation via case statement**: Instead of generating separate env-specific initializers or editing `config/environments/*.rb`, the initializer uses a `case Rails.env` block inside `Rails.application.configure do`. Test env raises (`:raise`); all other envs (development, production) log (`:log`). This keeps everything in one file and is evaluated at boot time per environment.
289
+
290
+ 3. **Config values set**: `strict_loading_by_default = true`, `strict_loading_mode = :n_plus_one_only`, `strict_loading_violation` = `:raise` (test) / `:log` (dev, prod).
291
+
292
+ ### Generator Methods
293
+
294
+ 1. `create_strict_loading_initializer` - generates `config/initializers/strict_loading.rb` from template
295
+
296
+ ### Idempotency
297
+
298
+ The `template` method overwrites the file on each run, making it idempotent by nature. No `inject_once` needed since no existing files are modified.
299
+
300
+ ### Spec Coverage
301
+
302
+ 9 tests covering:
303
+ - destination_root binding
304
+ - File existence
305
+ - frozen_string_literal comment
306
+ - strict_loading_by_default = true
307
+ - strict_loading_mode = :n_plus_one_only
308
+ - :raise in test env
309
+ - :log in dev/prod
310
+ - strict_loading_violation config key present
311
+ - Idempotency (run twice, verify identical output)
312
+
313
+ ## Maintenance Generator (Task 17)
314
+
315
+ The maintenance generator adds the `maintenance_tasks` gem (Shopify), mounts the engine at `/maintenance_tasks`, and templates an example task plus a rake task.
316
+
317
+ ### Key Decisions
318
+
319
+ 1. **Example task structure**: Based on the maintenance_tasks gem API (`MaintenanceTasks::Task` with `collection`, `count`, `process`). The example uses `User.in_batches(of: 1000)` for collection, `User.count` for count, and `batch.update_all(updated_at: Time.current)` for process. This mirrors the real task patterns in looper_core (e.g. `BackfillMatchHasScreenshotTask`).
320
+
321
+ 2. **Rake task pattern**: Authored a minimal `maintenance_counters.rake` with two tasks: `update_counters` (eager loads models, iterates descendants) and `purge_stale` (RETENTION_DAYS env var, cutoff date). Modeled on looper_core's `counters.rake` and `data_retention.rake` but kept minimal and illustrative.
322
+
323
+ 3. **Route mounting**: Uses `insert_route` from base class with `mount MaintenanceTasks::Engine, at: "/maintenance_tasks"`. Idempotent via the base class's content-inclusion guard.
324
+
325
+ ### Generator Methods
326
+
327
+ 1. `add_maintenance_tasks_gem` - adds `maintenance_tasks` gem via `add_gem`
328
+ 2. `insert_maintenance_tasks_route` - mounts engine at `/maintenance_tasks` via `insert_route`
329
+ 3. `create_example_task` - generates `app/tasks/maintenance/example_task.rb` from template
330
+ 4. `create_rake_task` - generates `lib/tasks/maintenance_counters.rake` from template
331
+
332
+ ### Spec Coverage
333
+
334
+ 13 tests covering:
335
+ - destination_root binding
336
+ - Gem addition
337
+ - Route insertion
338
+ - Example task file existence + structure (class, collection, process)
339
+ - frozen_string_literal on both generated files
340
+ - Rake task file existence + namespace/tasks
341
+ - Idempotency for gem, route, example task, and rake task (4 tests)
342
+
343
+ ### maintenance_tasks Gem API
344
+
345
+ The gem requires tasks to be in `app/tasks/maintenance/` and subclass `MaintenanceTasks::Task`. Required methods: `collection` (returns AR Relation, BatchEnumerator, or Array) and `process(item)`. Optional: `count` for progress reporting. The gem provides a web UI at the mount point for queuing, pausing, and monitoring tasks.
346
+
347
+ ## Frontend Core Generator (Task 18)
348
+
349
+ The frontend_core generator adds importmap-rails, pagy (conditionally), and annotaterb, plus their configuration files.
350
+
351
+ ### Key Decisions
352
+
353
+ 1. **class_option for skip_pagy**: Uses `class_option :skip_pagy, type: :boolean, default: false` to allow opting out of Pagy. In specs, pass `"skip_pagy" => true` as the options hash (second argument to `.new`). Thor does NOT apply class_option defaults when instantiating directly in tests, so `options[:skip_pagy]` is `nil` (falsy) by default and `true` when explicitly set. This works correctly with `return if options[:skip_pagy]` since `nil` is falsy.
354
+
355
+ 2. **Importmap config**: Based on inscripto-v2's `config/importmap.rb` (fetched via `gh api`). Trimmed to a sensible default for a new Rails app: application pin, Hotwire (turbo-rails, stimulus, stimulus-loading), Trix/actiontext, activestorage, and `pin_all_from` for app/javascript/controllers.
356
+
357
+ 3. **Pagy initializer**: Based on looper_core's pagy initializer but trimmed to only the 4 required extras (arel, array, countless, bootstrap) plus `Pagy::DEFAULT[:limit] = 25` and `Pagy::DEFAULT.freeze`. The looper_core version has many more extras commented out; we keep it minimal.
358
+
359
+ 4. **Annotaterb config (models-only)**: The `.annotaterb.yml` uses symbol keys (e.g. `:models: true`) matching the YAML dump format from `AnnotateRb::ConfigGenerator.default_config_yml`. Models-only means: `models: true`, `routes: false`, `active_admin: false`, and all `exclude_*` keys set to `true` (tests, fixtures, factories, serializers, controllers, helpers, scaffolds). The `additional_file_patterns: []` key is also included.
360
+
361
+ ### Generator Methods
362
+
363
+ 1. `add_frontend_gems` - adds importmap-rails and annotaterb (group: :development)
364
+ 2. `add_pagy_gem` - adds pagy gem (skipped when `options[:skip_pagy]`)
365
+ 3. `create_importmap_config` - generates config/importmap.rb from template
366
+ 4. `create_pagy_initializer` - generates config/initializers/pagy.rb (skipped when `options[:skip_pagy]`)
367
+ 5. `create_annotaterb_config` - generates .annotaterb.yml from template
368
+
369
+ ### Spec Coverage (Both Branches)
370
+
371
+ 22 tests covering:
372
+ - destination_root binding
373
+ - Gem additions: importmap-rails, annotaterb (with development group), pagy (default), pagy absent with skip_pagy
374
+ - Importmap: file existence, pin content, frozen_string_literal
375
+ - Pagy: file existence (default), absent with skip_pagy, backend extras (arel/array/countless), bootstrap extra, frozen_string_literal
376
+ - Annotaterb: file existence, models-only config (models true, routes false, active_admin false, exclude_* true), generated with skip_pagy too
377
+ - Importmap generated with skip_pagy too
378
+ - Idempotency: gems, importmap, pagy initializer, annotaterb config (4 tests)
379
+
380
+ ### Annotaterb Configuration Keys
381
+
382
+ From `lib/annotate_rb/options.rb` in the annotaterb gem:
383
+ - `FLAG_OPTIONS`: boolean keys like `exclude_tests`, `exclude_fixtures`, `exclude_factories`, `exclude_serializers`, `exclude_controllers`, `exclude_helpers`, `exclude_scaffolds`, `classified_sort`, `show_indexes`, `show_foreign_keys`
384
+ - `OTHER_OPTIONS`: `active_admin`, `models`, `routes`, `additional_file_patterns`
385
+ - `POSITION_OPTIONS`: `position` (nil/before/top/after/bottom)
386
+
387
+ The YAML uses symbol-prefixed keys (`:models: true`) because `YAML.dump` on a hash with symbol keys produces this format.
388
+
389
+ ## Docs Render Generator (Task 19)
390
+
391
+ The docs_render generator adds Redcarpet (Markdown to HTML), Rouge (syntax highlighting), and Mermaid (client-side diagram rendering) to a Rails app, along with a DocsController and Document model for serving markdown documentation.
392
+
393
+ ### Canonical Sources
394
+
395
+ - **Verbatim files**: `app/models/document.rb` (Document model with inline Redcarpet::Render::HTML subclass and Rouge syntax highlighting), `app/javascript/docs.js` (Mermaid client-side rendering), `app/assets/stylesheets/documentation.scss` (docs styling with dark mode support). All three are byte-identical to looper_core fixtures.
396
+ - **Adapted files**: `app/controllers/docs_controller.rb` is adapted from looper_core (changed `AdminController` to `ApplicationController` for generic use). `app/views/docs/show.html.erb.tt` is adapted from looper_core's `show.html.haml` (converted HAML to ERB, removed app-specific helpers like `content_header` and `shared/box` partial).
397
+ - **Authored files**: `config/initializers/redcarpet_rouge.rb` is authored (looper_core has no separate redcarpet/rouge initializer; the Document model handles all configuration inline).
398
+
399
+ ### ERB-in-ERB Escaping for View Templates
400
+
401
+ The `show.html.erb.tt` template contains app-level ERB that must be preserved literally in the generated `.html.erb` file. Use `<%%` (double percent) to escape `<%` so Thor's template engine outputs literal `<%` instead of evaluating it at generation time:
402
+
403
+ ```
404
+ <%% content_for :head do %>
405
+ <%%= javascript_include_tag 'docs', 'data-turbo-track': 'reload' %>
406
+ <%% end %>
407
+ ```
408
+
409
+ This produces the correct ERB output in the generated `show.html.erb`:
410
+ ```erb
411
+ <% content_for :head do %>
412
+ <%= javascript_include_tag 'docs', 'data-turbo-track': 'reload' %>
413
+ <% end %>
414
+ ```
415
+
416
+ ### Document Model Architecture
417
+
418
+ The Document model is a plain Ruby class (includes `ActiveModel::Model`) that:
419
+ 1. Reads markdown files from `docs/` directory via `Rails.root.glob`
420
+ 2. Uses a custom `Redcarpet::Render::HTML` subclass (`Document::Renderer`) with:
421
+ - Obsidian wikilink support (`[[Note]]`, `[[Note#Heading]]`, `[[Note|Alias]]`)
422
+ - Callout blocks (`> [!NOTE]`, `> [!TIP]`, etc.)
423
+ - Mermaid diagram detection (language == 'mermaid' emits `<pre class="mermaid">` instead of syntax highlighting)
424
+ - Rouge syntax highlighting with Github dark theme
425
+ 3. Sanitizes HTML output with extended allowed tags/attributes
426
+
427
+ ### Route Pattern
428
+
429
+ The route `get "docs(/*id)" => "docs#show"` uses a glob parameter to match nested paths like `/docs`, `/docs/README`, `/docs/platform-guide/AUDITS`. The `(*id)` syntax makes the parameter optional.
430
+
431
+ ### Generator Methods
432
+
433
+ 1. `add_docs_gems` - adds redcarpet and rouge gems
434
+ 2. `create_docs_controller` - generates app/controllers/docs_controller.rb
435
+ 3. `create_document_model` - generates app/models/document.rb (verbatim)
436
+ 4. `create_docs_views` - generates app/views/docs/show.html.erb (adapted from HAML)
437
+ 5. `create_docs_javascript` - generates app/javascript/docs.js (verbatim)
438
+ 6. `create_docs_stylesheet` - generates app/assets/stylesheets/documentation.scss (verbatim)
439
+ 7. `create_redcarpet_rouge_initializer` - generates config/initializers/redcarpet_rouge.rb (authored)
440
+ 8. `insert_docs_route` - inserts `get "docs(/*id)" => "docs#show"` route
441
+
442
+ ### Spec Coverage
443
+
444
+ 28 tests covering:
445
+ - destination_root binding
446
+ - Gem additions (redcarpet, rouge)
447
+ - Controller existence, inheritance, frozen_string_literal
448
+ - Document model existence, Redcarpet/Rouge references, frozen_string_literal
449
+ - View existence, document render, javascript include
450
+ - JS existence, mermaid import
451
+ - Stylesheet existence, documentation section
452
+ - Initializer existence, Redcarpet/Rouge mentions, frozen_string_literal
453
+ - Route insertion
454
+ - Idempotency for gems, route, controller, model, view, JS, stylesheet, initializer (8 tests)
455
+
456
+ ### Fidelity Specs
457
+
458
+ 3 fidelity specs for verbatim files:
459
+ - `document.rb` matches canonical fixture
460
+ - `docs.js` matches canonical fixture
461
+ - `documentation.scss` matches canonical fixture
462
+
463
+ ## Doc Specs Generator (Task 20)
464
+
465
+ The doc_specs generator creates a self-documenting system spec pipeline: `bin/generate-docs` (parses `@category`/`@order` doc-comments + `doc_screenshot` calls from system specs to produce markdown + screenshots), a `doc_screenshot` Capybara helper, a `docs:check` rake task, and an example annotated system spec.
466
+
467
+ ### Canonical Sources
468
+
469
+ - **Verbatim file**: `bin/generate-docs` is copied byte-for-byte from `LooperInsights/looper_core/bin/generate-docs`. It is a Ruby script (shebang `#!/usr/bin/env ruby`), not bash as the task description stated. It is excluded from SimpleCov coverage via the `/templates/` filter. Fidelity spec verifies the `.tt` template renders identically to the fixture.
470
+ - **Authored file**: `spec/support/doc_screenshot_helper.rb` is extracted from looper_core's `spec/rails_helper.rb` where `doc_screenshot` was defined as an inline anonymous module (`config.include Module.new { def doc_screenshot... }`). Authored as a standalone `DocScreenshotHelper` module with `RSpec.configure` to include it for `type: :system`.
471
+ - **Adapted file**: `lib/tasks/docs_check.rake` is extracted from looper_core's `Rakefile` where the `docs:check` namespace was defined inline. Adapted into a standalone rake file.
472
+ - **Authored file**: `spec/system/example_doc_spec.rb` demonstrates the `@category`, `@order`, and `doc_screenshot` pattern on a fresh app.
473
+
474
+ ### Generator Methods
475
+
476
+ 1. `create_generate_docs_script` - templates `bin/generate-docs` and calls `File.chmod(0o755, dest)` to make it executable
477
+ 2. `create_doc_screenshot_helper` - templates `spec/support/doc_screenshot_helper.rb`
478
+ 3. `create_docs_check_rake_task` - templates `lib/tasks/docs_check.rake`
479
+ 4. `create_example_system_spec` - templates `spec/system/example_doc_spec.rb`
480
+
481
+ ### Key Decisions
482
+
483
+ 1. **No `File.exist?` guard on chmod**: Initially added `File.chmod(0o755, dest) if File.exist?(dest)` as defensive code, but this created an uncovered branch (the false path can't be reached since `template` always creates the file). Removed the guard for 100% branch coverage. The `template` method always creates the destination file, so the guard was unnecessary.
484
+
485
+ 2. **ERB safety of bin/generate-docs**: The script contains `#{...}` Ruby string interpolations and `<<~` heredocs, but no `<%` or `<%=` sequences, so it is safe to process through Thor's ERB template engine as a `.tt` file.
486
+
487
+ 3. **doc_screenshot helper location**: Placed in `spec/support/` (not `app/helpers/`) because it is a test-only helper included via `RSpec.configure` for `type: :system` specs, matching looper_core's pattern.
488
+
489
+ ### Spec Coverage
490
+
491
+ 22 tests covering:
492
+ - destination_root binding
493
+ - bin/generate-docs: existence, executable bit, ruby shebang, frozen_string_literal
494
+ - doc_screenshot_helper: existence, doc_screenshot method, frozen_string_literal, RSpec configure
495
+ - docs_check.rake: existence, namespace + task, frozen_string_literal
496
+ - example_doc_spec: existence, @category, @order, doc_screenshot call, frozen_string_literal
497
+ - Idempotency: bin/generate-docs, doc_screenshot_helper, docs_check.rake, example_doc_spec, executable bit preserved (5 tests)
498
+
499
+ ### Fidelity Specs
500
+
501
+ 1 fidelity spec for verbatim file:
502
+ - `bin/generate-docs` matches canonical fixture
503
+
504
+ ## Agents Docs Generator (Task 21)
505
+
506
+ The agents_docs generator creates root `AGENTS.md`, three adapted docs (`docs/development.md`, `docs/testing.md`, `docs/performance.md`), a verbatim PR template, and Faraday + VCR/WebMock support files with an example API spec.
507
+
508
+ ### Canonical Sources
509
+
510
+ - **Adapted files**: `AGENTS.md`, `docs/development.md`, `docs/testing.md`, `docs/performance.md` are adapted from `develoz-com/agent`. The source AGENTS.md is titled "# CLAUDE.md" but the adapted version uses "# AGENTS.md". All `bin/run` and `bin/develoz_agent` references are replaced with standard `bin/rails` / `bundle exec` commands. Agent-specific sections (develoz_agent CLI, ticket-state-machine docs, Jira API caching, opencode/git worktree stubs, database-backups container) are removed. Generic patterns (Docker-first, coverage requirements, N+1 detection, query patterns, caching) are retained.
511
+ - **Verbatim file**: `.github/pull_request_template.md` is copied byte-for-byte from `develoz-com/inscripto-v2`. The fixture already exists at `spec/fixtures/canonical/develoz-com-inscripto-v2/.github/pull_request_template.md` (from T6 demo), so no new fixture was needed. A new fidelity spec (`spec/fidelity/agents_docs_fidelity_spec.rb`) verifies the agents_docs template matches the same fixture.
512
+ - **Authored files**: `spec/support/vcr.rb.tt` (VCR config with WebMock), `spec/support/faraday.rb.tt` (FaradayFactory module with retry middleware), `spec/requests/example_api_spec.rb.tt` (demonstrates VCR + Faraday pattern), `spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt` (stub VCR cassette fixture).
513
+
514
+ ### Generator Methods
515
+
516
+ 1. `add_api_gems` - adds faraday, faraday-retry (default group); vcr, webmock (test group)
517
+ 2. `create_agents_md` - templates AGENTS.md
518
+ 3. `create_docs` - templates docs/development.md, docs/testing.md, docs/performance.md
519
+ 4. `create_pr_template` - templates .github/pull_request_template.md
520
+ 5. `create_vcr_support` - templates spec/support/vcr.rb
521
+ 6. `create_faraday_support` - templates spec/support/faraday.rb
522
+ 7. `create_example_api_spec` - templates spec/requests/example_api_spec.rb + stub cassette
523
+
524
+ ### Gem Idempotency Test Pitfall
525
+
526
+ When testing gem idempotency for `faraday` and `faraday-retry`, `gemfile.scan("faraday")` matches both gems (2 occurrences). Use a regex anchored to the gem declaration: `gemfile.scan(/^\s*gem\s+["']faraday["']/m).length` to count only exact `gem "faraday"` lines, not `gem "faraday-retry"`.
527
+
528
+ ### VCR Cassette as Template
529
+
530
+ The stub VCR cassette is a YAML file with no ERB content, but it is processed through Thor's `template` method (with `.tt` extension) for consistency with the generator pattern. Since the YAML contains no `<%` sequences, ERB processing is a no-op and the output is identical to the source.
531
+
532
+ ### Spec Coverage
533
+
534
+ 39 tests covering:
535
+ - destination_root binding
536
+ - 4 gem additions (faraday, faraday-retry, vcr, webmock)
537
+ - AGENTS.md existence, content (Quality Expectations, docs references)
538
+ - 3 docs existence + content (development Command Reference, testing coverage requirement, performance N+1 Detection)
539
+ - PR template existence + content (Changes section)
540
+ - vcr.rb existence, frozen_string_literal, cassette_library_dir, hook_into :webmock
541
+ - faraday.rb existence, frozen_string_literal, FaradayFactory module, :retry middleware
542
+ - example spec existence, frozen_string_literal, :vcr metadata, FaradayFactory usage
543
+ - cassette existence, http_interactions content
544
+ - 7 idempotency tests (gems, AGENTS.md, docs, PR template, vcr.rb, faraday.rb, example spec, cassette)
545
+
546
+ ### Fidelity Specs
547
+
548
+ 1 fidelity spec for verbatim file:
549
+ - `.github/pull_request_template.md` matches canonical fixture (reuses existing inscripto-v2 fixture)
550
+
551
+ ## Versioning Generator (Task 36)
552
+
553
+ The versioning generator replicates inscripto-v2's app versioning scheme: an `APP_VERSION` constant, an `app_version` helper, and a reusable display partial.
554
+
555
+ ### Canonical Sources
556
+
557
+ - **Verbatim (method snippet)**: The `app_version` method (`def app_version; APP_VERSION; end`) is copied verbatim from inscripto-v2's `app/helpers/application_helper.rb`. Stored as fixture `spec/fixtures/canonical/develoz-com-inscripto-v2/app/helpers/app_version.rb`. Fidelity spec verifies the rendered `application_helper.rb.tt` template includes the fixture content.
558
+ - **Adapted (constant injection)**: The `APP_VERSION = ENV.fetch("APP_VERSION", "dev")` line is adapted from inscripto-v2's `config/constants.rb`. Instead of creating a separate file, it is injected into the app's existing `config/initializers/constants.rb` (created by T10's tooling generator) via `inject_once` after the `# additional constants appended by generators` marker.
559
+ - **Authored (partial)**: The `_app_version.html.erb.tt` partial is authored, extracted from inscripto-v2's inline sidebar version display (`app/views/layouts/_admin_sidebar_nav.html.erb`) into a reusable partial.
560
+
561
+ ### Generator Methods
562
+
563
+ 1. `inject_app_version_constant` - injects `APP_VERSION = ENV.fetch("APP_VERSION", "dev")` into `config/initializers/constants.rb` after the marker comment, idempotently via `inject_once`
564
+ 2. `create_application_helper` - if `app/helpers/application_helper.rb` exists, injects the `app_version` method via `inject_once` with `before: /^end\s*$/`; else creates the file from template
565
+ 3. `create_app_version_partial` - templates `app/views/shared/_app_version.html.erb` from `.erb.tt`
566
+
567
+ ### Consumer-Side Integrations (NOT in T36)
568
+
569
+ T36 only provides the core constant + helper + partial. The following consumer-side integrations are handled by their respective tasks:
570
+ - **T25 (admin)**: Admin sidebar should render `<%= render "shared/app_version" %>` to display the version
571
+ - **T26 (PWA)**: Service worker should use `APP_VERSION` for `CACHE_VERSION`
572
+ - **T30 (kamal)**: Deploy config should pass `APP_VERSION` as an env var to the container
573
+
574
+ ### Key Decisions
575
+
576
+ 1. **inject_once for both constants.rb and application_helper.rb**: The `inject_once` base class helper provides idempotency through content-inclusion checking. For constants.rb, the `after:` parameter targets the marker comment. For application_helper.rb, the `before: /^end\s*$/` parameter targets the module's closing `end`.
577
+
578
+ 2. **Both branches of application_helper.rb tested**: The `if File.exist?` branch has two paths - inject (file exists) and create (file absent). Both are tested with seeded fixtures. The inject branch seeds an existing helper with an `existing_method`, then verifies both methods coexist.
579
+
580
+ 3. **ERB-in-ERB escaping for partial**: The `_app_version.html.erb.tt` template uses `<%%` and `<%%=` to escape ERB tags so Thor's template engine outputs literal `<%` and `<%=` in the generated `.erb` file.
581
+
582
+ 4. **Anonymous block forwarding for with_tmp_dir**: Ruby 4.0's anonymous block forwarding (`&` without name) avoids `Style/ExplicitBlockArgument` and `Naming/BlockForwarding` cops.
583
+
584
+ ### Spec Coverage
585
+
586
+ 13 tests covering:
587
+ - destination_root binding
588
+ - APP_VERSION injection into constants.rb (content, position after marker, idempotency)
589
+ - application_helper.rb creation (content, frozen_string_literal, idempotency)
590
+ - application_helper.rb injection (method coexists with existing, idempotency)
591
+ - Partial creation, content, idempotency
592
+
593
+ ### Fidelity Specs
594
+
595
+ 1 fidelity spec:
596
+ - `app_version` method in rendered template matches canonical inscripto-v2 fixture
597
+
598
+ ## Task 9 - InstallGenerator (Meta-Generator Invocation)
599
+
600
+ ### The Core Problem
601
+ The install generator must invoke sub-generators (tooling, testing, solid, etc.) programmatically. Standard Thor invocation mechanisms ALL FAIL:
602
+ - `invoke_all` - treats base-class helpers (`inject_once`, `add_gem`) as tasks, raises "no such task"
603
+ - `generate` / `Rails::Generators.invoke` - same Thor task-resolution failure
604
+ - `Rails::Generators.invoke` with explicit args - same failure path
605
+
606
+ ### The Working Approach: Direct Instantiation + public_instance_methods(false)
607
+ Instead of Thor's invocation chain, directly require the generator file, instantiate it bound to `destination_root`, and call each public method defined in the SUBCLASS:
608
+
609
+ ```ruby
610
+ def invoke_generator(name)
611
+ require "generators/develoz/#{name}/#{name}_generator"
612
+ klass = Develoz::Generators.const_get("#{name.camelize}Generator")
613
+ gen = klass.new([], {}, destination_root: destination_root)
614
+ klass.public_instance_methods(false).each { |method| gen.public_send(method) }
615
+ rescue LoadError, NameError
616
+ say "generator develoz:#{name} not yet available", :yellow
617
+ end
618
+ ```
619
+
620
+ **Why `public_instance_methods(false)`**: The `false` argument excludes inherited methods. This is critical because `Develoz::Generators::Base` defines helper methods (`inject_once`, `add_gem`, `insert_route`, etc.) that are NOT generator steps - they're utilities called BY the steps. Without `false`, Thor would try to call those helpers as standalone steps, causing errors or duplicate execution.
621
+
622
+ **Why `klass.new([], {}, destination_root: destination_root)`**: The `[]` is args (empty), `{}` is options (empty hash - sub-generators get their own class_options, not the install generator's), and `destination_root:` binds all file operations to the target directory. This is the same signature Thor uses internally.
623
+
624
+ ### Rescue Branch for Not-Yet-Available Generators
625
+ Opt-in generators (api, auth, pwa, etc.) don't have generator files yet. The `require` raises `LoadError`, caught by the rescue, which prints a yellow warning. This is tested by calling `invoke_generator("api")` directly and asserting `/not yet available/` on stdout.
626
+
627
+ ### Idempotency
628
+ The install generator is idempotent because every sub-generator's methods are idempotent (via `inject_once` content-checking, `add_gem` regex matching, Thor's `template` "identical" behavior). Running install twice produces no duplicate Gemfile entries.
629
+
630
+ ### RuboCop Notes
631
+ - `build_options` originally had 24 ABC size (limit 17) due to 11 `options[:flag]` lookups. Fixed by extracting `OPTION_FLAGS` constant and using `index_with` to build the hash dynamically.
632
+ - Constants defined under `private` need `private_constant :NAME` - the `private` keyword does NOT affect constant visibility (triggers `Lint/UselessConstantScoping`).
633
+
634
+ ## Task 8 - CLI (Thor-based entry point)
635
+
636
+ ### Overview
637
+
638
+ The CLI (`lib/develoz/cli.rb`) is the user-facing entry point. It provides `develoz version` and `develoz new APP_NAME` commands. The `new` command resolves opt-in flags, resolves Ruby/Rails versions via `VersionResolver`, runs `rails new`, writes version files, and invokes the `InstallGenerator`.
639
+
640
+ ### Thor Boolean Option Behavior (VERIFIED)
641
+
642
+ Thor boolean options WITHOUT `default:`:
643
+ - `options[:flag]` is `nil` when the flag is NOT passed
644
+ - `options[:flag]` is `true` when `--flag` is passed
645
+
646
+ So `options[flag].nil?` correctly means "not passed". This is used in `resolve_flag` to distinguish between "user explicitly passed `--api`" (use the value) and "user didn't pass it" (prompt or default to false with `--yes`).
647
+
648
+ ### require_relative Path
649
+
650
+ The prompt specified `require_relative "develoz/version"` but `cli.rb` lives at `lib/develoz/cli.rb`. `require_relative` resolves relative to the current file, so `require_relative "develoz/version"` from `lib/develoz/cli.rb` looks for `lib/develoz/develoz/version.rb` (wrong). The correct path is `require_relative "version"` since the file is already inside the `develoz/` directory.
651
+
652
+ ### RuboCop Fixes Applied
653
+
654
+ 1. `each_with_object({}) { |flag, h| h[flag] = resolve_flag(flag) }` -> `to_h { |flag| [flag, resolve_flag(flag)] }` (Rails/IndexWith, Style/ReduceToHash)
655
+ 2. Empty line after guard clause (`return false if options[:yes]`)
656
+ 3. `Performance/ConstantRegexp` - append `/o` to `/develoz #{Develoz::VERSION}/o`
657
+ 4. `RSpec/AnyInstance` and `RSpec/MultipleExpectations` - disabled with `# rubocop:disable`/`# rubocop:enable` block, matching the pattern in `canonical_fetcher_spec.rb`
658
+
659
+ ### Spec Strategy
660
+
661
+ The spec stubs `VersionResolver`, `InstallGenerator`, `system`, and `Dir.chdir` to test the CLI in isolation without actually running `rails new`. `allow_any_instance_of` is used for `system` because the CLI calls it as a private method on the instance, and there's no way to get the instance reference before `described_class.start` creates it.
662
+
663
+ ## API Generator (Task 22)
664
+
665
+ The api generator adds Blueprinter, RSwag (api/ui/specs), generates a versioned API base controller, blueprinter initializer with LowerCamelTransformer, rswag initializers, mounts rswag routes, and creates an example rswag request spec.
666
+
667
+ ### Canonical Sources
668
+
669
+ - **Adapted files**: All templates are adapted from `LooperInsights/looper_core`:
670
+ - `app/controllers/api/v1/base_controller.rb.tt` adapted from looper_core's `app/controllers/api/v3/base_controller.rb`. Key changes: v3→v1 namespace, removed all Cognito/auth_validation dependencies (`context`, `ensure_authenticated`, `valid_token?`, `admin_logged_in?`, `scoped_client`, `user_id`, `user_email`), replaced with a simple `def authenticate; end` stub (no-op by default, override in subclasses). Retained: `Pagy::Backend`, `render_json_error`, `render_resource`/`render_resources`, `render_json`, `paginated`, `meta`, `common_params`, `default_page_size` class method, `rescue_from` for RecordNotFound/RecordInvalid.
671
+ - `config/initializers/blueprinter.rb.tt` adapted from looper_core's `config/initializers/blueprinter.rb`. Kept `LowerCamelTransformer` class (verbatim) and `default_transformers` + `datetime_format`. Removed `BlueprinterActiveRecord::Preloader` (requires separate `blueprinter-activerecord` gem not added) and the `unless` lambda (app-specific).
672
+ - `config/initializers/rswag_api.rb.tt` adapted from looper_core (added `# frozen_string_literal: true`, otherwise verbatim).
673
+ - `config/initializers/rswag_ui.rb.tt` adapted from looper_core (endpoint label changed from 'Looper Core API V3' to 'API V1', path v3→v1, added frozen_string_literal).
674
+ - **Authored files**: `app/blueprints/example_blueprint.rb.tt` (minimal blueprint inheriting `Blueprinter::Base` directly, since the generated app has no `BaseBlueprint`), `spec/requests/api/v1/examples_spec.rb.tt` (minimal rswag spec demonstrating `path`/`response`/`schema` DSL, no auth dependencies).
675
+
676
+ ### No v3→v1 Chain Pattern
677
+
678
+ LooperInsights/looper_core has `Api::V3::BaseController < ApplicationController` directly. There is no intermediate `Api::ApplicationController` or `Api::V3::ApplicationController`. Therefore, the `app/controllers/api/v1/application_controller.rb.tt` template was NOT created (the task said "if looper_core has the v3→v1 chain pattern").
679
+
680
+ ### No Fidelity Specs
681
+
682
+ All T22 templates are adapted (not verbatim) from looper_core. The primary differences are: added `# frozen_string_literal: true` (looper_core files lack it), removed Cognito dependencies, simplified for a new Rails app. Since there are no byte-identical verbatim files, no fidelity specs were created.
683
+
684
+ ### Install Generator Spec Update
685
+
686
+ The install generator spec (`install_generator_spec.rb`) had a test "warns when a generator is not yet available" that used `invoke_generator("api")` as the not-yet-available example. Since T22 creates the api generator, this test was updated to use `invoke_generator("auth")` instead (auth is still not implemented).
687
+
688
+ ### Generator Methods
689
+
690
+ 1. `add_api_gems` - adds blueprinter, rswag-api, rswag-ui (default group); rswag-specs (test group)
691
+ 2. `create_base_controller` - templates `app/controllers/api/v1/base_controller.rb`
692
+ 3. `create_blueprinter_initializer` - templates `config/initializers/blueprinter.rb`
693
+ 4. `create_example_blueprint` - templates `app/blueprints/example_blueprint.rb`
694
+ 5. `create_rswag_api_initializer` - templates `config/initializers/rswag_api.rb`
695
+ 6. `create_rswag_ui_initializer` - templates `config/initializers/rswag_ui.rb`
696
+ 7. `insert_rswag_routes` - mounts `Rswag::Ui::Engine` and `Rswag::Api::Engine` at `/api-docs` via `insert_route`
697
+ 8. `create_example_request_spec` - templates `spec/requests/api/v1/examples_spec.rb`
698
+
699
+ ### Spec Coverage
700
+
701
+ 36 tests covering:
702
+ - destination_root binding
703
+ - 4 gem additions (blueprinter, rswag-api, rswag-ui, rswag-specs)
704
+ - Base controller: existence, frozen_string_literal, inheritance, Pagy::Backend, authenticate stub, render_json_error
705
+ - Blueprinter initializer: existence, LowerCamelTransformer, default_transformers, frozen_string_literal
706
+ - Example blueprint: existence, Blueprinter::Base inheritance
707
+ - RSwag initializers: rswag_api existence + openapi_root, rswag_ui existence + openapi_endpoint + v1 swagger
708
+ - Routes: rswag ui mount, rswag api mount
709
+ - Example spec: existence, frozen_string_literal, path DSL, response DSL
710
+ - Idempotency: gems, base controller, blueprinter initializer, example blueprint, rswag_api, rswag_ui, routes, example spec (8 tests)
711
+
712
+ ### RuboCop Notes
713
+
714
+ - `Style/RedundantRegexpArgument` triggers on `routes.scan(/mount Rswag::Ui::Engine/)` when the argument contains no regex metacharacters. Use a plain string instead: `routes.scan("mount Rswag::Ui::Engine")`.
715
+
716
+ ## Auth Generator (Task 23)
717
+
718
+ The auth generator scaffolds Rails 8's built-in authentication (equivalent to `bin/rails generate authentication`), adapted for the develoz-rails template pattern.
719
+
720
+ ### Canonical Sources
721
+
722
+ - **Adapted files**: All templates are adapted from the Rails 8 authentication generator source (`rails/rails` repo, `railties/lib/rails/generators/rails/authentication/templates/`):
723
+ - `app/models/user.rb.tt` adapted from Rails 8's `user.rb.tt`. Key changes: uses `email` instead of `email_address`, adds explicit `validates :email` with `URI::MailTo::EMAIL_REGEXP` (Rails 8 relies on `normalizes` + DB uniqueness only). Kept `has_secure_password` and `normalizes :email`.
724
+ - `app/models/current.rb.tt` adapted from Rails 8's `current.rb.tt`. Key change: uses `attribute :user` directly instead of `attribute :session` with `delegate :user, to: :session` (no Session model in this simplified version).
725
+ - `app/controllers/concerns/authentication.rb.tt` adapted from Rails 8's `authentication.rb.tt`. Key changes: uses `Current.user` instead of `Current.session`, `cookies.signed[:user_id]` instead of `cookies.signed[:session_id]`, renamed `allow_unauthenticated_access` to `allow_authentication_as` (per task spec), removed `start_new_session_for`/`terminate_session` Session model logic (simplified to set `Current.user` directly).
726
+ - `app/controllers/sessions_controller.rb.tt` adapted from Rails 8's `sessions_controller.rb.tt`. Key change: uses `email` instead of `email_address` in `authenticate_by` params.
727
+ - `app/controllers/passwords_controller.rb.tt` adapted from Rails 8's `passwords_controller.rb.tt`. Key changes: uses `email` instead of `email_address`, removed conditional `<%- if defined?(ActionMailer::Railtie) -%>` ERB guards (always include mailer-dependent code since these are templates for a Rails app that will have ActionMailer).
728
+ - `app/mailers/passwords_mailer.rb.tt` adapted from Rails 8's `passwords_mailer.rb.tt`. Key change: `to: user.email` instead of `to: user.email_address`.
729
+ - `app/views/sessions/new.html.erb.tt` adapted from Rails 8 ERB template engine's `sessions/new.html.erb`. Key change: `:email` field instead of `:email_address`.
730
+ - `app/views/passwords/edit.html.erb.tt` adapted from Rails 8 ERB template engine's `passwords/edit.html.erb`. Verbatim except for `<%%` ERB escaping.
731
+ - `app/views/passwords_mailer/reset.html.erb.tt` adapted from Rails 8's `passwords_mailer/reset.html.erb.tt`. Verbatim except for `<%%` ERB escaping.
732
+ - **Authored files**: `db/migrate/create_users.rb.tt` (Rails 8 generates migrations via `rails g migration` at runtime, not as a template; we author a static migration template with `email` + `password_digest` columns and unique index). `spec/requests/sessions_spec.rb.tt` and `spec/requests/passwords_spec.rb.tt` (Rails 8 generates these via `hook_for :test_framework`; we author minimal request specs demonstrating sign in/out and password reset flows).
733
+
734
+ ### Rails 8 Authentication Architecture (Latest vs Task Spec)
735
+
736
+ The Rails 8 authentication generator on `main` has evolved to use a `Session` model (with `ip_address` and `user_agent` columns) and `email_address` field. The task spec asks for the simpler earlier version: `Current.user` directly (no Session model), `email` field. The templates follow the task spec, not the latest `main` branch.
737
+
738
+ ### Rails 8 Password Reset Token
739
+
740
+ Rails 8's `has_secure_password` includes `password_reset_token` and `find_by_password_reset_token!` methods (derived from the password digest via `ActiveSupport::MessageVerifier`). The passwords controller and mailer view use these methods. No additional database columns are needed for password reset tokens.
741
+
742
+ ### ERB-in-ERB Escaping for View Templates
743
+
744
+ All `.html.erb.tt` view templates use `<%%` and `<%%=` to escape ERB tags so Thor's template engine outputs literal `<%` and `<%=` in the generated `.erb` files. This is the same pattern used by T19 (docs_render) and T36 (versioning).
745
+
746
+ ### Install Generator Spec Update
747
+
748
+ The install generator spec had a test "warns when a generator is not yet available" that used `invoke_generator("auth")` as the not-yet-available example (set in T22 when api was created). Since T23 creates the auth generator, this test was updated to use `invoke_generator("pwa")` instead (pwa is still not implemented). This is the same pattern as T22's update from "api" to "auth".
749
+
750
+ ### Generator Methods
751
+
752
+ 1. `add_bcrypt_gem` - adds bcrypt gem via `add_gem`
753
+ 2. `create_user_model` - templates `app/models/user.rb`
754
+ 3. `create_current_model` - templates `app/models/current.rb`
755
+ 4. `create_authentication_concern` - templates `app/controllers/concerns/authentication.rb`
756
+ 5. `create_sessions_controller` - templates `app/controllers/sessions_controller.rb`
757
+ 6. `create_passwords_controller` - templates `app/controllers/passwords_controller.rb`
758
+ 7. `create_passwords_mailer` - templates `app/mailers/passwords_mailer.rb`
759
+ 8. `create_sessions_views` - templates `app/views/sessions/new.html.erb`
760
+ 9. `create_passwords_views` - templates `app/views/passwords/edit.html.erb`
761
+ 10. `create_passwords_mailer_views` - templates `app/views/passwords_mailer/reset.html.erb`
762
+ 11. `create_users_migration` - templates `db/migrate/create_users.rb`
763
+ 12. `insert_auth_routes` - inserts `resource :session` and `resources :passwords, param: :token` routes via `insert_route`
764
+ 13. `create_sessions_request_spec` - templates `spec/requests/sessions_spec.rb`
765
+ 14. `create_passwords_request_spec` - templates `spec/requests/passwords_spec.rb`
766
+
767
+ ### Spec Coverage
768
+
769
+ 59 tests covering:
770
+ - destination_root binding
771
+ - bcrypt gem addition
772
+ - User model: existence, has_secure_password, normalizes email, validates email, frozen_string_literal
773
+ - Current model: existence, attribute :user, CurrentAttributes inheritance
774
+ - Authentication concern: existence, before_action, resume_session, request_authentication, after_authentication_url, allow_authentication_as, frozen_string_literal
775
+ - Sessions controller: existence, new/create/destroy actions, frozen_string_literal
776
+ - Passwords controller: existence, edit/update actions, frozen_string_literal
777
+ - Passwords mailer: existence, reset method, frozen_string_literal
778
+ - Sessions view: existence, form_with
779
+ - Passwords view: existence, form_with
780
+ - Mailer view: existence, edit_password_url
781
+ - Migration: existence, email column, password_digest column, unique index, frozen_string_literal
782
+ - Routes: session route, passwords route
783
+ - Request specs: sessions existence + frozen_string_literal + describe, passwords existence + frozen_string_literal + describe
784
+ - Idempotency: bcrypt gem, user model, current model, authentication concern, sessions controller, passwords controller, passwords mailer, sessions view, passwords view, mailer view, migration, routes, sessions spec, passwords spec (14 tests)
785
+
786
+ ## UI Generator (Task 24)
787
+
788
+ The ui generator sets up the `develoz_ui` gem as a git submodule at `vendor/develoz-ui`, with env-conditional Gemfile entries, a `.gitmodules` section, a setup script, and importmap pins for Stimulus controllers.
789
+
790
+ ### Key Decisions
791
+
792
+ 1. **inject_once for Gemfile (not add_gem)**: The base class `add_gem` has an idempotency guard that checks for ANY `gem "develoz_ui"` line in the Gemfile. Since we need TWO declarations (one in `group :development, :test` with `path:`, one in `group :production` with `github:`), calling `add_gem` twice would skip the second call. Instead, use `inject_once` with a multi-line content block containing both group declarations. The content-inclusion guard ensures the entire block is only injected once.
793
+
794
+ 2. **.gitmodules creation + injection**: The base class `inject_once` returns early if the target file doesn't exist (`return unless File.exist?(file_path)`). For `.gitmodules`, which may not exist yet, the generator first creates an empty file via `File.write` if it doesn't exist, then calls `inject_once` to append the submodule section. Both branches (file exists / doesn't exist) are tested for 100% coverage.
795
+
796
+ 3. **Bash setup script (not Ruby)**: The `bin/setup_develoz_ui` script is a bash script (`#!/usr/bin/env bash`) rather than Ruby, since it runs `git submodule update --init --recursive`. The script includes error handling: if the submodule directory is empty or missing after the update, it prints an error message and exits 1. The `.tt` template is processed through Thor's ERB engine, but since the script contains no `<%` sequences, ERB processing is a no-op.
797
+
798
+ 4. **Importmap pins**: Injects two pins into `config/importmap.rb` via `inject_once`: `pin "develoz-ui"` for the main entry point and `pin_all_from` for Stimulus controllers under `develoz-ui/controllers`. The spec seeds a minimal importmap.rb fixture with existing pins to verify both preservation of existing content and injection of new pins.
799
+
800
+ ### Generator Methods
801
+
802
+ 1. `add_develoz_ui_gems` - injects two group blocks into Gemfile via `inject_once` (dev/test with `path:`, production with `github:`)
803
+ 2. `create_gitmodules` - creates `.gitmodules` if absent, then injects submodule section via `inject_once`
804
+ 3. `create_setup_script` - templates `bin/setup_develoz_ui` and chmods to 0o755
805
+ 4. `inject_importmap_pins` - injects develoz-ui Stimulus controller pins into `config/importmap.rb` via `inject_once`
806
+
807
+ ### Spec Coverage
808
+
809
+ 17 tests covering:
810
+ - destination_root binding
811
+ - Gemfile: develoz_ui path in dev/test group, github in production group, idempotency
812
+ - .gitmodules: creation when absent, submodule section content, append to existing, idempotency
813
+ - Setup script: existence, executable bit, git submodule command, error documentation, idempotency, executable bit preserved on re-run
814
+ - Importmap: pin injection, existing content preservation, idempotency
815
+
816
+ ### RuboCop Notes
817
+
818
+ - Long string literals with `\n` and `\t` escapes can exceed the 120-char line limit. Use Ruby string continuation (`\` at end of line) to split across multiple lines: `"[submodule ...]\n" \` followed by `"\tpath = ...\n" \`.
819
+
820
+
821
+ ## DB Backup Generator (Task 31)
822
+
823
+ The db_backup generator creates a `bin/db-backup` shell script (pg_dump + gzip + retention pruning), a `lib/tasks/backup.rake` rake fallback, and optionally injects a `db-backup` compose service when `--docker` is present.
824
+
825
+ ### Key Decisions
826
+
827
+ 1. **Spec path follows existing convention (`spec/develoz/generators/`)**: The task referenced `spec/generators/db_backup_generator_spec.rb`, but all 19 existing generator specs live at `spec/develoz/generators/`. Placing the spec there keeps consistency and ensures the `RSpec/SpecFilePathFormat` cop passes. The task path was treated as shorthand.
828
+
829
+ 2. **Compose service via template + ERB rendering**: The `compose_service.tt` template contains `<%= app_name %>` ERB. Since `inject_once` takes a raw content string (not a Thor template render), the generator reads the template file and renders it with `ERB.new(raw, trim_mode: "-").result(binding)`. The generator's binding provides `app_name` from the base helper. This keeps the service definition in a template file (as required) while still using `inject_once` for idempotent injection.
830
+
831
+ 3. **`class_option :docker` with default false**: The install generator invokes sub-generators with `klass.new([], {}, destination_root: ...)` (empty options), so `options[:docker]` defaults to false in the install flow. When run standalone with `--docker`, the compose service is injected. This satisfies "Do NOT require docker" - the rake fallback is always created; compose injection is opt-in.
832
+
833
+ 4. **`inject_once` with marker guard for compose**: The `marker:` parameter ("# db-backup service (develoz:db_backup)") provides idempotency independent of the full content block. The `after: /^services:\n/` anchor inserts the service right after the `services:` line, preserving existing services.
834
+
835
+ 5. **bin/db-backup is a shell script (excluded from coverage)**: The spec_helper filters `/bin/` and `.sh` files from coverage. The rake task IS Ruby and gets coverage. The rake task uses `Open3.popen3` + `Zlib::GzipWriter` for the Ruby fallback path.
836
+
837
+ ### Generator Methods
838
+
839
+ 1. `create_backup_script` - templates `bin/db-backup` and chmods to 0o755
840
+ 2. `create_backup_rake` - templates `lib/tasks/backup.rake`
841
+ 3. `inject_compose_service` - returns early unless `options[:docker]`; injects compose service snippet via `inject_once` with marker guard
842
+ 4. `ensure_backups_gitignored` - adds `/backups/` to `.gitignore` via `ensure_gitignore`
843
+
844
+ ### Spec Coverage
845
+
846
+ 22 tests covering:
847
+ - destination_root binding
848
+ - bin/db-backup: existence, executable bit, pg_dump+gzip, .sql.gz, retention pruning, idempotency, executable bit preserved on re-run
849
+ - rake task: existence, backup namespace with create+prune tasks, frozen_string_literal, retention env var, idempotency
850
+ - gitignore: /backups/ added, idempotency
851
+ - compose service: NOT injected when docker=false, NOT injected when docker unset, injected when docker=true, 6h schedule (21600s), retention env, depends_on postgres, preserves existing services, idempotency
852
+
853
+ ### RuboCop Notes
854
+
855
+ - `Layout/TrailingEmptyLines: Final newline missing` appeared on both generated files after initial write. Fixed via `rubocop -A` autocorrect. Always verify files end with exactly one newline.