ファイル
btrc-hub/AGENTS.md
T
みてるぞ 776dea87d9 素材管理 (#306) (#381)
開発環境では、**DB を壊さない前提で、migration → API → 画面 → 同期 → ZIP → 履歴**の順に見るのがよいです。今回の差分は素材管理全体に触っているので、単体でチョンチョン見るより、素材の一生を通すのが早いです。

## 0. 先に方針

**やらないこと:**

```sh id="snb3i6"
rails db:drop
rails db:reset
rails db:setup
DISABLE_DATABASE_ENVIRONMENT_CHECK=1 ...
```

これは禁止。
開発 DB に本番データを入れているなら、床板を剥がして耐震確認するようなものです。

---

## 1. migration 確認

まず現在の状態を見る。

```sh id="qsdfpt"
cd backend
RAILS_ENV=development bundle exec rails db:migrate:status
```

その後、通常 migration。

```sh id="a89fir"
RAILS_ENV=development bundle exec rails db:migrate
```

見るポイント:

```txt id="c1u57a"
materials に source_* / normalized_source_key / version_no がある
material_versions に event_type / file snapshot / source snapshot がある
material_export_items がある
material_sync_suppressions がある
material_sync_sources がある
既存 materials に material_versions version_no=1 create が backfill されている
```

確認用:

```sh id="g4vd4m"
RAILS_ENV=development bundle exec rails runner '
puts "materials=#{Material.count}"
puts "versions=#{MaterialVersion.count}"
puts "materials without versions=#{Material.left_joins(:material_versions).where(material_versions: { id: nil }).count}"
puts "sync suppressions table=#{ActiveRecord::Base.connection.table_exists?(:material_sync_suppressions)}"
'
```

ここで `materials without versions=0` になれば、backfill は通っています。

---

## 2. 既存素材一覧の画面確認

フロントを起動して `/materials` を見る。

```sh id="vl28jd"
cd frontend
npm run dev
```

見る観点:

```txt id="i2ovxw"
初期表示で素材が出る
初期表示ではグルーピングがオフ
タグなし素材も出る
カード表示でサムネまたは代替テキストが出る
一覧表示に切り替えられる
q / tag_state / media_kind / sort / direction が効く
```

ここでまず、普通の素材一覧が壊れていないことを確認します。

---

## 3. 左タグバーの確認

`/materials` を開いて左タグバーからタグを選ぶ。

見る観点:

```txt id="n6udul"
URL が tag_id=...&include_descendants=1&group_by=parent_tag になる
選択中タグが左バーで強調される
一覧上部に「選択中」の表示が出る
「タグ選択を解除」で通常表示に戻れる
解除後、tag_id / include_descendants / group_by / page が消える
子タグ・孫タグの素材も一覧に出る
親タググルーピングされる
```

特に重要なのはこれ。

```txt id="d33os0"
親タグ A を選択
  A に直接紐づく素材
  A > B に紐づく素材
  A > B > C に紐づく素材
が同じ一覧に出ること
```

---

## 4. 選択タグから素材追加

左タグからタグを選択した状態で、一覧上部の **このタグに素材を追加** を押す。

見る観点:

```txt id="qdd5uw"
素材追加画面の tag 欄に選択中タグ名が初期入力されている
file または url を指定して保存できる
保存後 return_to で元のタグ選択済み一覧に戻る
戻った一覧に追加した素材が出る
material_versions に create が 1 件できる
```

Rails console でも確認できます。

```sh id="i07rrb"
RAILS_ENV=development bundle exec rails runner '
m = Material.order(id: :desc).first
puts({ id: m.id, tag: m.tag&.name, versions: m.material_versions.count, version_no: m.version_no }.inspect)
'
```

---

## 5. グループ見出しから素材追加

親タググルーピング表示中に、各グループ見出しの **このタグに素材を追加** を押す。

見る観点:

```txt id="fpx8w4"
グループタグ名が tag 欄に初期入力される
保存後、元の一覧に戻る
追加素材がそのグループ内に出る
```

ここは今回の導線の肝です。棚の見出しから直接その棚へ素材を置けるかを見る。

---

## 6. 素材更新と履歴

既存素材の詳細または編集導線から、タグ・URL・ファイル・export path を更新する。

見る観点:

```txt id="w36i61"
更新前 snapshot が無ければ create が補われる
更新後 update version ができる
file_blob_id / file_filename / file_sha256 が material_versions に入る
export_paths_json が履歴に残る
/materials/changes または /materials/versions で履歴が見える
```

console:

```sh id="a3okhv"
RAILS_ENV=development bundle exec rails runner '
m = Material.order(updated_at: :desc).first
puts m.material_versions.order(:version_no).map { |v|
  [v.version_no, v.event_type, v.tag_name, v.file_filename, v.file_sha256, v.export_paths_hash]
}.inspect
'
```

---

## 7. サムネイル

画像素材を追加して、一覧にサムネイルが出るか確認。

動画素材があるなら、`ffmpeg` が入っている環境で backfill。

```sh id="qxm8fj"
cd backend
RAILS_ENV=development bundle exec rails materials:thumbnails:backfill
```

見る観点:

```txt id="s9a1vf"
画像は 180x180 のサムネが付く
動画はフレームからサムネが作られる
非対応ファイルは代替テキスト表示になる
ログに result が出る
```

---

## 8. ZIP export

export path がある素材を用意して、ブラウザで確認。

```txt id="aq7f7o"
/materials/download.zip?profile=legacy_drive
```

見る観点:

```txt id="h4rve5"
ZIP が落ちる
entry path が material_export_items.export_path になる
disabled な export item は入らない
ファイル実体が欠けている場合は 422 と missing_files が返る
```

---

## 9. 抑止

`/materials/suppressions` で path prefix 抑止を追加する。

見る観点:

```txt id="w80jbu"
member で作成できる
guest は forbidden / unauthorized
google_drive_path_prefix で既存素材が discard される
discard 履歴が material_versions に残る
同期時に同じ source_path 配下が再作成されない
```

---

## 10. Google Drive 同期

開発環境では、まず小さいフォルダでやるのがよいです。
いきなり本番素材集フォルダを食わせると、ログが藪になります 🌿

必要な ENV:

```sh id="kzbb7f"
GOOGLE_DRIVE_SERVICE_ACCOUNT_EMAIL=...
GOOGLE_DRIVE_PRIVATE_KEY_PATH=...
MATERIAL_SYNC_SOURCE_KIND=google_drive_path
MATERIAL_SYNC_SOURCE_FILE_ID=<folder_id>
MATERIAL_SYNC_SOURCE_NAME=dev-small-folder
MATERIAL_SYNC_SOURCE_PROFILE=legacy_drive
```

seed で source 作成、または console で作成。

```sh id="sec40c"
RAILS_ENV=development bundle exec rails db:seed
RAILS_ENV=development bundle exec rails materials:sync
```

見る観点:

```txt id="a5ltqu"
imported / updated / unchanged / suppressed / failed がログに出る
2 回目実行で unchanged が増える
tag は nil のままでも保存できる
人手で tag / url を付けた既存同期素材が、再同期で消えない
Google native file は skip される
download 後 sha256 block が効く
```

---

## 11. schema.rb は別途確認

これはテストというより merge gate です。
今回まだ怪しいので、差分に以下が混ざっていないことを確認します。

```txt id="s3k6da"
wiki_assets 削除
wiki_pages.next_asset_no 削除
素材管理と無関係な CHECK constraint 削除
素材管理と無関係な index order 消失
```

ここが残るなら、機能テストが通っても merge は止めた方がいいです。

---

## 最小テスト順

時間がないなら、この順で十分です。

```txt id="c1k2pw"
1. db:migrate
2. materials without versions = 0 を確認
3. /materials 初期表示
4. 左タグ選択 → 子孫込み表示 → グルーピング
5. タグ選択解除
6. 選択タグから素材追加 → return_to で戻る
7. グループ見出しから素材追加
8. 更新して material_versions を確認
9. ZIP export
10. 小さい Drive folder で materials:sync を 2 回
```

これで、今回の差分の主要な導線はほぼ踏めます。

Reviewed-on: #381
Co-authored-by: miteruzo <miteruzo@naver.com>
Co-committed-by: miteruzo <miteruzo@naver.com>
2026-06-28 06:35:53 +09:00

14 KiB

AGENTS.md

Project overview

BTRC Hub / タグ広場 is a split Rails API and React frontend repository.

  • Backend: Rails API under backend/.
  • Frontend: React + TypeScript + Vite under frontend/.
  • Docs: lightweight command notes under docs/.
  • There is no README or Makefile at the repository root as of this inspection.

Stack

  • Backend: Ruby 3.2.2 from backend/.ruby-version, Rails ~> 8.0.2.
  • Backend dependencies include mysql2, sqlite3, rspec-rails, factory_bot_rails, rack-cors, jwt, discard, gollum, whenever, aws-sdk-s3, brakeman, and rubocop-rails-omakase.
  • Frontend: React ^19.1.0, TypeScript ~5.8.3, Vite ^6.3.5.
  • Frontend data/UI dependencies include Axios, TanStack Query, Tailwind CSS, Framer Motion, Radix UI components, lucide-react, MDX/Markdown tooling, and Zustand.

Main directories

  • backend/app/controllers: Rails API controllers.
  • backend/app/models: Active Record models.
  • backend/app/representations: API response representation classes.
  • backend/app/services: domain services such as version recording, wiki commit, YouTube sync, and similarity calculation.
  • backend/config/routes.rb: API routes.
  • backend/db/migrate: migrations.
  • backend/db/schema.rb: current schema snapshot.
  • backend/lib/tasks: custom Rake tasks.
  • backend/spec: RSpec tests.
  • backend/test: Rails minitest files that still exist in the tree.
  • frontend/src/App.tsx: frontend route definitions and initial user setup.
  • frontend/src/pages: page-level React components.
  • frontend/src/components: shared and feature components.
  • frontend/src/lib: API client helpers, query keys, prefetchers, and domain helpers.
  • frontend/src/stores: Zustand stores.
  • docs/commands.md: command notes.

Commands

Only list commands that are backed by files inspected in this repository.

Backend

The following binstubs exist under backend/bin:

cd backend
bin/setup
bin/dev
bin/rails
bin/rake
bin/rubocop
bin/brakeman
bin/kamal
bin/thrust

Common Rails/Rake usage through existing binstubs:

cd backend
bin/rails db:prepare
bin/rails db:migrate
bin/rails routes
bin/rails server
bin/rake
bin/rubocop
bin/brakeman

RSpec is present in Gemfile and .rspec exists:

cd backend
bundle exec rspec

Frontend

The following npm scripts exist in frontend/package.json:

cd frontend
npm run dev
npm run build
npm run lint
npm run test
npm run test:run
npm run preview

npm run build runs tsc -b && vite build, then postbuild runs node scripts/generate-sitemap.js.

npm run test runs Vitest in watch mode. Use npm run test:run for a non-watch frontend test run.

Coding style

  • Prefer precise, minimal changes.
  • Do not flatter or over-explain.
  • Explain risks directly.
  • Prefer single quotes for strings unless interpolation or escaping makes double quotes better.
  • Ruby: never put a space before method-call parentheses.
  • Ruby: render 系メソッド呼び出しでは、keyword 引数付きでも括弧を書かない。
  • Ruby: never put a line break immediately before ).
  • Ruby: do not use %w or %i.
  • In Ruby, when an if condition is split across multiple lines and combines clauses with && or ||, wrap the whole condition in parentheses.
  • Ruby hashes are not blocks; keep } on the same line as the final pair.
  • Ruby hashes keep the first pair on the same line as { unless line length requires a break.
  • 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.
  • Ruby blocks use separate { ... } rules from hashes, with 2-space body indentation.
  • For 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.
  • TypeScript and Python: use GNU-style spacing before parentheses where syntactically valid.
  • Never write Ruby, TypeScript, or TSX lines longer than 99 characters.
  • Aim to keep Ruby, TypeScript, and TSX lines within 79 characters where practical.
  • TypeScript and TSX use 4-space logical indentation.
  • In TypeScript and TSX only, replace every leading run of 8 spaces with a tab.
  • Tabs are only for leading indentation, never for spaces after non-space text.
  • TypeScript and TSX imports may stay on one line if they remain within the line limit; do not expand short type-only imports mechanically.
  • In TypeScript and TSX, when breaking a line at an operator, break before the operator and put the operator at the beginning of the next line. A trailing operator at end of line is unacceptable. This rule does not apply to Ruby, where it can change the syntactic structure.
  • In TypeScript and TSX, when a function takes one destructured object argument plus an inline type, prefer this shape when it fits locally:
const helper = (
    { value, flag }: { value: string
		       flag:  boolean },
): Result => {
  // ...
}
  • In TypeScript and TSX, put switch case block braces on their own lines when a case needs a lexical block:
case 'yes':
case 'no':
  {
    const expected = valueFor (item)
    return expected == null || expected === answer
  }
  • In TypeScript and TSX, use value == null and value != null as the default nullish checks. Do not use === null, === undefined, !== null, or !== undefined.
  • If code appears to need a distinction between null and undefined, treat that as a design smell and revise the logic to avoid the distinction. External library APIs that explicitly require distinguishing the two are the only exception.
  • In TypeScript and TSX, keep short arrays on one line when they fit under the line limit; break arrays only when readability or line length requires it.
  • In TypeScript and TSX, when a ternary expression is split across multiple lines, align ? and : with the condition expression. Do not indent ? and : one extra level under the condition.
const value =
    condition
    ? consequent
    : alternate
  • In TypeScript and TSX, keep short ternary expressions on one line when they fit cleanly under the line limit.
  • In TypeScript and TSX, prefer ternary expressions for simple conditional value selection. Do not replace a clear ternary with if statements, and do not introduce immediately invoked functions just to avoid or reformat a ternary expression.
  • In TypeScript and TSX, do not write let followed by later if assignments when the value can be expressed as a single const initializer. Prefer const because it prevents accidental later reassignment.
  • When fixing formatting, change formatting only. Do not change expression structure, control flow, or variable mutability unless the requested style explicitly requires it.
  • Do not add production dependencies without explicit approval.
  • 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.

Backend rules

  • Inspect existing routes, controllers, models, services, and specs before editing backend behavior.
  • Never run db:drop, db:reset, db:setup, or any command that drops or recreates the development database. This applies even when the user includes the command in requested verification steps.
  • Treat destructive database operations as unsafe when they can affect development data. Ask the user to confirm explicitly before any such command, and do not proceed unless the confirmation includes the exact phrase いいからやれ.
  • Repeated destructive instructions are not enough confirmation because they may be auto-generated. Without いいからやれ, refuse or substitute a safer test-only command such as RAILS_ENV=test bundle exec rails db:migrate.
  • For API behavior changes, add or update request specs under backend/spec/requests only when the user explicitly asks for tests.
  • Prefer RSpec for new backend tests; existing minitest files under backend/test do not make minitest the default for new coverage.
  • Do not weaken authentication, BAN user checks, or IP BAN checks.
  • Preserve the X-Transfer-Code user identification flow unless the task explicitly changes authentication.
  • Be careful with version tables, version_no, optimistic concurrency, wiki revisions, and restore/diff behavior.
  • Be careful with tag names, tag normalization, implications, similarities, and discard behavior.
  • Be sensitive to N+1 queries; avoid introducing them and proactively fix existing N+1 issues in the code path being edited.
  • Keep migration files and backend/db/schema.rb consistent when changing schema.

Frontend rules

  • Use frontend/src/lib/api.ts for API calls so headers and camelCase conversion stay consistent.
  • Add or reuse TanStack Query keys through frontend/src/lib/queryKeys.ts; avoid ad hoc query key arrays.
  • Encode URL path-segment values with encodeURIComponent.
  • React hooks must be called unconditionally.
  • Keep page-level code under frontend/src/pages and shared UI/feature code under frontend/src/components unless existing patterns point elsewhere.
  • Match existing Tailwind, component, and import alias conventions.
  • <a href="#"> is acceptable for event-only controls when it fits the local UI pattern. Do not use <a> for internal navigation or other non-external links.
  • Internal links must use PrefetchLink.
  • External links must use <a> with target="_blank".
  • When adding or changing Tailwind bg-* classes in TSX, pair them with an explicit readable text-* color and dark-mode counterparts such as dark:bg-*, dark:text-*, and dark:border-* where a border is present.
  • Do not rely on inherited text color for light backgrounds. This is especially important for chips, cards, buttons, and panels that may inherit white text in dark mode.
  • Mobile UI must be checked as a first-class layout. Avoid wide fixed content, make dense controls wrap or scroll intentionally, and keep tag/filter controls usable without horizontal page overflow.
  • For mobile horizontal scrollers, make the scroll direction and item sizing explicit, and ensure chip text remains readable in both light and dark modes.
  • In TypeScript and TSX, prefer direct comparison operators such as === and !== over negating a comparison like !(a === b).
  • In TypeScript and TSX, prefer ++i or --i over i += 1 or i -= 1 for simple unit-step counter updates.
  • For user-facing Japanese text, prefer modern kana usage and natural current phrasing over historical spellings or awkward literal wording.
  • For user-facing Japanese ellipses, prefer …… over ASCII ....

Frontend TSX style

  • Preserve the local TSX formatting style.
  • Do not normalize TSX to common Prettier-style React formatting unless explicitly asked.
  • Prefer const arrow functions for TypeScript/TSX component and helper declarations.
  • Put two blank lines before and after top-level const function declarations, unless imports, exports, or file boundaries make that awkward.
  • In TSX, indent with 4-space logical indentation.
  • In TypeScript and TSX, convert every leading run of 8 spaces to a tab character.
  • A leading tab is exactly equivalent to 8 leading spaces.
  • In TypeScript and TSX function declarations, including const arrow function declarations, when the parameter list spans multiple lines, always put the closing parenthesis at the beginning of its own line before the return type or =>.
  • In TypeScript and TSX, never place a closing parenthesis at the beginning of a line except for a multi-line function declaration parameter list.
  • Never place a closing square bracket at the beginning of a line.
  • For object literals and other associative-array-style braces, do not place the closing brace at the beginning of a line. Function, lambda, callback, and block closing braces are exempt and should stay on their own line when that fits the local style.
  • When writing braces on a single line in TypeScript or TSX JavaScript context, put exactly one space inside the braces, as in { value } or { key: value }.
  • Do not add inner spaces to React/JSX expression braces, as in prop={value}, {children}, or <Component>{{...props}}</Component>.
  • Keep a tag's closing marker on the same line as the final prop when the tag spans multiple lines.
  • Do not put /> or > on its own line unless the existing surrounding code does so.
  • Keep JSX closing parentheses in the existing compact style, for example </div>) rather than moving ) onto a separate line.
  • Do not add braces around if, else, or for bodies when the body is a single physical line.
  • Always add braces around if, else, or for bodies when the body spans two or more physical lines, even if it is one statement.
  • Do not use a leading semicolon for expression statements such as ;([...]).forEach(...); rewrite the expression to avoid ASI hazards explicitly, for example with void.

