Rules any code-generation agent must follow when editing this repository. Keep changes minimal, validated, and consistent with existing patterns.
- Prefer surgical edits. Don’t reformat unrelated code. Preserve public APIs unless required.
- Source of truth is the code: read actual files, signatures, and call sites before changing anything.
- Think before acting: understand root causes; don’t treat symptoms.
- Ruby 3.4 + Rails 8.1
- No-build frontend: Importmap + tailwindcss-rails (Tailwind v4)
- Use
bin/devfor local dev (Rails + Tailwind watcher + GoodJob) - Do not introduce Node/Yarn or JS bundlers
- Tests:
bundle exec rails test(all) or targeted files - System tests:
bundle exec rails test:system - Coverage:
COVERAGE=1 bundle exec rails test - Lint:
bin/rubocop - ERB lint:
bundle exec erb_lint --lint-all - Assets:
RAILS_ENV=production bin/rails assets:precompile - Data:
bin/rails data:plans:seed
- Keep diffs surgical; don’t shuffle files or reformat unrelated code.
- Add/update minimal tests when behavior changes.
- Use i18n keys for user-facing copy (no hardcoded UI strings).
- Keep WebMock enabled in tests; do not allow live HTTP.
- New dependencies (gems / JS packages / tooling).
- Database migrations or schema changes.
- Changes to provider contracts, auth/authorization, billing, or security boundaries.
- Editing Rails credentials / secrets handling.
- Commit secrets/credentials, master keys, or decrypted values.
- Add Node/Yarn/bundlers to the stack.
- Add debug prints/breakpoints in app/runtime code (
puts,pp,binding.pry).- Exception: intentional CLI output in Rake tasks is acceptable.
- Use Conventional Commits.
- Subject line ≤ 80 chars, imperative, no trailing period.
- Body (when non-trivial): explain why and how (not what).
- Types:
feat,fix,refactor,chore,test,docs.
app/: Rails MVC, jobs, services (legacy), views.lib/: External providers and infra (lib/providers,lib/faraday,lib/rack).- Data in
lib/data/(e.g.,plans.yml). Rake tasks inlib/tasks/.
- Data in
lib/providers: Provider interfaces/results + implementations. UseProviders::Page/Providers::Reviewat boundaries.- Webhook resources via
Providers::Resources(:page,:review).
- Webhook resources via
test/: Minitest (integration/,system/, etc.)- VCR cassettes:
test/support/cassettes/ - Webhook captures:
test/support/webhook_captures/
- VCR cassettes:
- Controllers: root under
app/controllers/; namespaced in matching folders.- Base classes:
PublicBaseController,DashboardBaseController,Admin::BaseController.
- Base classes:
- Is the change trivial (typo, small conditional, tiny refactor)?
- Yes → keep the existing Actor/service shape.
- No → convert the touched service to a PORO as part of the change.
- When converting:
- Prefer explicit constructors + methods (
ThingCreator.new(...).create) - Or move behavior onto the relevant model when it naturally belongs there.
- Prefer explicit constructors + methods (
- Are you adding new UI/shared UI?
- Yes → use Rails partials under
app/views/shared/(layout-aware; see below).
- Yes → use Rails partials under
- Are you touching an existing ViewComponent?
- Small change → keep it as-is.
- Meaningful change → prefer migrating it to a partial as part of the change.
- Routing: keep routes simple and close to Rails defaults.
- Avoid redundant
defaults:/controller:overrides when convention can solve it. - Avoid
:asoverrides unless strictly necessary. - Prefer
namespace+resources. - Prefer REST/CRUD: introduce a resource instead of custom actions.
- Avoid redundant
Directional rules. Apply them to new code and methods/files you already touch. Do not do churn-only refactors solely to “match style”.
- Prefer expanded conditionals over guard clauses.
- Exception: early return at the very start of a method when the main body is non-trivial.
- Method ordering in classes:
- class methods
- public methods (with
initializefirst) - private methods
- Order methods vertically by invocation order (top-down flow).
- Only use
!when there is a corresponding non-!method. - No blank line after visibility modifiers; indent methods under them.
- Thin controllers invoking rich domain APIs; avoid introducing “service artifacts” as glue.
- Jobs should be shallow: enqueue via
*_later, execute logic in*_now/ domain methods.
We prefer code to break loudly when something unexpected happens.
- Avoid “defensive” checks that mask errors:
respond_to?,try, dynamicsendto avoid using the real API- broad
rescuethat swallows exceptions - returning nil/false as a fallback for unexpected states
- Prefer explicit contracts:
find_by!instead offind_bywhen it must existHash#fetchwhen a key must exist
- Rescue only specific exceptions you truly handle, and keep handling intentional.
- Current state: there are service objects using the Actor gem.
- Direction: migrate toward POROs inline with Rails/DHH style.
- Do not add new Actor-based services.
- Use the decision tree above to decide when to migrate.
- Do not add new ViewComponents.
- Prefer Rails partials under
app/views/shared/. - Because we have 3 layouts (
public,application,admin), prefer layout-aware shared folders:app/views/shared/public/…app/views/shared/application/…app/views/shared/admin/…
- Pass data via
locals; avoid relying on instance vars in shared partials.
- Less is more: prefer less code without sacrificing readability.
- Use clear, intention-revealing names; avoid vague names like
Manager,Handler. - Performance:
- Avoid N+1 (
includes,preload). - Use DB constraints/indexes for new query patterns.
- Batch processing for large datasets (
find_each,insert_all).
- Avoid N+1 (
- Dependencies:
- Do not add new dependencies for trivial tasks.
- Prefer stdlib / Rails built-ins already in the stack.
- Tailwind v4 tokens live in
app/assets/tailwind/application.css; use theme variables and brand classes. - Forms baseline: “Labels on left” layout.
- Stimulus-first:
- Prefer controllers from
tailwindcss-stimulus-componentsand@stimulus-components/*. - New controllers must be generic, reusable, under
app/javascript/controllers/, and registered incontrollers/index.js.
- Prefer controllers from
- Copy must follow
docs/brand.md. Marketing pages must followdocs/marketing/style-guide.md. - No inline styles (
style="..."). Use Tailwind utilities. - Prefer semantic HTML and accessibility basics (labels, autocomplete where appropriate).
- Mailer views live under
app/views/mailers/. - Shared layout:
app/views/layouts/mailer.html.erb - Prefer partials under
app/views/mailers/for shared sections. - Use
premailer-railsto inline styles. - Set
deliver_later_queue_nameto:latency_5min mailers.
- Tests must cover application behavior, not framework behavior.
- Tests must validate observable outcomes (rendered content, redirects, DB changes, enqueued jobs, outbound payloads).
- Avoid:
- tautological tests (re-implementing method logic in the test)
- “Rails works” tests
- status-only assertions with no behavioral check
- heavy stubbing that can’t catch regressions
- Framework: Minitest. Fixtures only (factories have been removed). Do not add factories.
- Anatomy: setup (optional); exercise; assertions; cleanup (optional). Leave an empty line between sections.
- Controller/system tests must assert status/redirect AND content (selectors/text) or side effects.
- System tests: use
data-test-idselectors; adddata-test-idattributes in views when needed. - Keep review pages small in tests (~10) for speed and deterministic assertions.
- WebMock enabled.
- Outgoing API calls: recorded with VCR (see Project map for cassette location).
- VCR structure mirrors provider/API path segments.
- Filename:
{platform}_{resource}[optional_suffix].yml.
- Incoming webhooks: do not VCR. Use JSON captures (see Project map).
Canonical pattern (webhook fixture ingestion):
test "DataForSEO resolve with async mode" do
VCR.use_cassette("dataforseo/serp_google_maps_task_post/google_maps_page_resolve") do
result = Providers::Dataforseo::GoogleMaps::Page.new(
mode: :async,
recording: VCR.current_cassette.recording?,
platform: :google_maps
).submit(
place_eid: @page.place_eid,
business_name: @location.name,
country: "GB"
)
assert_equal :scheduled, result.status
ExternalTask.create!(
record_uuid: @page.uuid,
record_type: @page.class.name,
provider: Providers::External::DATAFORSEO,
resource: Providers::Resources::PAGE,
eid: result.eid
)
payload = WebhookCapture.read(
provider: Providers::External::DATAFORSEO,
platform: :google_maps,
resource: Providers::Resources::PAGE,
task_eid: result.eid
)
assert_not_nil payload
response = Providers::Dataforseo::GoogleMaps::Page.new(
mode: :async,
platform: :google_maps
).ingest(payload)
assert_equal :ok, response.status
assert_equal "ChIJrVtdwkDzdkgRHgNW25ELRtQ", response.data.place_eid
end
end