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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +200 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +589 -0
- data/Rakefile +17 -0
- data/lib/generators/tsykvas_rails_template/companions/companions_generator.rb +273 -0
- data/lib/generators/tsykvas_rails_template/concept/concept_generator.rb +145 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/index.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/index.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/new.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/new.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/show.html.slim.tt +4 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/show.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/controller.rb.tt +45 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/create.rb.tt +31 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/destroy.rb.tt +13 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/edit.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/index.rb.tt +9 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/new.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/show.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/update.rb.tt +31 -0
- data/lib/generators/tsykvas_rails_template/install/bootstrap_installer.rb +225 -0
- data/lib/generators/tsykvas_rails_template/install/install_generator.rb +298 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/buddy.md +157 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/code-reviewer.md +117 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/security-reviewer.md +113 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/tech-lead.md +150 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/check.md +51 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/code-review.md +60 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/docs-create.md +102 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pr-review.md +81 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pushit.md +160 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/refactor.md +132 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/task-sum.md +47 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tests.md +67 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tsykvas-claude.md +262 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-docs.md +78 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-rules.md +102 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-tests.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/architecture.md +315 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/authentication.md +96 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/background-jobs.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/code-style.md +101 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/commands.md +34 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/companions.md +128 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/concepts-refactoring.md +194 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/database.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/deployment.md +138 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/design-system.md +322 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/documentation.md +89 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/forms.md +174 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/i18n.md +165 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/routing-and-namespaces.md +114 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/security.md +122 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/stimulus-controllers.md +166 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing-examples.md +180 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing.md +117 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/tsykvas_rails_template.md +280 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/ui-components.md +196 -0
- data/lib/generators/tsykvas_rails_template/install/templates/CLAUDE.md.tt +81 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/component/base.rb +6 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/base.rb +124 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/result.rb +56 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.html.slim +49 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.rb +11 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/operation/index.rb +17 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/concerns/operations_methods.rb +148 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/home_controller.rb +10 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/policies/application_policy.rb +33 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/policies/home_policy.rb +8 -0
- data/lib/tasks/tsykvas.rake +11 -0
- data/lib/tsykvas_rails_template/probe.rb +236 -0
- data/lib/tsykvas_rails_template/railtie.rb +13 -0
- data/lib/tsykvas_rails_template/version.rb +5 -0
- data/lib/tsykvas_rails_template.rb +18 -0
- 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
|
data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/tsykvas_rails_template.md
ADDED
|
@@ -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. |
|