tsykvas_rails_template 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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +200 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +589 -0
  6. data/Rakefile +17 -0
  7. data/lib/generators/tsykvas_rails_template/companions/companions_generator.rb +273 -0
  8. data/lib/generators/tsykvas_rails_template/concept/concept_generator.rb +145 -0
  9. data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.html.slim.tt +5 -0
  10. data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.rb.tt +11 -0
  11. data/lib/generators/tsykvas_rails_template/concept/templates/component/index.html.slim.tt +5 -0
  12. data/lib/generators/tsykvas_rails_template/concept/templates/component/index.rb.tt +11 -0
  13. data/lib/generators/tsykvas_rails_template/concept/templates/component/new.html.slim.tt +5 -0
  14. data/lib/generators/tsykvas_rails_template/concept/templates/component/new.rb.tt +11 -0
  15. data/lib/generators/tsykvas_rails_template/concept/templates/component/show.html.slim.tt +4 -0
  16. data/lib/generators/tsykvas_rails_template/concept/templates/component/show.rb.tt +11 -0
  17. data/lib/generators/tsykvas_rails_template/concept/templates/controller.rb.tt +45 -0
  18. data/lib/generators/tsykvas_rails_template/concept/templates/operation/create.rb.tt +31 -0
  19. data/lib/generators/tsykvas_rails_template/concept/templates/operation/destroy.rb.tt +13 -0
  20. data/lib/generators/tsykvas_rails_template/concept/templates/operation/edit.rb.tt +10 -0
  21. data/lib/generators/tsykvas_rails_template/concept/templates/operation/index.rb.tt +9 -0
  22. data/lib/generators/tsykvas_rails_template/concept/templates/operation/new.rb.tt +10 -0
  23. data/lib/generators/tsykvas_rails_template/concept/templates/operation/show.rb.tt +10 -0
  24. data/lib/generators/tsykvas_rails_template/concept/templates/operation/update.rb.tt +31 -0
  25. data/lib/generators/tsykvas_rails_template/install/bootstrap_installer.rb +225 -0
  26. data/lib/generators/tsykvas_rails_template/install/install_generator.rb +298 -0
  27. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/buddy.md +157 -0
  28. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/code-reviewer.md +117 -0
  29. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/security-reviewer.md +113 -0
  30. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/tech-lead.md +150 -0
  31. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/check.md +51 -0
  32. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/code-review.md +60 -0
  33. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/docs-create.md +102 -0
  34. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pr-review.md +81 -0
  35. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pushit.md +160 -0
  36. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/refactor.md +132 -0
  37. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/task-sum.md +47 -0
  38. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tests.md +67 -0
  39. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tsykvas-claude.md +262 -0
  40. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-docs.md +78 -0
  41. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-rules.md +102 -0
  42. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-tests.md +135 -0
  43. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/architecture.md +315 -0
  44. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/authentication.md +96 -0
  45. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/background-jobs.md +135 -0
  46. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/code-style.md +101 -0
  47. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/commands.md +34 -0
  48. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/companions.md +128 -0
  49. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/concepts-refactoring.md +194 -0
  50. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/database.md +135 -0
  51. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/deployment.md +138 -0
  52. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/design-system.md +322 -0
  53. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/documentation.md +89 -0
  54. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/forms.md +174 -0
  55. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/i18n.md +165 -0
  56. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/routing-and-namespaces.md +114 -0
  57. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/security.md +122 -0
  58. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/stimulus-controllers.md +166 -0
  59. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing-examples.md +180 -0
  60. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing.md +117 -0
  61. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/tsykvas_rails_template.md +280 -0
  62. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/ui-components.md +196 -0
  63. data/lib/generators/tsykvas_rails_template/install/templates/CLAUDE.md.tt +81 -0
  64. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/component/base.rb +6 -0
  65. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/base.rb +124 -0
  66. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/result.rb +56 -0
  67. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.html.slim +49 -0
  68. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.rb +11 -0
  69. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/operation/index.rb +17 -0
  70. data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/concerns/operations_methods.rb +148 -0
  71. data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/home_controller.rb +10 -0
  72. data/lib/generators/tsykvas_rails_template/install/templates/app/policies/application_policy.rb +33 -0
  73. data/lib/generators/tsykvas_rails_template/install/templates/app/policies/home_policy.rb +8 -0
  74. data/lib/tasks/tsykvas.rake +11 -0
  75. data/lib/tsykvas_rails_template/probe.rb +236 -0
  76. data/lib/tsykvas_rails_template/railtie.rb +13 -0
  77. data/lib/tsykvas_rails_template/version.rb +5 -0
  78. data/lib/tsykvas_rails_template.rb +18 -0
  79. metadata +183 -0
