# backend/AGENTS.md

## Scope

These rules apply to work under `backend/`.

This is a Rails API app using Active Record, RSpec, request specs,
service objects, representation classes, and version tables for post/tag/wiki
history.

## Commands

Use commands backed by files and dependencies in this directory:

```sh
bin/setup
bin/dev
bin/rails
bin/rake
bin/rubocop
bin/brakeman
bundle exec rspec
```

Common checks:

```sh
bundle exec rspec
bin/rubocop
bin/brakeman
```

Common Rails commands:

```sh
bin/rails db:prepare
bin/rails db:migrate
bin/rails routes
bin/rails server
```

After backend behavior changes, run the relevant RSpec files. For broad backend changes, run:

```sh
bundle exec rspec
```

If a command cannot be run or fails, report the exact command and failure.

Do not create, modify, or run tests unless the user explicitly asks for test
work. When the user asks for tests, keep working and rerun them until they
pass or the remaining failure is clearly blocked.

## Rails structure

- `app/controllers`: API controllers.
- `app/models`: Active Record models and concerns.
- `app/representations`: JSON response shaping.
- `app/services`: domain services such as version recorders, wiki commit,
  YouTube sync, and similarity calculation.
- `config/routes.rb`: public API routes.
- `db/migrate`: migrations.
- `db/schema.rb`: schema snapshot.
- `lib/tasks`: custom Rake tasks.
- `spec`: RSpec tests.

Before changing behavior, inspect the matching route, controller, model,
service, representation, and spec.

## Shared backend systems

Before adding backend behaviour, search the existing backend first. At minimum,
check these locations:

- `app/controllers`
- `app/controllers/concerns`
- `app/models`
- `app/models/concerns`
- `app/representations`
- `app/services`
- `app/services/*`
- `app/jobs`
- `lib`
- `lib/tasks`
- `config/initializers`

Do not infer commonality from directory names alone. Read the actual
responsibility and representative usage sites.

### Controller reuse

Before adding logic to a controller, inspect:

- `ApplicationController` authentication, authorization, BAN, and IP BAN
- existing render and validation-error helpers
- existing param parsing
- controller concerns
- the controller for the same resource
- existing services
- existing representations

Keep controllers focused on:

- authentication and authorization
- parameter intake
- service and model invocation
- HTTP status selection
- representation selection

Do not reimplement these per controller when an existing path already owns
them:

- authentication and role checks
- validation error JSON
- URL normalisation
- tag normalisation
- thumbnail handling
- version recording
- complex transactions
- external HTTP fetching
- response representation assembly

### Authentication, authorization, and BAN

Treat these as the canonical backend entrypoints:

- `ApplicationController#authenticate_user`
- `current_user`
- `X-Transfer-Code`
- `reject_banned_ip_address!`
- `reject_banned_user!`
- `gte_member?`
- `admin?`

Do not create feature-local permission services, role comparisons, or header
parsing when the existing authentication boundary already owns the behaviour.
If the current boundary is insufficient, extend it minimally instead of adding
another permission path.

### Representations

If an endpoint for the same resource already uses `app/representations`, do not
assemble a separate JSON shape directly inside the controller without first
checking the existing representation contract.

Inspect at least:

- `PostRepr`
- `TagRepr`
- `MaterialRepr`
- `TheatreRepr`
- `UserRepr`
- `WikiPageRepr`
- `DeerjikistRepr`

When a lightweight response is genuinely different in purpose, keep it
deliberate and compatible with the surrounding contracts. Do not force every
identifier list into a large representation, but do not fork the same resource
shape casually either.

### Domain services

When work touches multiple models, transactions, external APIs, file handling,
history creation, or multi-step workflow, search `app/services` first.

At minimum, search for existing services in these responsibility areas:

- version recorder and versioning
- wiki commit
- YouTube or Google Drive API client
- material sync or ZIP export
- similarity calculation
- theatre selection or skip finalisation
- metadata, thumbnail, or file processing
- URL normaliser or sanitisation
- import or export
- preview safety or HTTP fetch

Do not create a same-responsibility service under another namespace or another
name. If an existing service is close, extend that API minimally instead of
wrapping it in a feature-local service.

### Versioning

When a feature writes history, snapshots, or restore roots, search the existing
versioning path first. At minimum, inspect:

- `VersionRecorder`
- `PostVersionRecorder`
- `TagVersionRecorder`
- `TagVersioning`
- `MaterialVersionRecorder`
- `NicoTagVersionRecorder`
- `WikiVersionRecorder`

