412 行
14 KiB
Markdown
412 行
14 KiB
Markdown
# 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.
|