@@ -0,0 +1,180 @@
1
+ # Testing — Spec Examples by Type
2
+
3
+ ## Operation
4
+
5
+ ```ruby
6
+ # frozen_string_literal: true
7
+
8
+ require 'rails_helper'
9
+
10
+ RSpec.describe Crm::Property::Operation::Edit do
11
+ subject(:result) do
12
+ described_class.call(params: ActionController::Parameters.new, current_user: user)
13
+ end
14
+
15
+ let(:user) { create(:user, :owner) }
16
+ let!(:property) { user.owned_property || create(:property, owner: user) }
17
+
18
+ describe '#perform!' do
19
+ context 'when user is owner of the property' do
20
+ it 'is successful' do
21
+ expect(result).to be_success
22
+ end
23
+
24
+ it 'returns the property wrapped in OpenStruct' do
25
+ expect(result.model.property).to eq(property)
26
+ end
27
+ end
28
+
29
+ context 'when user has no access to CRM' do
30
+ let(:user) { create(:user, :customer) }
31
+
32
+ it 'returns no records via policy_scope' do
33
+ expect(result.model.property).to be_nil
34
+ end
35
+ end
36
+ end
37
+ end
38
+ ```
39
+
40
+ - Always test the authorization happy path AND the `Pundit::NotAuthorizedError` (or empty `policy_scope`) case.
41
+ - Use `change(...).by(n)` for DB count assertions.
42
+ - Pass `current_user:` and `params:` explicitly — every operation requires both.
43
+
44
+ ## Component
45
+
46
+ ```ruby
47
+ # frozen_string_literal: true
48
+
49
+ require 'rails_helper'
50
+
51
+ RSpec.describe Admin::User::Component::Index, type: :component do
52
+ let(:users) { create_list(:user, 3, :customer) }
53
+ let(:component) { described_class.new(users: users) }
54
+
55
+ it 'renders the table' do
56
+ render_inline(component)
57
+ expect(page).to have_text(I18n.t('admin.users.index.table.email'))
58
+ users.each { |u| expect(page).to have_text(u.email) }
59
+ end
60
+
61
+ describe '#role_badge' do
62
+ subject { component.send(:role_badge, 'admin') }
63
+ it { is_expected.to include('badge bg-danger') }
64
+ end
65
+ end
66
+ ```
67
+
68
+ - Use `render_inline` + Capybara matchers for rendered output.
69
+ - Test private helpers via `.send(:method_name)`.
70
+ - Declare `type: :component` (not inferred from path).
71
+
72
+ ## Model
73
+
74
+ ```ruby
75
+ # frozen_string_literal: true
76
+
77
+ require 'rails_helper'
78
+
79
+ RSpec.describe User, type: :model do
80
+ subject { build(:user) }
81
+
82
+ it { is_expected.to validate_presence_of(:name) }
83
+ it { is_expected.to belong_to(:property).optional }
84
+ it { is_expected.to have_one(:owned_property).class_name('Crm::Property') }
85
+ it do
86
+ is_expected.to define_enum_for(:role)
87
+ .with_values(admin: 0, customer: 1, owner: 2, employee: 3, manager: 4)
88
+ end
89
+ end
90
+ ```
91
+
92
+ - Use Shoulda-Matchers for validations and associations.
93
+ - Prefer `build` over `create` for validation tests.
94
+ - Declare `type: :model` (not inferred).
95
+
96
+ ## Policy
97
+
98
+ ```ruby
99
+ # frozen_string_literal: true
100
+
101
+ require 'rails_helper'
102
+
103
+ RSpec.describe Crm::PropertyPolicy do
104
+ subject { described_class.new(user, property) }
105
+
106
+ let(:owner) { create(:user, :owner) }
107
+ let(:property) { owner.owned_property }
108
+
109
+ context 'when user is the owner' do
110
+ let(:user) { owner }
111
+
112
+ it { is_expected.to permit_action(:update) }
113
+ it { is_expected.to permit_action(:destroy) }
114
+ end
115
+
116
+ context 'when user is a different owner' do
117
+ let(:user) { create(:user, :owner) }
118
+
119
+ it { is_expected.not_to permit_action(:update) }
120
+ end
121
+
122
+ describe 'Scope' do
123
+ subject(:resolved) { described_class::Scope.new(user, Crm::Property).resolve }
124
+
125
+ let(:user) { create(:user, :admin) }
126
+ before { create(:property, owner: create(:user, :owner)) }
127
+
128
+ it { is_expected.to eq(Crm::Property.all) }
129
+ end
130
+ end
131
+ ```
132
+
133
+ ## Controller / request
134
+
135
+ ```ruby
136
+ # frozen_string_literal: true
137
+
138
+ require 'rails_helper'
139
+
140
+ RSpec.describe Admin::UsersController, type: :controller do
141
+ let(:admin) { create(:user, :admin) }
142
+
143
+ before do
144
+ allow(controller).to receive(:authenticate_user!).and_return(true)
145
+ allow(controller).to receive(:current_user).and_return(admin)
146
+ end
147
+
148
+ describe 'GET #index' do
149
+ it 'returns 200' do
150
+ get :index
151
+ expect(response).to have_http_status(:ok)
152
+ end
153
+ end
154
+ end
155
+ ```
156
+
157
+ - Always stub `authenticate_user!` and `current_user` in controller specs.
158
+ - Declare `type: :controller` or `type: :request` explicitly.
159
+
160
+ ## Operation with sub-operations
161
+
162
+ ```ruby
163
+ RSpec.describe Crm::Property::Operation::Create do
164
+ subject(:result) do
165
+ described_class.call(params: params, current_user: nil)
166
+ end
167
+
168
+ let(:params) do
169
+ ActionController::Parameters.new(
170
+ user: { name: 'Foo', email: 'foo@example.com', password: 'password', password_confirmation: 'password' },
171
+ property_name: 'Riverside House'
172
+ )
173
+ end
174
+
175
+ it 'creates a user with role :owner and a property' do
176
+ expect { result }.to change(User, :count).by(1).and change(Crm::Property, :count).by(1)
177
+ expect(result).to be_success
178
+ end
179
+ end
180
+ ```
@@ -0,0 +1,117 @@
1
+ # Testing Guide
2
+
3
+ ## Framework & Setup
4
+
5
+ - **RSpec** + `rails_helper` (required in every spec file)
6
+ - **FactoryBot** — `create`, `build`, `build_stubbed` available globally (`config.include FactoryBot::Syntax::Methods` in `spec/rails_helper.rb`)
7
+ - **Faker** — realistic test data; unique sequences cleared between examples (`Faker::UniqueGenerator.clear`)
8
+ - **Shoulda-Matchers** — model/association matchers (configured for `:rspec` + `:rails`)
9
+ - **Capybara + selenium-webdriver** — system tests
10
+ - `config.use_transactional_fixtures = true` — DB isolation per test
11
+ - `infer_spec_type_from_file_location!` is **NOT** enabled — declare `type:` explicitly when needed (`type: :request`, `type: :component`, `type: :model`, ...)
12
+
13
+ ## File location
14
+
15
+ Mirror `app/` in `spec/`:
16
+
17
+ | Source | Spec |
18
+ |---|---|
19
+ | `app/concepts/admin/user/operation/index.rb` | `spec/concepts/admin/user/operation/index_spec.rb` |
20
+ | `app/concepts/admin/user/component/users_table.rb` | `spec/concepts/admin/user/component/users_table_spec.rb` |
21
+ | `app/policies/admin/base_policy.rb` | `spec/policies/admin/base_policy_spec.rb` |
22
+ | `app/models/user.rb` | `spec/models/user_spec.rb` |
23
+ | `app/controllers/admin/users_controller.rb` | `spec/controllers/admin/users_controller_spec.rb` (or `spec/requests/admin/users_spec.rb`) |
24
+
25
+ ## Spec structure
26
+
27
+ ```ruby
28
+ # frozen_string_literal: true
29
+
30
+ require 'rails_helper'
31
+
32
+ RSpec.describe Admin::User::Operation::Show do
33
+ subject(:result) do
34
+ described_class.call(params: { id: target_user.id }, current_user: admin_user)
35
+ end
36
+
37
+ let(:admin_user) { create(:user, :admin) }
38
+ let(:target_user) { create(:user, :customer) }
39
+
40
+ describe '#perform!' do
41
+ context 'when user is admin' do
42
+ it 'returns success' do
43
+ expect(result).to be_success
44
+ end
45
+
46
+ it 'returns the user wrapped in OpenStruct' do
47
+ expect(result.model.user).to eq(target_user)
48
+ end
49
+ end
50
+
51
+ context 'when user is not admin' do
52
+ let(:admin_user) { create(:user, :customer) }
53
+
54
+ it 'raises Pundit::NotAuthorizedError' do
55
+ expect { result }.to raise_error(Pundit::NotAuthorizedError)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ ```
61
+
62
+ - Use `described_class`, `let` (lazy), `let!` only when a record must exist before the example.
63
+ - `describe` → `context` → `it`; one logical assertion per example.
64
+ - Always test the **happy path** AND the **`Pundit::NotAuthorizedError` path** for operations.
65
+
66
+ ## Factories
67
+
68
+ `spec/factories/users.rb` already defines traits: `:admin`, `:customer`, `:owner`, `:employee`, `:manager`, `:with_property`. Reuse them:
69
+
70
+ ```ruby
71
+ let(:admin) { create(:user, :admin) }
72
+ let(:owner) { create(:user, :owner) }
73
+ let(:employee) { create(:user, :employee) }
74
+ ```
75
+
76
+ Rules for new factories:
77
+
78
+ - Always use **Faker** with `unique` for unique fields (`Faker::Internet.unique.email`).
79
+ - Use `trait` for variations.
80
+ - Use `association` for required relations.
81
+ - Prefer `build` over `create` in unit tests; `build_stubbed` for attribute-only logic.
82
+
83
+ ## Mocks, stubs & doubles
84
+
85
+ Prefer `instance_double` (verifies interface) over plain `double`. Use plain `double` only for value objects.
86
+
87
+ ```ruby
88
+ # Setup in before, assert separately
89
+ before { allow(service).to receive(:call).and_return(result) }
90
+ it { expect(service).to have_received(:call).once }
91
+
92
+ # Controller auth stubs
93
+ before do
94
+ allow(controller).to receive(:authenticate_user!).and_return(true)
95
+ allow(controller).to receive(:current_user).and_return(user)
96
+ end
97
+ ```
98
+
99
+ If you need HTTP stubbing later, add WebMock and `stub_request` — the project doesn't currently use it.
100
+
101
+ ## Running tests
102
+
103
+ ```bash
104
+ bundle exec rspec # all
105
+ bundle exec rspec spec/concepts/admin/user/operation/show_spec.rb # single file
106
+ bundle exec rspec spec/models/user_spec.rb:42 # by line
107
+ bundle exec rspec --tag focus # by tag
108
+ ```
109
+
110
+ ## Anti-patterns
111
+
112
+ - ❌ `FactoryBot.create` — use shorthand `create`
113
+ - ❌ Hardcoded strings in factories — use Faker
114
+ - ❌ `double` when `instance_double` is available
115
+ - ❌ `expect(...).to receive(...)` in `before` — use `allow` + `have_received`
116
+ - ❌ Skipping authorization assertions for operations
117
+ - ❌ Writing tests unless explicitly requested by the user
@@ -0,0 +1,280 @@
1
+ # tsykvas_rails_template — gem reference
2
+
3
+ Read this file when you need to understand how the gem is built, what each
4
+ generator does, and how the `.claude/` payload is supposed to be used. It
5
+ ships with the gem and lands in every host project's `.claude/docs/`.
6
+
7
+ This file is **gem-canonical**: `/tsykvas-claude` keeps it verbatim
8
+ and never tailors it per-project. If a section feels out of date, that's a
9
+ gem update, not a project tailoring — bump the gem and re-run install.
10
+
11
+ ## 1. What the gem is
12
+
13
+ Four pillars:
14
+
15
+ 1. **Thin-controller / `endpoint` DSL.** Controllers become one-liners:
16
+ `endpoint Crm::Property::Operation::Index, Crm::Property::Component::Index`.
17
+ `OperationsMethods` (the controller concern shipped at
18
+ `app/controllers/concerns/operations_methods.rb`) handles HTML / JS /
19
+ JSON / `format.any` dispatch, flash, redirects, and the Pundit
20
+ authorization-check enforcement.
21
+ 2. **`Base::Operation::Base` + `Base::Operation::Result`.** Plain-Ruby
22
+ alternative to Trailblazer. Operations subclass `Base::Operation::Base`,
23
+ implement `perform!(params:, current_user:)`, set `self.model = ...` /
24
+ `self.redirect_path = ...`, call `notice(text)` and at least one of
25
+ `authorize!` / `policy_scope` / `skip_authorize` / `skip_policy_scope`.
26
+ `run_operation(OpClass, params)` chains sub-operations; `add_errors` /
27
+ `invalid!` flag failures; `Result#success?` / `failure?` decide flow.
28
+ 3. **`Base::Component::Base` + `app/concepts/<feature>/{operation,component}/`
29
+ layout.** Components extend `ViewComponent::Base`, take constructor
30
+ kwargs with **specific data names** (`initialize(events:)`, never
31
+ `initialize(model:)`), and live next to a `.html.slim` template.
32
+ `config.autoload_paths += %W[#{config.root}/app/concepts]` is patched
33
+ into `config/application.rb` by the install generator.
34
+ 4. **`.claude/` payload.** 4 subagents (`buddy`, `code-reviewer`,
35
+ `security-reviewer`, `tech-lead`), 12 slash commands (incl. the
36
+ probe-driven `/tsykvas-claude`), 12 architecture docs incl. this
37
+ file. `CLAUDE.md` at the repo root is the navigation index, capped at
38
+ 100 lines for token economy.
39
+
40
+ ## 2. How `:install` works
41
+
42
+ `bin/rails g tsykvas_rails_template:install` runs these steps in order:
43
+
44
+ 1. `directory app/concepts/base, ...` — copies `app/concepts/base/operation/{base,result}.rb`
45
+ and `app/concepts/base/component/base.rb` into the host.
46
+ 2. `copy_file app/controllers/concerns/operations_methods.rb` — drops the
47
+ `endpoint` DSL.
48
+ 3. **Patches `config/application.rb`** with
49
+ `config.autoload_paths += %W[#{config.root}/app/concepts]`. Idempotent —
50
+ re-runs detect the existing line and skip.
51
+ 4. **Wires `ApplicationController`** unconditionally:
52
+ `inject_into_class` adds `include Pundit::Authorization` and
53
+ `include OperationsMethods`. The `endpoint` DSL itself uses
54
+ `try(:current_user)`, so it works whether or not Devise is mounted —
55
+ `current_user` resolves to `nil` until you add an auth source. Both
56
+ includes are idempotent — re-runs check existing content.
57
+ 5. `app/policies/application_policy.rb` — generated only if missing.
58
+ 6. **Home example concept** (`generate_home_example` + `add_root_route`):
59
+ scaffolds `HomeController#index` (one-line `endpoint` call),
60
+ `Home::Operation::Index`, `Home::Component::Index` (+ Slim template),
61
+ `HomePolicy`, and `root "home#index"`. Skipped via `--skip-home-example`
62
+ or if any of the targets already exist.
63
+ 7. `directory .claude/{agents,commands,docs}` — drops the full Claude
64
+ payload.
65
+ 8. `template CLAUDE.md.tt → CLAUDE.md` — fenced layout, ≤ 100 lines (skipped
66
+ if `CLAUDE.md` already exists, so `claude init` output stays untouched).
67
+
68
+ **Opt-out flags:** `--skip-application-policy`, `--skip-autoload-paths`,
69
+ `--skip-claude`, `--skip-home-example`, `--keep-sqlite`. Each maps to a
70
+ single step above.
71
+
72
+ ## 3. How `:concept` works
73
+
74
+ `bin/rails g tsykvas_rails_template:concept Crm::Property [--controller] [--actions index show new create]`.
75
+
76
+ 1. **Validates input.** First step is `validate_concept_name`. Regex:
77
+ `/\A[A-Za-z][A-Za-z0-9]*(?:(?:::|\/)[A-Za-z][A-Za-z0-9]*)*\z/`. Empty
78
+ string, leading `::` or `/`, or characters outside the alphabet raise
79
+ `Thor::Error` immediately. No file is touched if validation fails.
80
+ 2. **Generates 7 operations** by default (subset via `--actions`):
81
+ `index`, `show`, `new`, `create`, `edit`, `update`, `destroy`. Files
82
+ land at `app/concepts/<path>/operation/<action>.rb`.
83
+ 3. **Generates 4 components** (`index`, `show`, `new`, `edit`) with both
84
+ `<action>.rb` and `<action>.html.slim`.
85
+ 4. **`--controller`** generates `app/controllers/<path>_controller.rb` with
86
+ thin actions calling `endpoint Op, Component`.
87
+ 5. **`create.rb` and `update.rb` raise `NotImplementedError`** in their
88
+ `_params` method. This is intentional — `params.require(:foo).permit`
89
+ without an attribute list silently saves empty records, which is worse
90
+ than failing loud. The error message tells you to either inline-permit
91
+ `params.require(:foo).permit(:name, :description)` or promote to a
92
+ `<Concept>::Form` object (see `forms.md`).
93
+ 6. **Re-run safety.** Thor prompts on file conflicts by default. Don't pass
94
+ `--force` unless you intend to overwrite hand-written code.
95
+
96
+ ## 4. How `:companions` works
97
+
98
+ `bin/rails g tsykvas_rails_template:companions [--skip-X ...]`. Adds the
99
+ recommended companion gems used across the gem author's reference projects
100
+ and runs their `:install` sub-generators. Run **after** `:install`.
101
+
102
+ Groups (default: all enabled):
103
+
104
+ - `auth` — `devise` + `omniauth-rails_csrf_protection`.
105
+ Post-install: `rails g devise:install`. **Does NOT generate a User model**
106
+ — run `rails g devise User` (or your resource name) yourself when ready.
107
+ - `forms` — `simple_form`. Post-install: `rails g simple_form:install`,
108
+ with `--bootstrap` if Probe sees Bootstrap in the host.
109
+ - `images` — `mini_magick`. Post-install: nothing (you need ImageMagick
110
+ installed system-wide; the gem doesn't try to install OS packages).
111
+ - `jobs-ui` — `mission_control-jobs`, gated on `:solid_queue` being in
112
+ `Gemfile.lock`. Post-install: injects an admin-gated mount into
113
+ `config/routes.rb`:
114
+ ```ruby
115
+ mount MissionControl::Jobs::Engine,
116
+ at: "/jobs",
117
+ constraints: ->(req) {
118
+ user = req.env["warden"]&.user
119
+ user.respond_to?(:admin?) && user.admin?
120
+ }
121
+ ```
122
+ The lambda runs per request, so a missing User model at boot doesn't
123
+ crash. Without `User#admin?` all `/jobs` requests return 404
124
+ (lock-by-default). Assumes Warden-based auth (Devise or compatible);
125
+ swap the constraint for non-Warden stacks.
126
+ - `test` — `rspec-rails`, `factory_bot_rails`, `faker` (in `:development, :test`)
127
+ + `shoulda-matchers`, `webmock` (in `:test`). Post-install:
128
+ `rails g rspec:install` + appends shoulda-matchers and WebMock config
129
+ blocks to `spec/rails_helper.rb`.
130
+ - `dev` — `dotenv-rails`. Post-install: appends `.env`, `.env.*`, and
131
+ `!.env.example` rules to `.gitignore`.
132
+
133
+ **Opt-out flags:** `--skip-auth`, `--skip-forms`, `--skip-images`,
134
+ `--skip-jobs-ui`, `--skip-test`, `--skip-dev`, plus `--skip-bundle` and
135
+ `--skip-post-install` for the tail-end steps.
136
+
137
+ **Idempotency:** every Gemfile addition checks current contents first; every
138
+ `:install` sub-generator skips if its canonical config file already exists;
139
+ every config injection (rails_helper.rb, routes.rb, .gitignore) checks for
140
+ its marker and bails if present. Re-running is safe.
141
+
142
+ ## 5. How `Probe` + `tsykvas:probe` work
143
+
144
+ `TsykvasRailsTemplate::Probe.run(root: Dir.pwd)` returns a Hash with:
145
+
146
+ - `schema_version: 2` — bump if the structure changes.
147
+ - `gem_version`, `root` — provenance.
148
+ - `ruby_version`, `rails_version`, `default_branch`.
149
+ - `api_only` — `true` if `config.api_only = true` in `application.rb`.
150
+ - `engine_host` — `true` if app class inherits `Rails::Engine`.
151
+ - `template_engine` — `:slim` / `:haml` / `:erb` / `nil` (api_only).
152
+ - `auth` — Hash with `devise`, `omniauth`, `omniauth_openid_connect`,
153
+ `warden`, `jwt`, `basic_auth`, `custom_current_user`, plus a coarse
154
+ `method` classification (`:devise`, `:devise_omniauth`, `:warden`,
155
+ `:jwt`, `:basic_auth`, `:custom`, `:none`).
156
+ - `authorization` — `:pundit` / `:action_policy` / `:cancancan` / `:none`.
157
+ - `has_api_v1` — `true` if routes have `namespace :api` + `:v1`, `scope "api/v1"`,
158
+ or `app/controllers/api/v1/` directory exists.
159
+ - `has_bootstrap`, `test_framework`, `background_jobs`, `databases`,
160
+ `concept_folders`, `application_controller_includes`.
161
+
162
+ Surface: pure-Ruby, no Rails dependency in the class itself. Wrapped in a
163
+ Rake task via `TsykvasRailsTemplate::Railtie`:
164
+ ```bash
165
+ bundle exec rake tsykvas:probe # JSON inventory of the host
166
+ ```
167
+
168
+ When in doubt, **read probe output before improvising**. Probe is the
169
+ deterministic source of truth that `/tsykvas-claude` consumes; you
170
+ should consume it the same way.
171
+
172
+ ## 6. Fence-based idempotency in `CLAUDE.md`
173
+
174
+ Gem-owned sections in `CLAUDE.md` are wrapped in HTML comments:
175
+
176
+ ```markdown
177
+ <!-- tsykvas-template:start v=0.1.0 section=must-know-rules -->
178
+ ## Must-know rules
179
+ ...
180
+ <!-- tsykvas-template:end -->
181
+ ```
182
+
183
+ `/tsykvas-claude` rewrites only the content between matching markers.
184
+ User-edited content above the first start marker, between fences, or below
185
+ the last end marker is preserved. Section names are stable across versions;
186
+ the `v=` tag changes when a section's structure changes meaningfully.
187
+
188
+ **Hard rule: `CLAUDE.md` ≤ 100 lines.** Token economy — `CLAUDE.md` sits in
189
+ every Claude session's context, so each line is a per-prompt token tax.
190
+ Push depth into `.claude/docs/<topic>.md` and link from the routing table.
191
+ Phase 5 of `/tsykvas-claude` runs `wc -l CLAUDE.md` and rolls back any
192
+ rewrite that exceeds 100 lines. The shipped template is currently ~95
193
+ lines with ~5 lines of headroom; if you need more, compress, don't grow.
194
+
195
+ ## 7. `/tsykvas-claude` workflow
196
+
197
+ Six phases:
198
+
199
+ - **Phase 0** — `bundle exec rake tsykvas:probe`. Source of truth.
200
+ - **Phase 1** — Read existing fenced content and unfenced (user-authored)
201
+ content. The latter is sacred.
202
+ - **Phase 2** — Plan rewrite per fence section, budgeting against the
203
+ 100-line cap. Drop docs that the host doesn't need (e.g.
204
+ `api-endpoints.md` if `probe.has_api_v1` is false).
205
+ - **Phase 3** — If `--dry-run`, print unified diff and exit. Otherwise show
206
+ a confirmation table and ask `yes / no / dry-run / diff <file>`.
207
+ - **Phase 4** — Apply only inside fences. Never touch unfenced content.
208
+ - **Phase 5** (mandatory verify, rollback on failure):
209
+ 1. `wc -l CLAUDE.md` ≤ 100 — HARD GATE.
210
+ 2. Every internal link resolves.
211
+ 3. Every fence has matching start/end.
212
+ 4. `bin/rails zeitwerk:check` passes.
213
+ 5. Probe re-run matches the values that drove this rewrite.
214
+
215
+ If any verify check fails, restore the pre-write state from the in-memory
216
+ snapshot. Never leave the repo in a half-rewritten state.
217
+
218
+ ## 8. Slash commands and subagents
219
+
220
+ | Command | Use when |
221
+ |---|---|
222
+ | `/check` | RSpec/Minitest + RuboCop + i18n-tasks. Run before commit. |
223
+ | `/code-review` | Parallel code-reviewer + security-reviewer + tech-lead audit. Run before PR. |
224
+ | `/pr-review <num>` | Same audit for a GitHub PR. |
225
+ | `/refactor <files>` | Refactor following code-style + architecture docs. |
226
+ | `/tests` / `/update-tests` | Audit + write missing specs. |
227
+ | `/update-docs` / `/update-rules` | Sync project docs / `.claude/` after meaningful changes. |
228
+ | `/pushit` | Full pre-push: docs → rules → safety scan → checks → commit + push. |
229
+ | `/task-sum` | Release notes from branch diff. |
230
+ | `/docs-create <feature>` | Deep technical doc for a feature. |
231
+ | `/tsykvas-claude` | Rebuild `.claude/` + `CLAUDE.md` against host stack. |
232
+
233
+ | Agent | Use when |
234
+ |---|---|
235
+ | `buddy` | Plan a new feature; produces `feature_plan.md`. |
236
+ | `code-reviewer` | Concepts Pattern + style review. |
237
+ | `security-reviewer` | Pundit auth, IDOR, mass assignment, SQL injection. |
238
+ | `tech-lead` | Architectural decisions, pre-PR review, design trade-offs. |
239
+
240
+ ## 9. Common gotchas
241
+
242
+ - **`current_user` is `nil` in operations** → expected on a fresh install
243
+ before you add Devise. `endpoint` uses `try(:current_user)`, so the call
244
+ succeeds and `current_user:` is passed as `nil`. Operations that need a
245
+ real user (anything past the home example) should authorize against the
246
+ Pundit policy and let it deny `nil`. Once Devise is mounted, its
247
+ `current_user` helper shadows the fallback automatically.
248
+ - **Bootstrap not loaded but app uses `format.js`** → modal dismiss step
249
+ is feature-checked (`if (window.bootstrap && window.bootstrap.Modal)`),
250
+ so it no-ops gracefully. The rest of the modal-injection JS still runs.
251
+ If you don't use Bootstrap modals at all, replace the `format.js` branch
252
+ in your generated `OperationsMethods`.
253
+ - **Concept generator silently overwrites** → it doesn't, unless you pass
254
+ `--force`. Default behavior is Thor's interactive conflict prompt.
255
+ - **`mini_magick` works but conversions fail** → ImageMagick isn't
256
+ installed system-wide. `brew install imagemagick` (mac) /
257
+ `apt install imagemagick` (Linux).
258
+ - **Multi-DB project** — `Probe.run[:databases]` returns the list. The gem
259
+ itself is stack-agnostic; the docs are if probe drives them.
260
+ - **Rails Engine host** (`Probe.run[:engine_host] == true`) — most of the
261
+ install steps still apply; auth wiring may not. The gem hasn't been
262
+ exercised against Engine hosts as much as Application hosts; treat
263
+ results with a probe in hand.
264
+
265
+ ## 10. Cross-doc index (when to read what)
266
+
267
+ | Doc | When to read |
268
+ |---|---|
269
+ | `.claude/docs/architecture.md` | Designing a new feature; need the Concepts Pattern in depth. |
270
+ | `.claude/docs/forms.md` | Form has virtual attributes, sub-operation calls, or multi-record submits. Promote to `<Concept>::Form`. |
271
+ | `.claude/docs/concepts-refactoring.md` | Migrating a legacy controller to the `endpoint` shape. |
272
+ | `.claude/docs/companions.md` | Choosing which `:companions` group to install or skip. |
273
+ | `.claude/docs/api-endpoints.md` | Building an HTTP API with `api_endpoint` + ParamsAdapters. |
274
+ | `.claude/docs/ui-components.md` | UI helpers — `Base::Component::Table`, `modal()`, `header()`. |
275
+ | `.claude/docs/stimulus-controllers.md` | Stimulus listener cleanup, action/target naming. |
276
+ | `.claude/docs/code-style.md` | Ruby style, I18n key format, git workflow. |
277
+ | `.claude/docs/testing.md` + `testing-examples.md` | Spec patterns; what NOT to test. |
278
+ | `.claude/docs/commands.md` | Project commands (bin/dev, test, lint). |
279
+ | `.claude/docs/modal-refactoring.md` | Migrating raw Bootstrap modals to the `modal()` helper. |
280
+ | **this file (`tsykvas_rails_template.md`)** | Big-picture gem reference; how generators / Probe / fences / slash commands fit together. |