Do not implement history writes in controllers, callbacks, or ad hoc feature
services when the recorder layer already owns the transaction boundary and
meaning.

### Normalisation, sanitisation, and parsing

For URLs, tag names, times, video durations, identifiers, and paths, search the
existing normaliser, sanitisation rule, parser, and model-callback path first.

Do not let frontend, controller, service, and model each invent different rules
for the same value. Use one canonical normalisation path and keep input
validation distinct from pre-persistence normalisation.

### External HTTP and URL safety

When fetching external URLs, reuse the existing preview-safety stack. Search at
least for:

- URL safety
- redirect validation
- response size limits
- timeouts
- network failure mapping
- HTML metadata extraction
- known-site extraction
- thumbnail fetching

Do not add direct `Net::HTTP`, `Faraday`, or equivalent feature-local HTTP code
that reimplements SSRF checks, redirect restrictions, size limits, or timeouts.
If the current fetcher is insufficient, extend its existing safety contract.

### Storage, files, and Active Storage

When handling files, thumbnails, ZIP output, object storage, or Active Storage
blobs, inspect existing storage helpers, exporters, thumbnail generators, and
checksum helpers first. Do not reimplement the same attach, export path,
download, resize, or checksum flow in a controller or one-off service.

### Concerns

Do not create controller or model concerns merely because some code is shared.
Use a concern only when multiple classes share the same lifecycle, macro,
callback, or tightly cohesive behaviour. Utility collections belong in explicit
objects or services, not in `CommonConcern`, `SharedMethods`, or `Utils`.

### Model boundaries

Model-specific invariants, associations, validations, and normalisation may
live in the model. Multi-model workflow, external access, complex transaction
flow, and feature orchestration belong in services. Do not hide feature
workflow in model callbacks.

### Transactions, locking, and race handling

If transactions, locking, idempotency, or race recovery already exist in a
service or model method, do not add a second implementation in a controller or
new service. Inspect the existing transaction boundary first, avoid wrapping
the same operation in needless nested transactions, and handle unique-constraint
races according to the target constraint's business meaning.

## Ruby style

- Prefer precise, minimal changes.
- Use single quotes unless interpolation or escaping makes double quotes better.
- Do not put a space before Ruby method-call parentheses.
- For `render`-family method calls, omit parentheses even when passing
  keyword arguments.
- Never put a line break immediately before `)` in Ruby.
- Do not use `%w` or `%i` in new Ruby code.
- Never write a Ruby line longer than 99 characters.
- Aim to keep Ruby lines within 79 characters where practical.
- For small Ruby method definitions that take keyword arguments, match the
  local no-parentheses style when nearby code uses it.
- When an `if` condition is split across multiple lines and combines clauses
  with `&&` or `||`, wrap the whole condition in parentheses.
- Treat Ruby hash `{ ... }` style and Ruby block `{ ... }` style as separate
  rules.
- Do not format Ruby hashes like Ruby blocks.
- For Ruby hashes, keep the closing `}` on the same line as the final pair.
- Keep the first pair on the same line as `{` by default.
- Short Ruby hashes may stay visually compact across two lines with the first
  pair kept on the opening line and aligned continuation pairs below it.
- If the hash would exceed the line limit, break after `{` and indent pairs
  by 4 spaces.
- Put one logical pair per line when the expression would otherwise become
  dense.
- For Ruby arrays, never put whitespace or a line break immediately before `]`.
- Keep the first element on the same line as `[` by default.
- If an array would exceed the line limit, break after `[` and indent
  elements by 4 spaces.
- For Ruby blocks, use 2-space indentation for the block body.
- Keep comments short and useful; avoid narrating obvious code.
- Do not add production dependencies without approval.

## Authentication and authorization

- Authentication is handled through the `X-Transfer-Code` header in
  `ApplicationController#authenticate_user`.
- `current_user` is set by looking up `User.inheritance_code`.
- Do not bypass or weaken the `X-Transfer-Code` flow unless the task
  explicitly changes authentication.
- Unauthenticated write actions should return `:unauthorized` consistently
  with existing controllers.
- Role checks use `User` enum roles: `guest`, `member`, and `admin`.
- Use `current_user.gte_member?` for member-or-admin write permissions where
  existing controllers do so.
- Use `current_user.admin?` only for admin-only paths, such as tag child relationship changes.
- Do not replace role checks with looser presence checks.