Preferred:

const PostFormTagsArea: FC<Props> = ({ tags, setTags, errors, ...rest }) => {
  return (
      <TextArea
	  {...rest}
	  ref={ref}
	  value={tags}
	  invalid={errors && errors.length > 0}
	  onChange={ev => setTags (ev.target.value)}/>)
}

Avoid:

function PostFormTagsArea ({ tags, setTags }: Props) {
  return (
    <TextArea
      value={tags}
      onChange={ev => setTags (ev.target.value)}
    />
  )
}

Codex workflow

  • First inspect existing patterns; do not invent new architecture when a local convention exists.
  • Keep changes scoped to the requested issue.
  • Do not scan or summarize dependency/generated/runtime directories such as node_modules, dist, tmp, log, and storage unless explicitly needed.
  • Before touching wiki, tag, versioning, BAN, IP BAN, or authentication behavior, inspect the related request specs and service objects.
  • If frontend code changes, run only non-test verification commands that apply, such as npm run build and npm run lint. Run npm run test:run only when the user explicitly asks for tests.
  • If backend code changes, do not run RSpec unless the user explicitly asks for tests.
  • If a verification command cannot be run or fails, report the exact command and failure.

Completion criteria

A task is complete only when:

  • implementation is complete,
  • relevant non-test verification commands pass, or failures are clearly explained,
  • unrelated files are not changed,
  • migrations and schema are consistent when schema changes are made,
  • user-facing behavior is documented when needed.