## BAN and IP BAN

- `ApplicationController` runs these before actions in order:
  - `reject_banned_ip_address!`
  - `authenticate_user`
  - `reject_banned_user!`
- User and IP bans use `banned_at`, not a boolean `banned` column.
- `User#banned?` and `IpAddress#banned?` check `banned_at.present?`.
- Do not weaken BAN or IP BAN behavior.
- If changing request authentication or controller before actions, add or
  update request specs covering banned users and banned IP addresses only when
  the user explicitly asks for tests.

## RSpec

- Prefer RSpec for new backend tests.
- Put API behavior coverage under `spec/requests`.
- Put model behavior under `spec/models`.
- Put service behavior under `spec/services`.
- Put Rake task coverage under `spec/tasks`.
- `spec/rails_helper.rb` loads `spec/support/**/*.rb`.
- Request specs include `AuthHelper` and `JsonHelper`.
- `AuthHelper#sign_in_as(user)` stubs
  `ApplicationController#current_user`; use it when matching existing
  request spec style.
- Add or update request specs for API behavior changes only when the user
  explicitly asks for tests, especially status codes, permissions, response
  shape, and version conflict behavior.

## Migrations

- Keep migrations and `db/schema.rb` consistent.
- Use reversible migrations where practical; otherwise define explicit `up` and `down`.
- For data backfills inside migrations, follow the existing pattern of
  defining migration-local `ActiveRecord::Base` classes with
  `self.table_name`.
- Preserve existing indexes, foreign keys, check constraints, and null constraints.
- Be careful with MySQL-specific options already present in migrations, such as `after:`.
- Do not edit old migrations just to change current behavior unless
  explicitly requested; add a new migration.

## Version tables

- Versioned records include posts, tags, nico tags, and wiki pages.
- Current records have `version_no`; version tables have positive
  `version_no` with unique indexes scoped to the parent record.
- Version event types are `create`, `update`, `discard`, and `restore`.
- Version rows are readonly through the `VersionRecord` concern.
- Use the existing recorder services instead of manually inserting version
  rows in application code:
  - `PostVersionRecorder`
  - `TagVersionRecorder`
  - `NicoTagVersionRecorder`
  - `WikiVersionRecorder`
  - `TagVersioning`
- `VersionRecorder` locks the current record, validates sequence consistency,
  skips unchanged update snapshots, creates the next version row, and updates
  the record `version_no`.
- Do not update versioned records without considering whether a version snapshot must be created.
- For optimistic concurrency paths, preserve `base_version_no`, `force`, and
  `merge` semantics. Cover conflicts in request specs only when the user
  explicitly asks for tests.

## Domain cautions

- Posts have tag snapshots, parent post implications, original-created ranges,
  viewed state, and version conflict behavior.
- Tags have canonical names, aliases through `TagName`, categories, parent
  implications, discard behavior, and version snapshots.
- Nico tags have separate relation/version behavior; do not treat them like
  normal editable tags without checking existing code.
- Wiki pages involve page content, revisions/history, version rows,
  title/tag-name behavior, and diff/restore paths.
- Materials, theatres, and comments have user and permission checks; inspect
  the controller before changing them.

## API responses

- Use representation classes under `app/representations` when existing endpoints do.
- Keep response keys consistent with existing JSON contracts.
- Frontend code expects camelCase conversion client-side, while Rails params
  and JSON keys are generally snake_case.
- Preserve existing HTTP status conventions:
  `:unauthorized` for no user, `:forbidden` for insufficient role or banned
  user, `:not_found` for missing records, and `:unprocessable_entity` for
  validation failures.
- For diagnostic or internal helper JSON, prefer a deliberately light response
  shape over full representation classes when callers only need identifiers,
  labels, URLs, or weights.

## Active Record performance

- When a controller action serializes nested associations, preload the
  associations it will touch instead of allowing N+1 queries.
- Be sensitive to N+1 queries in all backend work.
- Avoid introducing N+1 queries, and proactively fix existing N+1 issues when
  you find them in the code path you are editing.
- When an association may already be preloaded, prefer loaded-association
  checks that reuse the preloaded data without losing the efficient database
  path.

## Files to avoid in routine work

- Do not inspect or edit `tmp/`, `log/`, `storage/`, `vendor/`, or dependency
  directories unless explicitly needed.
- Do not modify generated schema or migration output without the corresponding
  migration when schema changes are made.
