diff --git a/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md b/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md index c62cb9f..3be7aab 100644 --- a/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md +++ b/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md @@ -1,7 +1,7 @@ --- title: 'BTRC Hub / タグ広場 現行実装仕様書' -subtitle: '現行ソース btrc-hub-main.zip に基づく実装事実ベース仕様(2026-05-10)' -date: '2026-05-10' +subtitle: 'btrc-hub-main.zip / btrc-hub.wiki.zip / 2026-04-25 旧本番DBダンプ確認版' +date: '2026-06-11' lang: ja-JP toc: true toc-depth: 3 @@ -10,1513 +10,1351 @@ numbersections: true # 本書の位置づけ -本書は、2026-05-10 時点で添付された `btrc-hub-main.zip` の実装を確認し、タグ広場(BTRC Hub)の現行仕様を再構成した文書である。 +本書は、添付された `btrc-hub-main.zip` の現行ソース、`btrc-hub.wiki.zip` の Wiki、`btrc_hub_20260425時点本番DBデータ.zip` の旧本番 DB ダンプ、既存仕様書 `BTRC_Hub_現行実装仕様書_2026-05-10_ヒアリング反映版.md` を確認し、2026-06-11 時点のタグ広場の実装事実を再構成した仕様書である。 -既存の `BTRC_Hub_仕様書_2026-03-23.md` は有用な土台だが、現行実装とはすでに差分がある。したがって本書では、過去仕様書の記述よりも現行ソース・`db/schema.rb`・`routes.rb`・フロントエンドルーティング・主要コントローラ/モデルの実装を優先する。 +本書は願望ではなく、まずコード・スキーマ・画面・テストに現れてゐる事実を書く。未確定の設計意図は `開発者ヒアリング` として分離し、回答後に仕様へ反映する。 -本書の分類は次のとおり。 +## 確認対象 + +| 区分 | 確認対象 | +| --- | --- | +| バックエンド | Rails 8 API、models、controllers、services、representations、routes、schema、RSpec | +| フロントエンド | React/Vite/TypeScript、routes、pages、components、lib、types、Vitest | +| Wiki | 開発 Wiki、テーブル定義書、環境構築手順 | +| DB | 2026-04-25 旧本番 DB SQL ダンプ。現行 schema との差分あり | +| 既存仕様 | 2026-05-10 版仕様書 | +| 課題一覧 | Gitea API 取得を試行したが、この実行環境では取得失敗。未反映 | + +## 表記ルール | 表記 | 意味 | | --- | --- | -| 現行仕様 | 現在のソース・スキーマ・画面で確認できるもの | -| 実装あり・UI薄い | バックエンドやモデルは存在するが、画面や導線が不足しているもの | -| 計画・未確定 | ソースだけでは確定できず、開発者ヒアリングが必要なもの | -| 注意 | 実装上の落とし穴、仕様として明文化すべき制約 | - -本書は「願望」ではなく、コードにあるものをまず書く。曖昧な点は `開発者ヒアリング` として末尾に隔離する。 +| 現行仕様 | ソース・スキーマ・画面・テストで確認できる仕様 | +| 実装あり・UI薄い | API/モデルはあるが画面導線が弱い、または管理導線が薄い | +| 未実装・残骸候補 | スキーマだけある、または途中の設計痕跡はあるが呼び出し実装が薄いもの | +| 注意 | 仕様として固定するには危険な実装差・不整合・セキュリティ上の穴 | +| 開発者ヒアリング | コードだけでは判断不能。開発者確認が必要な事項 | ## 用語: 公開と公表 -本書では、以後 **公開** と **公表** を分ける。 - -| 用語 | 意味 | -| --- | --- | -| 公開 | URL として到達可能であり、検索エンジン等にも拾われ得る状態。現時点のタグ広場はすでにこの状態である。 | -| 公表 | ぼざクリ界隈等へ明示的に告知し、利用導線を出して、人を呼び込む状態。 | - -したがって、旧来の「一般公開前」「公開前」という表現は、本書では原則として **公表前** と読み替える。すでにサービスは外部から到達可能であり、問題は「存在をどこまで告知し、利用を促すか」である。 - - +現行のタグ広場は URL として到達可能であり、実質的にはすでに **公開** 状態である。一方、界隈へ明示的に告知し、利用導線を整へ、人を呼び込む状態を **公表** と呼ぶ。 +したがって「一般公開前」という古い表現は、本書では原則として **公表前** と読み替へる。 # システム概要 ## 目的 -BTRC Hub は、ぼざろクリーチャー関連コンテンツへのリンクを収集し、タグ・Wiki・素材・外部同期・上映会機能を通じて、作品群や関連知識を整理・再発見しやすくするための共同編集型基盤である。 +BTRC Hub / タグ広場は、ぼざろクリーチャー関連コンテンツへのリンクを収集し、タグ・Wiki・素材・上映会・推測ゲームを通じて、作品群と関連知識を整理・再発見しやすくする共同編集型基盤である。 -中核は次の 5 系統である。 +開発 Wiki の Home では、目的が次のやぅに整理されてゐる。 -1. **投稿**: 外部 URL を中心としたリンクデータ。 -2. **タグ**: カテゴリ、別名、親子関係、外部タグ連携、素材連携を持つ分類単位。 -3. **Wiki**: タグ名と結びつく説明ページ。行単位ストアと改訂履歴を持つ。 -4. **素材**: キャラクター・素材タグに紐づくファイルまたは参考 URL。 -5. **上映会**: 投稿を共同視聴し、コメント・在席・ホスト制御を行う実験的機能。 +- ぼざろクリーチャーシリーズ関連のあらゆるコンテンツへのリンクを保持する。 +- 各リンクにタグを付け、検索しやすくする。 +- ニコニコのタグ数制限への対応。 +- プラットフォームを超越し、あらゆるユーザによってぼざクリを一元管理できるやぅにする。 +- コンテンツ本体ではなくリンクを保持する。 +- SNS 性はなるべく排除する。 + +## 中核ドメイン + +| ドメイン | 内容 | +| --- | --- | +| 投稿 | 外部 URL によるリンクデータ。タグ・サムネ・親子関係・閲覧済み状態・類似投稿を持つ | +| タグ | カテゴリ、別名、親タグ、外部 Nico タグ連携、ニジラー紐づけ、素材紐づけを持つ分類単位 | +| Wiki | タグ名と結びつく説明ページ。行単位ストアと改訂履歴を持つ | +| 素材 | キャラクター/素材タグに紐づくファイルまたは URL | +| 上映会 | 投稿を共同視聴し、在席・ホスト・コメント・番組表・スキップ投票を扱ふ | +| Gekanator | 管理者向けの「投稿当て」推測ゲーム兼質問学習機構 | ## 非目的 -現行実装は次を主目的にしていない。 +現行実装は次を主目的にしてゐない。 - 汎用 SNS。 -- コメント掲示板。 -- 外部コンテンツ本体の転載保存サービス。 -- 高度な動画配信基盤。 -- 通常のメール/パスワード式アカウント管理。 +- 雑談掲示板。 +- 外部コンテンツ本体の転載保存。 +- 本格的な動画配信基盤。 +- メール/パスワード式の通常アカウント管理。 +- 誰でも自由に高度編集できる完全オープン Wiki。 -ただし、上映会や素材投稿などにより、単純なリンク集より重い知識基盤へ寄っている。 +ただし、上映会コメント・素材投稿・Gekanator 学習など、単なるリンク集より機能はかなり重い。仕様境界を曖昧にすると、SNS 化・荒らし対応・権限崩壊が一気に来る。ここは甘く見ないこと。 -## 技術構成 +# 技術構成 -| 層 | 現行構成 | +## バックエンド + +| 項目 | 現行 | | --- | --- | -| バックエンド | Ruby 3.2.2 / Rails 8.0.2 API | -| DB | MySQL 8 / utf8mb4_0900_ai_ci | -| ファイル | Active Storage。Cloudflare R2/S3 利用を想定する構成あり | -| フロントエンド | React 19.1 / Vite 6.3 / TypeScript 5.8 | -| 通信 | Axios + TanStack Query | -| UI | Tailwind CSS / Framer Motion / shadcn 風 UI コンポーネント | -| Markdown | react-markdown / react-markdown-editor-lite / remark-wiki-autolink | -| バッチ | Rake task。Nico 同期、YouTube 同期、類似度計算など | +| 言語 | Ruby | +| フレームワーク | Rails `~> 8.0.2` API | +| DB | MySQL 8 系想定。Gemfile には sqlite3 も残る | +| ファイル | Active Storage。S3/R2 互換想定あり | +| 画像処理 | image_processing / MiniMagick | +| HTML 解析 | Nokogiri | +| Wiki 移行/旧資産 | gollum / gollum-lib | +| 差分 | diff-lcs | +| テスト | RSpec | +| BAN/soft delete | discard | +| i18n | rails-i18n | + +## フロントエンド + +| 項目 | 現行 | +| --- | --- | +| UI | React 19.1 + Vite 6.3 | +| 言語 | TypeScript 5.8 | +| 通信 | Axios。レスポンスは `camelcase-keys` で deep camelCase 化 | +| 状態/取得 | TanStack Query、localStorage、必要箇所で state | +| スタイル | Tailwind CSS、shadcn 風ローカル UI、Framer Motion | +| Markdown | react-markdown、react-markdown-editor-lite、remark-gfm、wiki autolink | +| テスト | Vitest、Testing Library、jsdom | + +## 検証コマンド + +リポジトリ文書上の推奨コマンドは次である。 + +```sh +cd backend && bundle exec rspec +cd frontend && npm run test:run && npm run build && npm run lint +``` + +# ルーティング概要 + +## バックエンド API + +主な API は次である。 + +| 領域 | 代表エンドポイント | +| --- | --- | +| 投稿 | `GET /posts`, `GET /posts/:id`, `POST /posts`, `PUT/PATCH /posts/:id`, `GET /posts/random`, `GET /posts/versions`, `POST/DELETE /posts/:id/viewed` | +| タグ | `GET /tags`, `GET /tags/:id`, `PUT/PATCH /tags/:id`, `GET /tags/autocomplete`, `GET /tags/with-depth`, `GET /tags/versions` | +| Nico タグ | `GET /tags/nico`, `PUT /tags/nico/:id` | +| タグ親子 | `POST /tags/:parent_id/children/:child_id`, `DELETE /tags/:parent_id/children/:child_id` | +| Wiki | `GET/POST /wiki`, `GET/PUT /wiki/:id`, `GET /wiki/search`, `GET /wiki/changes`, `GET /wiki/:id/diff`, `GET /wiki/title/:title` | +| 素材 | `GET/POST /materials`, `GET/PUT/DELETE /materials/:id` | +| ニジラー紐づけ | `GET/PUT/DELETE /deerjikists/:platform/:code` | +| プレビュー | `GET /preview/title`, `GET /preview/thumbnail` | +| ユーザ | `POST /users`, `POST /users/verify`, `POST /users/code/renew`, `GET /users/me`, `PUT/PATCH /users/:id` | +| 上映会 | `GET /theatres/:id`, `PUT /watching`, `PATCH /next_post`, `PUT/DELETE /skip_vote`, `GET /post_selection_weights`, comments/programmes/skip_events | +| Gekanator | `GET /gekanator/posts`, `GET /gekanator/questions`, `POST /gekanator/games`, `POST /gekanator/question_suggestions`, `POST /ai_convert` | + +## フロントエンド画面 + +| パス | 画面 | +| --- | --- | +| `/posts` | 投稿一覧 | +| `/posts/new` | 投稿作成 | +| `/posts/search` | 投稿検索 | +| `/posts/:id` | 投稿詳細/編集 | +| `/posts/changes` | 投稿履歴 | +| `/tags` | タグ一覧 | +| `/tags/:id` | タグ詳細/編集 | +| `/tags/:id/deerjikists` | ニジラー紐づけ | +| `/tags/nico`, `/nico/tags` | Nico タグ一覧/連携編集 | +| `/tags/changes` | タグ履歴 | +| `/wiki` | Wiki 検索 | +| `/wiki/:title` | Wiki 表示 | +| `/wiki/new` | Wiki 新規作成 | +| `/wiki/:id/edit` | Wiki 編集 | +| `/wiki/:id/diff` | Wiki 差分 | +| `/wiki/changes` | Wiki 履歴 | +| `/materials` | 素材一覧 | +| `/materials/new` | 素材作成 | +| `/materials/:id` | 素材詳細 | +| `/theatres/:id` | 上映会 | +| `/gekanator` | 管理者専用 Gekanator | +| `/users/settings`, `/settings` | ユーザ設定 | +| `/tos` | 利用規約 | +| `/more` | その他 | # 認証・ユーザ・BAN -## ユーザモデル - -`users` は次の主要属性を持つ。 - -| 属性 | 意味 | -| --- | --- | -| name | 表示名 | -| inheritance_code | 引継ぎコード。認証トークンとして使う | -| role | `guest` / `member` / `admin` | -| banned_at | BAN 時刻。NULL なら有効ユーザ | - -ロールは文字列 enum である。 - -| ロール | 権限の概略 | -| --- | --- | -| guest | 閲覧中心。編集系は不可 | -| member | 投稿・Wiki・タグ・素材など通常編集が可能 | -| admin | member 権限に加え、タグ親子関係など管理系操作が可能 | - -`User#gte_member?` は `member? || admin?` を返す。 - ## 認証方式 -認証は通常の ID / パスワードではなく、`inheritance_code` による軽量認証である。 +通常のログインではなく、引継ぎコードによる軽量認証である。 -- フロントは `localStorage.user_code` にコードを保持する。 -- API 呼び出し時、`X-Transfer-Code` ヘッダにコードを付与する。 -- サーバは `users.inheritance_code` と照合し、`current_user` を決定する。 +- `users.inheritance_code` が認証トークン。 +- フロントは `localStorage.user_code` に保存。 +- API 呼び出し時、`X-Transfer-Code` ヘッダに付与。 +- バックエンドは `ApplicationController#authenticate_user` で `current_user` を設定する。 ## 初回利用フロー -フロント起動時の流れは次である。 +フロント起動時に次を行ふ。 -1. `localStorage.user_code` を確認する。 -2. ある場合は `POST /users/verify` へ送る。 -3. 有効なら返却ユーザを採用する。 -4. 無効またはコード無しなら `POST /users` で guest ユーザを新規作成する。 -5. 新規作成時、返却された引継ぎコードを localStorage に保存する。 +1. `localStorage.user_code` があれば `POST /users/verify`。 +2. 有効なら返却ユーザを採用。 +3. 無効またはコードなしなら `POST /users` で guest ユーザを作成。 +4. 新規作成された `inheritance_code` を localStorage へ保存。 -## BAN 強制 +## ロール -過去仕様書との差分として重要。 +| role | 意味 | +| --- | --- | +| `guest` | 自動生成される通常閲覧者。閲覧中心 | +| `member` | 投稿・タグ・Wiki・素材等の編集者 | +| `admin` | 管理者。Gekanator、タグ親子 API 等も利用可能 | -現行 `ApplicationController` では、すべての API に対して次が `before_action` で走る。 +`User#gte_member?` は `member` または `admin` を許可する。 + +## BAN + +`ApplicationController` の `before_action` は次の順序で動く。 1. `reject_banned_ip_address!` 2. `authenticate_user` 3. `reject_banned_user!` -したがって現行実装では、**BAN は保存されているだけではなく、API レベルで強制される**。 +したがって BAN は全 API にかかる。 -| 対象 | 判定 | +| 対象 | 判定 | 応答 | +| --- | --- | --- | +| IP BAN | `ip_addresses.banned_at` が存在 | 403 | +| User BAN | `users.banned_at` が存在 | 403 | + +IP は `IPAddr.new(request.remote_ip).hton` により binary 化して `ip_addresses.ip_address` に保存する。 + +### 注意 + +`users.inheritance_code` はモデル上必須・64文字以内だが、現行 schema では一意 index が見当たらない。UUID なので衝突可能性は低いが、認証トークンに DB 一意制約がないのは設計として弱い。 + +# エラー応答 + +## バリデーションエラー + +`ApplicationController#render_validation_error` 系は概ね次の JSON を返す。 + +```json +{ + "type": "validation_error", + "message": "入力内容を確認してください.", + "errors": {}, + "base_errors": [] +} +``` + +フロントは `camelcase-keys` により `baseErrors` などへ変換して扱ふ。 + +## 競合 + +投稿更新では version based optimistic concurrency があり、競合時は 409 を返す。 + +主なフィールド: + +- `error: 'conflict'` +- `message` +- `post_id` +- `base_version_no` +- `current_version_no` +- `base` +- `current` +- `mine` +- `changes` +- `conflicts` +- `mergeable` + +# 投稿仕様 + +## 投稿モデル + +`posts` は外部 URL を中心とするリンク記録である。 + +| 属性 | 仕様 | | --- | --- | -| IP BAN | `ip_addresses.banned_at` が存在すれば 403 | -| User BAN | `users.banned_at` が存在すれば 403 | +| `url` | 必須・一意・HTTP/HTTPS のみ | +| `title` | NULL 可 | +| `thumbnail_base` | 外部サムネイル URL。長さ 2000 | +| `thumbnail` | Active Storage 添付 | +| `uploaded_user_id` | 投稿者。同期投稿では NULL 可 | +| `original_created_from` | 元コンテンツ作成日時の下限 | +| `original_created_before` | 元コンテンツ作成日時の上限 | +| `version_no` | 投稿内の現行版番号。1 以上 | -IP は `request.remote_ip` を `IPAddr#hton` で 16 byte binary 化し、`ip_addresses.ip_address` に保存する。 +## URL 正規化 -## IP 紐づけ +保存前に次を行ふ。 -`POST /users` と `POST /users/verify` のタイミングで、ユーザと IP が紐づけられる。 +- 前後空白の除去。 +- URI として parse できる場合、host を小文字化。 +- path 末尾の `/` を除去。 +- HTTP/HTTPS 以外は不正。 -- `ip_addresses`: IP アドレス本体。 -- `user_ips`: user と ip_address の複合主キー関係。 - -注意点として、`request.remote_ip` が不正・空になるケースへの明示的な救済は薄い。ここはプロキシ構成・Cloudflare 経由・bot アクセスで事故り得る。 - -# 主要ドメインモデル - -## 投稿 posts - -投稿は外部 URL を中心とするリンクデータである。 - -| 属性 | 意味 | -| --- | --- | -| title | 表示タイトル | -| url | 投稿 URL。一意 | -| thumbnail_base | 外部サムネイル URL | -| uploaded_user_id | 手動投稿者。同期投稿は NULL になり得る | -| original_created_from | 元コンテンツ作成日時の下限 | -| original_created_before | 元コンテンツ作成日時の上限 | -| thumbnail | Active Storage 添付 | - -### URL 仕様 - -- URL は必須。 -- URL は一意。 -- HTTP / HTTPS のみ許可。 -- 保存前に前後空白を除去する。 -- host は小文字化される。 -- path 末尾の `/` は除去される。 - -### 元コンテンツ日時 +## 元コンテンツ日時 `original_created_from` と `original_created_before` の両方がある場合、`from < before` が必須である。 -YouTube/Nico 同期では、動画公開時刻を分単位の範囲として保持する実装がある。 +同期系では動画公開時刻を `from = 公開時刻の秒切捨て`, `before = from + 1分` として持つ。 -### サムネイル +## 投稿作成 -手動投稿時に `thumbnail` が渡されると Active Storage に添付され、`Post#resized_thumbnail!` により 180x180 の JPEG へリサイズされる。 +`POST /posts` は `member` 以上が必要。 -## 投稿親子関係 post_implications +主要入力: -現行実装では、投稿の親子関係は `posts.parent_id` ではない。 +| 入力 | 仕様 | +| --- | --- | +| `title` | 任意 | +| `url` | 必須 | +| `thumbnail` | 任意。添付時は 180x180 JPEG へ変換 | +| `thumbnail_base` | 任意 | +| `tags` | 空白区切りタグ文字列 | +| `parent_post_ids` | 必須。空でも送る必要あり | +| `original_created_from` / `before` | 任意 | -`post_implications` により、投稿と親投稿は多対多で表現される。 +タグは `Tag.normalise_tags!` で正規化され、別名解決、カテゴリ prefix 解釈、タグ希望/ニジラー不詳の自動付与、親タグ展開が行はれる。 + +## 投稿更新 + +`PUT/PATCH /posts/:id` は `member` 以上が必要。 + +現行では version based optimistic concurrency が実装されてゐる。 + +| パラメータ | 仕様 | +| --- | --- | +| `base_version_no` | `force` でない限り必須。正整数 | +| `force` | 強制上書き | +| `merge` | 競合がなければ自動マージ | + +`force` と `merge` の同時指定は禁止。 + +競合判定対象: + +- `title` +- `original_created_from` +- `original_created_before` +- `tag_names` +- `parent_post_ids` + +スカラー値は、base から current と mine が別々に変更され、かつ値が違ふ場合に競合する。集合値は、同じ要素について片方が追加し片方が削除してゐる場合に競合する。 + +`merge` 可能なら、現在状態と自分の変更を統合する。競合があれば 409。 + +## タグ処理 + +投稿タグは `post_tags` に保持される。 + +- 物理削除ではなく `discarded_at` による論理削除。 +- 同一投稿・同一タグの active 重複は禁止。 +- `PostTag#destroy` は読み取り専用例外を投げる。 +- `discard_by!` により `discarded_at` と `deleted_user` を設定し、`tags.post_count` を減算する。 + +投稿に手動で `nico:` タグを入れることは通常禁止される。Nico タグは同期/連携経由で扱ふ。 + +## 親子投稿 + +`post_implications` は投稿間の多対多親子関係である。 | カラム | 意味 | | --- | --- | -| post_id | 子投稿 | -| parent_post_id | 親投稿 | +| `post_id` | 子投稿 | +| `parent_post_id` | 親投稿 | -仕様: +制約: -- 主キーは `(post_id, parent_post_id)`。 -- 自己参照は禁止。 -- 投稿作成・更新 API では `parent_post_ids` が必須。 -- `parent_post_ids` は空白区切りの ID 文字列として解釈される。 -- 存在しない親 ID が含まれると 422。 -- 自分自身を親にすると 422。 +- 複合主キー。 +- 自己親は禁止。 +- 存在しない親 ID はエラー。 -この仕様は重要で、フロントや API クライアントは空でも `parent_post_ids` を送る必要がある。 +フロントの PostList は親子投稿の存在に応じてカード枠を変へる。 -## 投稿版 post_versions +## 関連投稿 -投稿本体とタグ・親投稿構成のスナップショットは `post_versions` に保存される。 +`post_similarities` は、投稿同士の類似度を保持する。 -| 属性 | 意味 | +- `Similarity::Calc.call(Post, :tags)` により生成。 +- 各投稿につき上位 20 件を保存。 +- 類似度はタグ集合の cosine similarity。 +- `Post#related(limit:)` は `cos DESC` で取得する。 + +## 投稿一覧検索 + +`GET /posts` は次の絞り込みを持つ。 + +| パラメータ | 仕様 | | --- | --- | -| post_id | 対象投稿 | -| version_no | 投稿内連番。1 以上 | -| event_type | create / update / discard / restore | -| title / url / thumbnail_base | 投稿本体スナップショット | -| tags | タグ名の空白区切りスナップショット | -| parent_post_ids | 親投稿 ID の空白区切りスナップショット | -| original_created_from / before | 元日時範囲 | -| created_by_user_id | 操作者 | +| `url` | URL 部分一致 | +| `title` | タイトル部分一致 | +| `tags` | 空白区切りタグ。別名 canonicalise あり | +| `match` | `all` または `any` | +| `not:` prefix | 除外タグ指定 | +| `original_created_from/to` | 元作成日時範囲 | +| `created_from/to` | 作成日時範囲 | +| `updated_from/to` | 更新日時範囲。タグ更新も考慮 | +| `order` | `title`, `url`, `original_created_at`, `created_at`, `updated_at` + asc/desc | +| `page`, `limit` | 1 未満は 1 へ補正 | -`PostVersionRecorder` は更新前スナップショットを保証し、変更がなければ update 版を増やさない。 +`updated_at` sort は、投稿本体の `updated_at` と `post_tags.updated_at` の最大値を使ふ。 -## タグ tags / tag_names +## 投稿履歴 -タグ名文字列とタグ実体は分離されている。 +`post_versions` は immutable snapshot である。 -- `tag_names`: 名前、別名関係、Wiki ページとの結合点。 -- `tags`: カテゴリ、投稿件数、タグ実体。 - -### カテゴリ - -現行カテゴリは次である。 - -| category | 用途 | +| 属性 | 内容 | | --- | --- | -| deerjikist | ニジラー | -| meme | ミーム・原作・ネタ元など | -| character | キャラクター | -| general | 一般 | -| material | 素材 | -| nico | ニコニコ外部タグ | -| meta | メタタグ | +| `post_id` | 対象投稿 | +| `version_no` | 投稿内連番 | +| `event_type` | `create`, `update`, `discard`, `restore` | +| `title`, `url`, `thumbnail_base` | 投稿本体 | +| `tags` | タグ名の空白区切り snapshot | +| `parent_post_ids` | 親投稿 ID の空白区切り snapshot | +| `original_created_from/before` | 元日時範囲 | +| `created_by_user_id` | 操作者 | -### 別名 +`VersionRecorder` は次を保証する。 + +- 永続化済み version は readonly。 +- 初回イベントは `create` でなければならない。 +- 同一 snapshot の update は版を増やさない。 +- 作成後、対象 record の `version_no` を更新する。 + +### 注意: frontend 型と API のズレ + +フロント `PostVersion` 型には `parentPosts` がある。しかし現行 `PostVersionsController#index` は `parent_post_ids` を select/serialize しておらず、`parentPosts` を返してゐない。これは履歴 UI の仕様不整合である。親投稿変更履歴を画面に出すつもりなら、API 実装が不足してゐる。 + +# タグ仕様 + +## タグ名とタグ実体 + +タグは `tag_names` と `tags` に分離される。 + +| テーブル | 役割 | +| --- | --- | +| `tag_names` | 名前文字列、canonical/alias 関係、Wiki ページとの結合点 | +| `tags` | カテゴリ、投稿件数、実体 ID、version_no | + +`Tag#name` は `tag_name.name` の delegate である。 + +## タグカテゴリ + +| category | 表示/用途 | +| --- | --- | +| `deerjikist` | ニジラー | +| `meme` | 原作・ネタ元・ミーム等 | +| `character` | キャラクター | +| `general` | 一般 | +| `material` | 素材 | +| `meta` | メタタグ | +| `nico` | ニコニコタグ | + +`nico` タグは名前が必ず `nico:` で始まる必要があり、非 nico タグが `nico:` で始まることも禁止される。 + +## システムタグ + +`Tag` には次の作成ヘルパがある。 + +| メソッド | タグ名 | 用途 | +| --- | --- | --- | +| `Tag.tagme` | `タグ希望` | タグ不足の印 | +| `Tag.bot` | `bot操作` | bot/sync 由来の印 | +| `Tag.no_deerjikist` | `ニジラー情報不詳` | ニジラー不明 | +| `Tag.video` | `動画` | 動画投稿 | +| `Tag.niconico` | `ニコニコ` | ニコニコ由来 | +| `Tag.youtube` | `YouTube` | YouTube 由来 | + +`タグ希望`, `bot操作`, `ニジラー情報不詳`, `動画`, `ニコニコ` は名称変更が明示的に禁止される。 + +## タグ正規化 + +`Tag.normalise_tags!` の仕様: + +- 入力はタグ名配列。 +- 空白・空文字を除去。 +- カテゴリ prefix を解釈する。 +- `TagName.canonicalise` により別名を正規名へ解決する。 +- 存在しない tag_name/tag は作成する。 +- `with_tagme` が true かつタグ数が 10 未満で `タグ希望` がなければ追加。 +- `with_no_deerjikist` が true かつ deerjikist タグがなければ `ニジラー情報不詳` を追加。 +- `deny_nico` が true なら `nico:` prefix はエラー。 + +対応 prefix: + +| prefix | category | +| --- | --- | +| `general:`, `gen:` | general | +| `deerjikist:`, `djk:` | deerjikist | +| `meme:` | meme | +| `character:`, `chr:` | character | +| `material:`, `mtr:` | material | +| `meta:` | meta | + +## 別名 `tag_names.canonical_id` により別名を表現する。 - `canonical_id = NULL`: 正規名。 - `canonical_id != NULL`: 別名。 -- 別名の参照先も別名であってはならない。 -- 別名名に `:` を含められない。 +- 別名の参照先は正規名でなければならない。 +- 別名名に `:` は含められない。 - タグまたは Wiki ページを持つ tag_name は別名化できない。 -`TagName.canonicalise` は、既知の別名を正規名へ置換する。 +タグ詳細の full update では、名称変更時に旧名が aliases に追加される。 -## タグサニタイズ tag_name_sanitisation_rules +## タグ親子 -タグ名には優先度付きのサニタイズ規則がある。 - -| 属性 | 意味 | -| --- | --- | -| priority | 主キー。適用順序 | -| source_pattern | 正規表現 | -| replacement | 置換後文字列 | -| discarded_at | 論理削除 | - -`TagName` のバリデーションは、名前が `TagNameSanitisationRule.sanitise(name)` と一致することを要求する。 - -したがって、サニタイズ規則は「保存前に自動で直す」だけではなく、「規則に反する名前を拒否する」層でもある。 - -## タグ正規化 - -投稿作成・更新時、タグ入力は `Tag.normalise_tags!` で正規化される。 - -### カテゴリプレフィクス - -| 入力接頭辞 | category | -| --- | --- | -| general: / gen: | general | -| deerjikist: / djk: | deerjikist | -| meme: | meme | -| character: / chr: | character | -| material: / mtr: | material | -| meta: | meta | - -`nico:` は通常手入力では拒否される。 - -### 自動付与タグ - -通常の正規化では次が自動付与される。 - -| 条件 | 自動付与 | -| --- | --- | -| タグ数が 10 未満 | タグ希望 | -| deerjikist カテゴリが無い | ニジラー情報不詳 | - -投稿更新時は `with_tagme: false` のため、更新時に「タグ希望」を新規補完しない。 - -### 親タグ展開 - -`Tag.expand_parent_tags` により、入力タグの親タグを再帰的に追加する。 - -結果として投稿には、明示タグだけではなく、親カテゴリ的なタグも保存される。 - -## 投稿タグ post_tags - -投稿とタグは多対多である。 - -| 属性 | 意味 | -| --- | --- | -| post_id | 投稿 | -| tag_id | タグ | -| created_user_id | 付与者 | -| deleted_user_id | 削除者 | -| discarded_at | 論理削除時刻 | -| active_unique_key | 有効レコード一意制約用の生成列 | - -現行有効な `(post_id, tag_id)` の重複は DB レベルで防がれている。 - -`PostTag` は論理削除されるため、タグ付与履歴としても使われる。 - -## タグ親子 tag_implications - -タグの親子関係は `tag_implications` で表す。 +`tag_implications` は子タグから親タグへの関係を表す。 | カラム | 意味 | | --- | --- | -| tag_id | 子タグ | -| parent_tag_id | 親タグ | +| `tag_id` | 子タグ | +| `parent_tag_id` | 親タグ | -仕様: +- 同一組合せは一意。 +- 自己親は禁止。 +- 投稿側では、付与タグの親タグが再帰的に展開される。 -- 同一組み合わせは一意。 -- admin のみ API で作成・削除できる。 -- nico カテゴリタグは親子操作不可。 -- 投稿タグ保存時、親タグは自動展開される。 +### 注意: 権限仕様が二重化してゐる -## タグ版 tag_versions / nico_tag_versions +`TagChildrenController` の親子追加/削除 API は admin 限定である。一方、`TagsController#update_all` は member 以上で `parent_tags` を更新できる。つまり、API とタグ編集フォームの設計意図がズレてゐる可能性が高い。 -通常タグと nico タグは別テーブルで履歴を持つ。 +ここは仕様として固定する前に開発者ヒアリングが必要である。 -### tag_versions +## タグ一覧/検索 -| 属性 | 意味 | +`GET /tags` は次を持つ。 + +| パラメータ | 仕様 | | --- | --- | -| tag_id | 対象タグ | -| version_no | タグ内連番 | -| event_type | create / update / discard / restore | -| name | 名前スナップショット | -| category | カテゴリ | -| aliases | 別名リスト | -| parent_tag_ids | 親タグ ID リスト | -| created_by_user_id | 操作者 | +| `post` | 投稿 ID で絞り込み | +| `name` | 名前部分一致 | +| `category` | カテゴリ | +| `post_count_gte/lte` | 投稿件数範囲 | +| `created_from/to` | 作成日時 | +| `updated_from/to` | 更新日時 | +| `order` | `name`, `category`, `post_count`, `created_at`, `updated_at` | -### nico_tag_versions +カテゴリ順は独自順で、`deerjikist`, `meme`, `character`, `general`, `material`, `meta`, `nico` の順を使ふ。 -| 属性 | 意味 | +## タグ autocomplete + +`GET /tags/autocomplete`: + +| パラメータ | 仕様 | | --- | --- | -| tag_id | nico タグ | -| version_no | nico タグ内連番 | -| event_type | create / update / discard / restore | -| name | nico タグ名 | -| linked_tags | 連携内部タグ名 | -| created_by_user_id | 操作者 | +| `q` | 検索語 | +| `nico` | nico タグを含めるか。既定 true | +| `present` | 投稿件数 > 0 のものに絞るか。既定 true | -## ニコタグ連携 nico_tag_relations +別名 hit も見て、`matched_alias` を返す。 -`nico_tag_relations` は nico カテゴリタグと内部タグを結ぶ。 +## タグ履歴 -- `nico_tag_id`: nico カテゴリ必須。 -- `tag_id`: nico カテゴリ禁止。 +`tag_versions` は immutable snapshot。 -これは外部ニコニコタグを内部タグ体系へ変換するための関係である。 - -## ニジラー deerjikists - -外部プラットフォーム上の人物識別子と内部タグを結ぶ。 - -| 属性 | 意味 | +| 属性 | 内容 | | --- | --- | -| platform | nico / youtube | -| code | 外部識別子 | -| tag_id | deerjikist カテゴリのタグ | +| `tag_id` | 対象タグ | +| `version_no` | タグ内連番 | +| `event_type` | create/update/discard/restore | +| `name` | タグ名 snapshot | +| `category` | カテゴリ snapshot | +| `aliases` | 別名空白区切り snapshot | +| `parent_tag_ids` | 親タグ ID 空白区切り snapshot | +| `created_by_user_id` | 操作者 | -主キーは `(platform, code)`。 +`TagVersioning` は更新前 snapshot と更新後記録を制御する。 -YouTube の `@handle` は、更新 API 内でチャンネル ID `UC...` へ正規化を試みる。 +# Nico タグ仕様 -## 素材 materials +## 概要 -現行実装で追加されている重要機能。 +ニコニコ由来のタグは内部タグとは別性質である。 -素材は、素材タグまたはキャラクタータグに紐づくファイル・参考 URL である。 +- `tags.category = 'nico'` +- `tag_names.name` は必ず `nico:` で始まる。 +- 通常の手動入力では `nico:` は拒否される。 +- 内部タグとの連携は `nico_tag_relations` で行ふ。 -重要なのは、`character` と `material` は単純な上下カテゴリではない点である。現行運用では、`character` タグを素材集合の代表タグとし、関連する `material` タグを `TagImplication` によって子タグとしてぶら下げる。つまり、キャラクタータグは「そのキャラクターに関する素材タグ群の入口」として機能する。 +## Nico タグ連携 -タグ分けするほどではないが異なる素材差分については、将来的に **包摂素材** として単一 Material の配下へ複数保持できるようにする計画である。これは、立ち絵差分・音声差分・参考画像などを何でもタグ化してタグ空間を肥大化させるのを避けるための拡張である。 +`nico_tag_relations`: - - -| 属性 | 意味 | +| カラム | 意味 | | --- | --- | -| url | 参考 URL。論理削除されていない場合は一意 | -| parent_id | 親素材。将来的な包摂素材表現の土台になる | -| tag_id | 対応タグ。素材 1 件につき一意 | -| created_by_user_id | 作成者 | -| updated_by_user_id | 更新者 | -| discarded_at | 論理削除 | -| file | Active Storage 添付 | +| `nico_tag_id` | nico カテゴリタグ | +| `tag_id` | 内部タグ | -### 制約 +`NicoTagsController#update` は member 以上が利用可能。 -- `tag_id` は必須かつ一意。 -- タグは `material` または `character` カテゴリのみ許可。 -- `character` は素材集合の代表タグとして許可される。 -- `material` は具体的な素材タグとして許可される。 -- `url` または `file` の少なくとも一方が必須。 -- `discarded_at` が NULL の場合だけ `url` 一意制約が効く。 - -### 現行運用上の意味 - -- キャラクターに関連する複数素材は、現時点では `TagImplication` によって `character` 親タグと `material` 子タグに分解して扱う。 -- そのため、`materials.tag_id` が一意であっても、素材をタグ単位へ分ける限りは大きな問題は起きにくい。 -- ただし、タグを増やすほどではない素材差分を複数持ちたい場合は、現行設計だけでは表現力が足りない。 -- この不足分を埋める将来仕様が **包摂素材** である。 - -### バージョン - -`material_versions` が存在し、素材の URL・親素材・タグ・作成/更新者・discard 状態を履歴化する構造がある。 - -ただし、コントローラ上は素材の version API は確認できない。モデル・サービス側の実運用接続は追加確認が必要。 - -## Wiki - -Wiki はタグ名に紐づく説明ページである。 - -### wiki_pages - -| 属性 | 意味 | -| --- | --- | -| tag_name_id | Wiki タイトルに対応する tag_name | -| body | 現行本文キャッシュ | -| created_user_id | 作成者 | -| updated_user_id | 更新者 | -| next_asset_no | Wiki 画像/添付素材の採番用 | -| discarded_at | 論理削除 | - -`wiki_pages.tag_name_id` は一意。 - -### wiki_revisions / wiki_lines - -Wiki は改訂と行ストアを分離する。 - -- `wiki_revisions`: 改訂メタ情報。 -- `wiki_revision_lines`: 改訂内の行順序。 -- `wiki_lines`: 行本文を SHA-256 で重複排除保存。 - -`wiki_revisions.kind` は現行 enum で `content` / `redirect` を持つ。ただし `Wiki::Commit#redirect!` は現在 `廃止しました.` として例外を投げる。 - -### wiki_versions - -`wiki_versions` も存在し、Wiki ページの版管理スナップショットを保持する。 - -現行 `Wiki::Commit.content!` は `WikiVersionRecorder.record!` を呼ぶため、行単位 revision と別に、通常の version 履歴も記録される。 - -### Wiki 競合制御 - -`Wiki::Commit.content!` は `base_revision_id` を受け取る。指定があり、現在の最大 revision id と一致しない場合は `Wiki::Commit::Conflict` を投げ、コントローラは 409 を返す。 - -現行 `WikiPagesController#update` は `params[:base_revision_id]` を使う。したがって、過去仕様書にあった「クライアント送信値を使わないため競合検出が活きていない」という記述は、現行実装では修正済みである。 - -## 閲覧済 user_post_views - -ユーザごとの投稿閲覧済み状態を保持する。 - -- 主キーは `(user_id, post_id)`。 -- `POST /posts/:id/viewed` で付与。 -- `DELETE /posts/:id/viewed` で解除。 - -## 類似度 post_similarities / tag_similarities - -投稿類似度とタグ類似度は事前計算テーブルで保持される。 - -| テーブル | 単位 | 特徴量 | -| --- | --- | --- | -| post_similarities | 投稿 → 関連投稿 | 投稿に付いたタグ集合 | -| tag_similarities | タグ → 関連タグ | タグに属する投稿集合 | - -上位 20 件のみ保存する設計。 - -## 上映会 theatres - -上映会は共同視聴機能である。 - -| 属性 | 意味 | -| --- | --- | -| name | 会場名 | -| opens_at / closes_at | 開始/終了時刻 | -| kind | 種別。現行 UI では薄い | -| current_post_id | 現在上映中投稿 | -| current_post_started_at | 現在投稿の開始時刻 | -| next_comment_no | コメント採番 | -| host_user_id | 現在ホスト | -| created_by_user_id | 作成者 | -| discarded_at | 論理削除 | - -# API 仕様 - -## 投稿 API - -### GET /posts - -投稿一覧を取得する。 - -#### パラメータ - -| パラメータ | 意味 | -| --- | --- | -| tags | 空白区切りタグ検索 | -| match | `all` 相当または `any` | -| title | タイトル部分一致 | -| url | URL 部分一致 | -| original_created_from / original_created_to | 元コンテンツ作成日時範囲 | -| created_from / created_to | 投稿作成日時範囲 | -| updated_from / updated_to | 投稿またはタグ更新日時範囲 | -| page | ページ。既定 1 | -| limit | 件数。既定 20 | -| order | `field:direction` | - -`order` の field は次。 - -- title -- url -- original_created_at -- created_at -- updated_at - -`updated_at` は `posts.updated_at` と `post_tags.updated_at` の最大値を `updated_at_all` として使う。 - -### タグ検索意味論 - -`tags` は空白区切り。各要素は `TagName.canonicalise` により別名解決される。 - -- `match=any`: OR。 -- それ以外: AND。 -- `not:` 接頭辞: 否定条件。 - -注意: `match=any` で `not:` を混ぜると、SQL 的には「否定条件を OR する」ため、期待とズレる可能性がある。仕様として許すか、UI で制限するか要確認。 - -### GET /posts/random - -現在の `tags` / `match` 条件に合う投稿をランダムに 1 件返す。 - -### GET /posts/:id - -投稿詳細を取得する。 - -返却には次を含む。 - -- 投稿基本情報。 -- タグ木構造。 -- 関連投稿最大 20 件。 -- 現在ユーザの閲覧済みフラグ。 -- 親投稿・子投稿・兄弟投稿情報。 - -### POST /posts - -投稿を作成する。member 以上必須。 - -入力: - -| パラメータ | 必須 | 意味 | -| --- | --- | --- | -| title | 任意 | タイトル | -| url | 必須 | URL | -| thumbnail | 任意 | サムネイルファイル | -| tags | 任意 | 空白区切りタグ | -| original_created_from | 任意 | 元日時下限 | -| original_created_before | 任意 | 元日時上限 | -| parent_post_ids | 必須 | 空白区切り親投稿 ID | - -保存フロー: - -1. 投稿を保存。 -2. タグを正規化。 -3. 関連タグのバージョンスナップショットを確保。 -4. 親タグを展開。 -5. `post_tags` を同期。 -6. 親投稿関係を同期。 -7. サムネイルをリサイズ。 -8. `post_versions` に create 版を記録。 - -### PUT /posts/:id - -投稿を更新する。member 以上必須。 - -更新対象: - -- title -- original_created_from -- original_created_before -- tags -- parent_post_ids - -注意: - -- URL の更新は現行 API では行わない。 -- nico カテゴリタグは既存分を維持したまま、手入力側タグを再計算する。 -- `parent_post_ids` は更新時も必須。 -- 投稿の optimistic locking 用 `version_no` は posts テーブルにまだ無い。履歴はあるが、投稿更新 API で base version を照合する仕様は未実装。 - -### POST /posts/:id/viewed / DELETE /posts/:id/viewed - -ログイン済みユーザの閲覧済み状態を付与・解除する。 - -### GET /posts/changes - -投稿タグ付与履歴を取得する。 - -| パラメータ | 意味 | -| --- | --- | -| id | 投稿 ID 絞り込み | -| tag | タグ ID 絞り込み | -| page / limit | ページング | - -返却されるのは、投稿本体履歴ではなく `post_tags` の add/remove イベントである。 - -### GET /posts/versions - -投稿本体スナップショット履歴を取得する。 - -| パラメータ | 意味 | -| --- | --- | -| post | 投稿 ID 絞り込み | -| tag | タグ ID 絞り込み | -| page / limit | ページング | - -前版との差分として、title / url / thumbnail_base / tags / parent_post_ids / original_created_from / original_created_before などを返す。 - -## タグ API - -### GET /tags - -タグ一覧・検索を取得する。 - -| パラメータ | 意味 | -| --- | --- | -| post | 指定投稿に付いたタグだけに絞る | -| name | 名前部分一致 | -| category | カテゴリ一致 | -| post_count_gte / post_count_lte | 投稿件数範囲 | -| created_from / created_to | 作成日時範囲 | -| updated_from / updated_to | 更新日時範囲 | -| page / limit | ページング | -| order | `field:direction` | - -`order` の field は name / category / post_count / created_at / updated_at。 - -category 並びは独自順序。 - -1. deerjikist -2. meme -3. character -4. general -5. material -6. meta -7. nico - -注意: `count` は `q.size` を返しており、ページング後/前の扱いが ActiveRecord の状態に依存しうる。厳密な総件数仕様としては弱い。 - -### GET /tags/with-depth - -階層表示用タグを取得する。 - -| パラメータ | 意味 | -| --- | --- | -| parent | 親タグ ID。無ければルートタグ | - -対象カテゴリは `meme` / `character` / `material` に限定される。 - -返却タグには `has_children` が付く。 - -### GET /tags/autocomplete - -タグ補完。 - -| パラメータ | 既定 | 意味 | -| --- | --- | --- | -| q | 空 | 前方一致検索語 | -| nico | true | nico 候補を含める | -| present | true | post_count > 0 に限定 | - -挙動: - -- `q` 先頭の `not:` は除去する。 -- canonical 名前方一致を検索する。 -- 別名前方一致も拾い、正規タグを返す。 -- 別名ヒット時は `matched_alias` を返す。 -- 最大 20 件。 - -### GET /tags/:id / GET /tags/name/:name - -タグ詳細を取得する。 - -`/tags/name/:name` は exact name であり、別名から正規タグへ自動解決しない。 - -### PUT /tags/:id - -タグ全体更新。member 以上必須。 - -現行実装では以下を受ける。 - -- name -- category -- aliases -- parents - -仕様: - -- nico タグの編集は禁止。 -- `nico` カテゴリへの変更は禁止。 -- 特殊タグの改名は禁止。 -- name 変更時は、対応 Wiki があれば Wiki 版も更新記録される。 -- aliases 更新時は対象・影響タグの snapshot を確保する。 -- parents 更新時は親タグを再正規化し、既存親関係を全置換する。 - -### PATCH /tags/:id - -狭いタグ更新。member 以上必須。詳細は `update_all` と分担があり、実装確認継続対象。 - -### GET /tags/versions - -タグ履歴を取得する。 - -| パラメータ | 意味 | -| --- | --- | -| id | タグ ID 絞り込み | -| page / limit | ページング | - -前版との差分として、name / category / aliases / parent_tags を返す。 - -### POST /tags/:parent_id/children/:child_id - -タグ親子関係を追加する。admin のみ。 - -- nico タグは不可。 -- 子タグの snapshot を確保してから関係追加。 -- tag_versions に update を記録。 - -### DELETE /tags/:parent_id/children/:child_id - -タグ親子関係を削除する。admin のみ。 - -## ニコタグ API - -### GET /tags/nico - -nico カテゴリタグ一覧を取得する。 - -| パラメータ | 意味 | -| --- | --- | -| limit | 件数。既定 20 | -| cursor | ISO8601 時刻。`updated_at < cursor` | - -`linked_tags` を含む。 - -### PUT /tags/nico/:id - -nico タグと内部タグの連携を更新する。member 以上必須。 - -- 対象タグは nico カテゴリ必須。 -- 入力 `tags` を通常タグとして正規化する。 -- 連携先に nico カテゴリが含まれると 400。 -- `nico_tag_versions` に update が記録される。 - -## ニジラー API - -### GET /deerjikists/:platform/:code - -外部識別子からニジラー対応タグを取得する。 - -### PUT /deerjikists/:platform/:code - -ニジラー対応を作成・更新する。member 以上必須。 - -入力: - -- tag_id - -### DELETE /deerjikists/:platform/:code - -ニジラー対応を削除する。member 以上必須。 - -## 素材 API - -### GET /materials - -素材一覧を取得する。 - -| パラメータ | 意味 | -| --- | --- | -| page / limit | ページング | -| tag_id | 紐づくタグで絞る | -| parent_id | 親素材で絞る | - -返却は `materials` と `count`。 - -### GET /materials/:id - -素材詳細を取得する。 - -返却には素材本体に加え、素材タグに対応する Wiki 本文 `wiki_page_body` が含まれる。 - -### POST /materials - -素材を作成する。現行実装ではログイン必須。 - -計画仕様としては **member 以上に制限する**。当初は guest 作成可能の意図があったが、ファイルアップロードを伴うため、オブジェクトストレージへの大量投入・違法ファイル混入・容量爆撃のリスクを避ける。 - -入力: - -| パラメータ | 必須 | 意味 | -| --- | --- | --- | -| tag | 必須 | 対応タグ名 | -| file | file または url の一方必須 | 素材ファイル | -| url | file または url の一方必須 | 参考 URL | - -タグが存在しない場合は `material` カテゴリで作成される。 - -### PUT /materials/:id - -素材を更新する。member 以上必須。 - -- tag -- file -- url - -file が渡されない場合は既存ファイルを purge する。 - -### DELETE /materials/:id - -素材を論理削除する。member 以上必須。 - -## Wiki API - -### GET /wiki - -Wiki 一覧・タイトル検索。 - -| パラメータ | 意味 | -| --- | --- | -| title | タイトル部分一致。空なら全件 | - -本文検索は現行 `index` では行わない。 - -### GET /wiki/search - -現行では `index` と同じ処理。 - -### GET /wiki/:id / GET /wiki/title/:title - -Wiki 詳細を取得する。 - -| パラメータ | 意味 | -| --- | --- | -| version | revision id を指定して過去版取得 | - -返却: - -- id -- title -- body -- revision_id -- pred -- succ -- updated_at - -### GET /wiki/:id/exists / GET /wiki/title/:title/exists - -存在すれば 204、無ければ 404。 - -### GET /wiki/:id/diff - -Wiki 差分を取得する。 - -| パラメータ | 意味 | -| --- | --- | -| from | 古い revision id。省略時は空本文相当 | -| to | 新しい revision id。省略時は現行 revision | - -content revision 同士のみ比較可能。 - -### POST /wiki - -Wiki を作成する。member 以上必須。 - -入力: - -- title -- body -- message 任意 - -処理: - -1. title に対応する `tag_name` を取得または作成。 -2. `Wiki::Commit.create_content!` で page と revision を生成。 -3. `wiki_versions` に create 記録。 - -### PUT /wiki/:id - -Wiki を更新する。member 以上必須。 - -入力: - -- title -- body -- message 任意 -- base_revision_id 任意 - -現行では **タイトル変更も受け付ける**。タグがその tag_name を参照している場合、タグ版も更新記録される。 - -`base_revision_id` が現行最大 revision id と一致しない場合、409 conflict。 - -### GET /wiki/changes - -Wiki 改訂履歴を最大 200 件返す。 - -| パラメータ | 意味 | -| --- | --- | -| id | Wiki ページ ID 絞り込み | - -## プレビュー API - -### GET /preview/title - -指定 URL の HTML を取得し、`
` で検索。
+3. なければ投稿作成。
+4. サムネイルを HTML meta から取得して Active Storage 添付。
+5. `タグ希望`, `bot操作`, `ニコニコ`, `動画` を付与。
+6. ニコニコの生タグを `nico:` として `nico` タグ化。
+7. Nico タグに連携された内部タグも付与。
+8. 投稿版/Nico タグ版を記録。
-## YouTube 同期
+## Nico 逆連携
-YouTube 同期は `Youtube::Sync` に実装されている。
+`backend/lib/tasks/export_nico.rake` は、タグ広場上のニコニコ動画 ID を外部 `tracked_videos.put_bulk_upsert` へ渡す。
-主な処理:
+# YouTube 同期仕様
-1. 検索語および playlist id から動画 ID を収集する。
-2. YouTube Data API の `videos` で詳細を取得する。
-3. 既存投稿は YouTube URL 正規表現で探索する。
-4. タイトル・公開日時・サムネイル URL を反映する。
-5. 新規投稿には `タグ希望`、`bot操作`、`YouTube`、`動画` を付与する。
-6. チャンネル ID と `deerjikists` の対応があれば、ニジラータグを付与する。
+`backend/lib/tasks/sync_posts.rake` は `Youtube::Sync.new.sync!` を呼ぶ。
-`YOUTUBE_API_KEY` が環境変数として必須。
+## 検出対象
-## 類似度計算
+- 検索語:
+ - `ぼざろクリーチャーシリーズ`
+ - `伊地知ニジカ`
+ - `伊地知虹鹿`
+- プレイリスト ID 3 件。
+- 検索対象は直近 14 日。
-Rake task により投稿・タグの類似度を再計算する。
+## 同期処理
-- 投稿類似度: タグ集合ベース。
-- タグ類似度: 投稿集合ベース。
-- 上位 20 件保存。
+1. YouTube Data API で動画 ID を取得。
+2. `videos` API で snippet/status/contentDetails を取得。
+3. `youtube.com/watch?v=` または `youtu.be/` として既存投稿検索。
+4. なければ投稿作成。
+5. サムネイルを添付。
+6. 新規時 `タグ希望`, `bot操作`, `YouTube`, `動画` を付与。
+7. チャンネル ID が `deerjikists` に登録済みなら該当 deerjikist タグを付与。
+8. 未登録かつ deerjikist タグなしなら `ニジラー情報不詳` を付与。
+9. 投稿版を記録。
-# 非機能・運用仕様
+# ニジラー紐づけ仕様
-## 整合性
+## deerjikists
-- URL は posts で一意。
-- post_tags の現行有効関係は生成列で一意。
-- materials の有効 URL は生成列で一意。
-- タグ名は一意。
-- Wiki ページは tag_name ごとに一意。
-- 投稿親子は複合主キーで重複不可。
-- 投稿親子の自己参照は禁止。
+`deerjikists` は外部プラットフォーム上のユーザ/チャンネルとタグを紐づける。
-## 監査性
-
-現行で保存される履歴:
-
-| 対象 | 履歴 |
+| カラム | 意味 |
| --- | --- |
-| 投稿本体 | post_versions |
-| 投稿タグ付与 | post_tags の created/deleted/discarded |
-| タグ | tag_versions |
-| nico タグ連携 | nico_tag_versions |
-| Wiki | wiki_revisions / wiki_versions |
-| 素材 | material_versions。ただし API 接続状況は要追加確認 |
+| `platform` | `nico` または `youtube` |
+| `code` | 外部 ID |
+| `tag_id` | deerjikist カテゴリのタグ |
-## セキュリティ上の注意
+複合主キーは `(platform, code)`。
-公表前または利用拡大前に仕様として潰すべき箇所。
+`Deerjikist` モデルは紐づけ先タグが `deerjikist` カテゴリであることを要求する。
-1. **preview API の SSRF 対策**
- - private IP 禁止、localhost 禁止、リダイレクト制御、サイズ上限、タイムアウト、Content-Type 制限が仕様化されていない。
-2. **iframe 外部表示**
- - confirm はあるが、許可ドメインや CSP の明確仕様が無い。
-3. **引継ぎコード認証**
- - 簡便だが漏洩時の被害が大きい。コード更新導線はあるが、セッション管理は薄い。
-4. **素材作成権限**
- - 現行では guest でもログイン済みなら `POST /materials` できる。意図的か確認が必要。
-5. **IP BAN の remote_ip 前提**
- - 本番プロキシ設定がズレると、全員同一 IP 扱いになる危険がある。
+## API
-## パフォーマンス上の注意
-
-1. 投稿一覧はタグ・素材・Wiki を preload しているため、ページサイズやタグ数が増えると重くなる。
-2. タグサイドバーはクライアントで投稿配列からタグを収集する。1ページ件数増加時に効く。
-3. `order('RAND()')` は投稿数増加時に重くなる。
-4. Theatre の次動画選定も `RAND()` で、投稿数増加に弱い。
-5. `TagsController#index` の count は `q.size` であり、総件数計算として不安定。
-6. Wiki 本文検索は未実装であり、実装時には全文検索インデックス設計が必要。
-
-# 過去仕様書からの主要差分
-
-| 項目 | 2026-03-23 仕様書 | 2026-05-10 現行実装 |
+| API | 権限 | 内容 |
| --- | --- | --- |
-| BAN | 未接続と記述 | ApplicationController で IP / User BAN 強制済み |
-| 投稿親子 | parent_id 前提の記述あり | post_implications 多対多 |
-| 投稿履歴 | タグ履歴中心 | post_versions API あり |
-| タグ更新 | name/category 中心 | aliases / parents / deerjikists 更新も実装 |
-| Wiki 更新 | title 変更不可と記述 | title 変更可。tag version も記録 |
-| Wiki 競合制御 | 活用されていないと記述 | base_revision_id を受け取り 409 を返す |
-| 素材 | 旧仕様書では薄い/無し | materials / material_versions / 画面あり |
-| Theatre コメント順 | no ASC と記述 | 現行 API は no DESC |
-| YouTube | 一部計画寄り | Youtube::Sync 実装あり |
-| 利用規約 | 計画 | `/tos` MDX ルートあり。ただし内容精査は別途必要 |
+| `GET /deerjikists/:platform/:code` | public | 紐づけ取得 |
+| `PUT /deerjikists/:platform/:code` | member+ | 紐づけ作成/更新 |
+| `DELETE /deerjikists/:platform/:code` | member+ | 紐づけ削除 |
+| `PUT /tags/:id/deerjikists` | member+ | タグ側から複数紐づけ更新 |
-# 現行で「ある」と言ってよいもの
+YouTube では `@handle` が渡された場合、YouTube ページを取得して `UC...` channel ID へ正規化する処理がある。
-- 引継ぎコード認証。
-- guest / member / admin ロール。
-- IP BAN / User BAN の API 強制。
-- 投稿 CRUD のうち作成・参照・更新。
-- 投稿タグ検索、AND / OR / NOT、別名 canonicalise。
-- 投稿親子多対多。
-- 投稿本体バージョン履歴。
-- 投稿タグ付与履歴。
-- タグ一覧・検索・補完。
-- タグ別名・親タグ更新 API。
-- タグ履歴。
-- Nico タグ連携。
-- YouTube 同期。
-- ニジラー対応管理。
-- Wiki 作成・参照・更新・差分・履歴。
-- Wiki 競合検出。
-- 素材一覧・作成・更新・削除。
-- 上映会表示・在席・次投稿・コメント。
-- ユーザ設定の一部。
-- 利用規約ページのルート。
+### 注意
-# 現行で「あるつもり」は危険なもの
+YouTube handle 正規化は外部 HTML 取得に依存してゐる。失敗時は `nil` になり得るため、UI 側のエラー誘導が重要。
-- 通常のアカウント登録/ログイン。
-- 管理画面。
-- user / IP BAN の UI。
-- 投稿削除 API。
-- 素材 version 表示 API。
-- Theatre 一覧・作成・編集 API。
-- Wiki 本文検索。
-- Wiki 画像添付の画面導線。
-- Wiki redirect 作成機能。
-- 投稿更新の optimistic locking。
-- settings テーブルを使った汎用ユーザ設定。
-- `youtu.be` / Shorts / Bluesky / Pixiv 専用埋め込み。
-- preview API の安全仕様。
-- ゲスト編集の公表後運用。
+# Wiki 仕様
-# 開発者ヒアリング反映結果
+## Wiki ページとタグ名
-以下は、仕様書作成時に確認した開発者回答と、それを受けた仕様上の扱いである。
+`wiki_pages` は `tag_name_id` と一対一に紐づく。
-## H-001: 素材作成権限
+- Wiki のタイトルは `tag_name.name`。
+- タグ実体ではなく tag_name に紐づくため、タグがなくても Wiki ページは存在し得る。
+- タイトル変更時は tag_name を変更する。
-現行 `POST /materials` は `current_user` があれば許可され、`gte_member?` を要求していない。
+## 現行ストレージ
-開発者回答: 当初の意図どおり guest でも素材作成可能にしていたが、オブジェクトストレージへ大量投入されるリスクがあるため、**耕作員(member)以上で制限する方向** とする。
-
-仕様反映: 計画仕様として、素材作成は member 以上へ引き上げる。現行コードとの差分として明記する。
-
-## H-002: 投稿作成・更新で parent_post_ids 必須
-
-現行 `parse_parent_post_ids` は `params.key?(:parent_post_ids)` が無いと例外にする。
-
-開発者回答: 親投稿が無い投稿でも、API クライアントは必ず `parent_post_ids: ''` を送る仕様で確定。未指定によって誤って初期化されるのを防ぐためである。「変更なし」とする挙動は冪等性に反するため避ける。
-
-仕様反映: `POST /posts` および `PUT /posts/:id` は、親投稿が無い場合でも `parent_post_ids` を必須入力とする。空文字は「親投稿なし」を意味する。未指定は不正リクエストである。
-
-補足: これは `PUT` を部分更新ではなく完全置換寄りの API として扱う設計である。外部 API 化する場合も、この思想を明記しなければ事故る。
-
-## H-003: 投稿更新の排他制御
-
-`post_versions` はあるが、`posts.version_no` は現行 schema に無く、`PUT /posts/:id` も `base_version_no` を受け取らない。
-
-開発者回答: 投稿更新に `base_version_no` を入れる計画を仕様に含める。
-
-仕様反映: 計画仕様として、投稿更新には optimistic locking を導入する。詳細画面で取得した `version_no` を更新時に `base_version_no` として送信し、サーバ側の現行版と一致しない場合は競合として扱う。
-
-## H-004: Wiki redirect の扱い
-
-`wiki_revisions.kind = redirect` は残っているが、`Wiki::Commit#redirect!` は廃止例外になっている。
-
-開発者回答: Wiki redirect は今後廃止し、タグ別名へ一本化する。過去に使われた歴史もない。
-
-仕様反映: Wiki redirect は現行スキーマ上の残存要素として扱い、将来仕様では採用しない。別名・表記揺れ・旧名からの到達は `tag_names.canonical_id` によるタグ別名機能へ集約する。
-
-## H-005: Wiki title 変更
-
-現行 `PUT /wiki/:id` は title 変更を受け付け、対応 `tag_name` を rename する。
-
-開発者回答: Wiki 編集画面からのタイトル変更を正式仕様として許可する。ただし、紐づくタグが存在しない場合はタグ詳細画面からの変更を促したい。
-
-仕様反映: Wiki title 変更は正式仕様である。ただし、タグ名と Wiki title は同一知識ノードを指すため、Wiki title rename は実質的に tag rename である。権限・履歴・衝突処理では「Wiki の表題変更」ではなく「タグ名変更」として扱う。
-
-## H-006: Theatre コメント順
-
-現行 `GET /theatres/:theatre_id/comments` は `no DESC` で返す。
-
-開発者回答: DESC で確定。
-
-仕様反映: Theatre コメント一覧は `no DESC` を正式仕様とする。差分取得・画面表示では、クライアント側が必要に応じて並び替える。
-
-## H-007: TagSidebar のタグ順
-
-現行サイドバーは「表示中投稿に出現したタグ最大 25 件」をカテゴリ別・名前順で表示する。
-
-開発者回答: 理想は Danbooru を踏襲したい。
-
-仕様反映: 現行仕様はカテゴリ別・名前順表示である。計画仕様として、Danbooru 風に現在の検索結果内での出現頻度・関連度を重視した順序へ寄せる。
-
-## H-008: settings テーブル
-
-`settings` はあるが、汎用設定 API と UI は未確認。
-
-開発者回答: テーマ、タグ色、ミュートタグ、埋め込み自動再生などを settings に集約する方針で概ねよい。ただし、見た目にしか関係しない設定は `localStorage` にすることも検討中。
-
-仕様反映: サーバ側で永続化すべき設定と、端末ごとの表示設定を分ける。ミュートタグ・非表示タグなどユーザ体験に本質的な影響を与えるものは `settings` 候補、純粋な見た目は `localStorage` 候補とする。
-
-## H-009: 素材と material / character カテゴリ
-
-現行 `Material#tag_must_be_material_category` は `character` または `material` を許可する。
-
-開発者回答: `character` タグに直接素材を 1 件だけ紐づける仕様でよい。現時点では `TagImplication` によって `character` を親タグ、関連する `material` を子タグとする運用をしており、大きな問題は生じていない。キャラクタータグは実質素材タグの集合代表である。ただし、タグ分けするほどでもないが異なる素材については、**包摂素材** として複数持たせたい。
-
-仕様反映: 現行仕様として、`character` は素材集合の代表タグ、`material` は具体素材タグとして扱う。複数素材は原則 `TagImplication` による子 `material` タグ化で表現する。計画仕様として、タグ分けするほどではない差分素材を単一 Material 配下へ複数保持する「包摂素材」を追加検討する。
-
-## H-010: 利用規約ページ
-
-`/tos` ルートはある。
-
-開発者回答: 現行 `TOSPage.mdx` は現時点で有効な利用規約である。ただし草案の域を脱していないのも事実であり、基本はこれを踏襲する。
-
-仕様反映: `/tos` は現行の有効な利用規約ページとして扱う。ただし、公表に向けて文面精査が必要な草案ベースの利用規約であることも併記する。
-
-## H-011: preview API 安全仕様
-
-現行 preview は便利だが、公開サービスとしては SSRF 対策の仕様化が必要である。
-
-開発者回答: サムネ取得に関してはかなり危うい。対案を後ほど議論する。
-
-仕様反映: preview API の安全仕様は未確定とする。ただし、既にサービスは公開状態であるため、これは「公表前の理想論」ではなく、現行公開サービスのリスクとして扱う。
-
-## H-012: YouTube 同期対象
-
-`Youtube::Sync` は検索語と playlist id から動画 ID を集める。
-
-開発者回答: 現行はコード固定であるが、いずれは DB 管理に移行したい。汎用基盤化が夢である。
-
-仕様反映: 現行仕様では YouTube 同期対象はコード固定。計画仕様として、検索語・playlist ID を DB 管理へ移行し、外部同期を汎用基盤化する余地を残す。
-
-## H-013: 管理画面の最小範囲
-
-管理 API / UI はまだ薄い。
-
-開発者回答: 管理画面は公表後に追従して実装する。現時点では、Rails Console を管理ツールとして使用している。なお、サービス自体は既に公開済みであり、ここでいう「一般公開」は「公表」の意味である。
-
-仕様反映: 現行管理運用は Rails Console ベースとする。管理画面は未実装の計画機能であり、公表後に追従実装する。ただし、BAN / 差し戻し / 最近の更新確認などは、利用者増加時に早期に必要となる。
-
-# 実装優先度メモ
-
-現行仕様を踏まえると、公表前または利用拡大前に優先すべき順は次。
-
-1. **投稿更新の排他制御**
- - Wiki は 409 を返せる。投稿も合わせるべき。
-2. **素材権限の確認・修正**
- - guest 作成可は危険。
-3. **管理画面 MVP**
- - 現行運用は Rails Console ベース。少人数運用では成立するが、利用者が増えると UI が無い状態は運用負荷になる。
-4. **preview API 安全化**
- - SSRF は利用拡大後に燃える種類の穴。
-5. **Wiki 本文検索**
- - タグ整理基盤として価値に直結。
-6. **TagSidebar の並び順改善**
- - 現行は「たまたま表示された投稿の先着タグ」で、検索支援として弱い。
-7. **Theatre コメント順・会場管理**
- - 周辺機能なので中核より後。
-
-# 付録 A: 現行 API 一覧
-
-## 投稿
-
-- GET /posts
-- GET /posts/random
-- GET /posts/changes
-- GET /posts/versions
-- GET /posts/:id
-- POST /posts
-- PUT /posts/:id
-- POST /posts/:id/viewed
-- DELETE /posts/:id/viewed
-
-## タグ
-
-- GET /tags
-- GET /tags/autocomplete
-- GET /tags/with-depth
-- GET /tags/versions
-- GET /tags/:id
-- PUT /tags/:id
-- PATCH /tags/:id
-- GET /tags/:id/deerjikists
-- PUT /tags/:id/deerjikists
-- GET /tags/name/:name
-- GET /tags/name/:name/deerjikists
-- GET /tags/name/:name/materials
-- POST /tags/:parent_id/children/:child_id
-- DELETE /tags/:parent_id/children/:child_id
-
-## Nico タグ
-
-- GET /tags/nico
-- PUT /tags/nico/:id
-
-## Wiki
-
-- GET /wiki
-- GET /wiki/search
-- GET /wiki/changes
-- GET /wiki/:id
-- PUT /wiki/:id
-- GET /wiki/:id/exists
-- GET /wiki/:id/diff
-- GET /wiki/title/:title
-- GET /wiki/title/:title/exists
-- POST /wiki
-
-## 素材
-
-- GET /materials
-- GET /materials/:id
-- POST /materials
-- PUT /materials/:id
-- DELETE /materials/:id
-
-## ユーザ
-
-- POST /users
-- POST /users/verify
-- GET /users/me
-- POST /users/code/renew
-- PUT /users/:id
-
-## ニジラー
-
-- GET /deerjikists/:platform/:code
-- PUT /deerjikists/:platform/:code
-- DELETE /deerjikists/:platform/:code
-
-## プレビュー
-
-- GET /preview/title
-- GET /preview/thumbnail
-
-## 上映会
-
-- GET /theatres/:id
-- PUT /theatres/:id/watching
-- PATCH /theatres/:id/next_post
-- GET /theatres/:theatre_id/comments
-- POST /theatres/:theatre_id/comments
-
-# 付録 B: 主要テーブル一覧
+Wiki は本文を `wiki_pages.body` に持つだけではない。現行は行単位の改訂履歴を持つ。
| テーブル | 役割 |
| --- | --- |
-| posts | 投稿リンク本体 |
-| post_implications | 投稿親子関係 |
-| post_tags | 投稿タグ関係と付与履歴 |
-| post_versions | 投稿本体・タグ・親投稿構成の版 |
-| tags | タグ実体 |
-| tag_names | タグ名と別名 |
-| tag_implications | タグ親子関係 |
-| tag_versions | 通常タグ履歴 |
-| nico_tag_versions | nico タグ連携履歴 |
-| tag_name_sanitisation_rules | タグ名サニタイズ規則 |
-| nico_tag_relations | nico タグと内部タグの対応 |
-| deerjikists | 外部人物識別子とタグの対応 |
-| materials | 素材 |
-| material_versions | 素材履歴 |
-| wiki_pages | Wiki ページ |
-| wiki_revisions | Wiki 改訂 |
-| wiki_lines | Wiki 行本文ストア |
-| wiki_revision_lines | Wiki 改訂と行の対応 |
-| wiki_versions | Wiki スナップショット履歴 |
-| wiki_assets | Wiki 添付資産 |
-| users | ユーザ |
-| ip_addresses | IP 記録と IP BAN |
-| user_ips | ユーザと IP の対応 |
-| user_post_views | 閲覧済み |
-| post_similarities | 投稿類似度 |
-| tag_similarities | タグ類似度 |
-| theatres | 上映会 |
-| theatre_comments | 上映会コメント |
-| theatre_watching_users | 上映会在席 |
-| settings | 汎用設定候補 |
+| `wiki_pages` | ページ本体/現在情報 |
+| `wiki_revisions` | 改訂ヘッダ。kind/content/redirect、tree_sha 等 |
+| `wiki_lines` | 行本文を SHA256 で重複排除 |
+| `wiki_revision_lines` | 改訂と行の順序 |
+| `wiki_versions` | ページ title/body の version snapshot |
-# 付録 C: 確認した主要ソース
+## 作成/更新
-- backend/config/routes.rb
-- backend/db/schema.rb
-- backend/app/controllers/application_controller.rb
-- backend/app/controllers/posts_controller.rb
-- backend/app/controllers/tags_controller.rb
-- backend/app/controllers/nico_tags_controller.rb
-- backend/app/controllers/post_versions_controller.rb
-- backend/app/controllers/tag_versions_controller.rb
-- backend/app/controllers/wiki_pages_controller.rb
-- backend/app/controllers/materials_controller.rb
-- backend/app/controllers/theatres_controller.rb
-- backend/app/controllers/theatre_comments_controller.rb
-- backend/app/controllers/users_controller.rb
-- backend/app/models/post.rb
-- backend/app/models/tag.rb
-- backend/app/models/tag_name.rb
-- backend/app/models/material.rb
-- backend/app/models/wiki_page.rb
-- backend/app/models/wiki_revision.rb
-- backend/app/models/user.rb
-- backend/app/models/ip_address.rb
-- backend/app/services/wiki/commit.rb
-- backend/app/services/youtube/sync.rb
-- frontend/src/App.tsx
-- frontend/src/components/PostEmbed.tsx
-- frontend/src/components/TagSidebar.tsx
-- frontend/src/pages/materials/*
\ No newline at end of file
+Wiki 作成/更新は member 以上。
+
+`Wiki::Commit.create_content!` は次を行ふ。
+
+- CRLF を LF へ統一。
+- 不正 UTF-8 を置換文字で救済。
+- 末尾改行を strip。
+- 行ごとに SHA256 を計算。
+- 未登録行だけ `wiki_lines` に upsert。
+- `wiki_revision_lines` に行順を保存。
+- `wiki_versions` を記録。
+- `base_revision_id` が渡された場合、現在 revision と一致しなければ conflict。
+
+## 差分
+
+`GET /wiki/:id/diff` は `diff-lcs` による行差分を返す。
+
+返却 type:
+
+- `context`
+- `added`
+- `removed`
+
+content revision 同士のみ差分対象。
+
+## redirect
+
+`Wiki::Commit.redirect!` は現行では `raise '廃止しました.'` で無効化されてゐる。ただし schema と controller には redirect revision の痕跡がある。
+
+## 注意: wiki_assets
+
+`wiki_assets` テーブルは schema に存在するが、現行ソースで明確な model/controller/画面導線は確認できない。Wiki 内画像/添付の将来設計または残骸候補である。
+
+# 素材仕様
+
+## Material
+
+`materials` はタグに紐づく素材である。
+
+| 属性 | 仕様 |
+| --- | --- |
+| `tag_id` | 必須・一意 |
+| `url` | 任意。file がなければ必須 |
+| `file` | Active Storage 添付。url がなければ必須 |
+| `parent_id` | 親素材。任意 |
+| `created_by_user_id` | 作成者 |
+| `updated_by_user_id` | 更新者 |
+| `discarded_at` | 論理削除 |
+
+`Material` は `MyDiscard` ではなく独自に `default_scope -> { kept }` を持つ。
+
+## タグ制約
+
+素材に紐づくタグは `character` または `material` カテゴリでなければならない。
+
+注意: バリデーション文言は「素材カテゴリのタグ」と言ってゐるが、実装上は `character` も許可してゐる。
+
+## API
+
+| API | 権限 | 内容 |
+| --- | --- | --- |
+| `GET /materials` | public | 素材一覧 |
+| `GET /materials/:id` | public | 素材詳細。関連 Wiki 本文も返す |
+| `POST /materials` | current_user 必須 | 素材作成 |
+| `PUT/PATCH /materials/:id` | member+ | 素材更新 |
+| `DELETE /materials/:id` | member+ | 論理削除 |
+
+### 注意: 作成権限が緩い
+
+`POST /materials` は `current_user` があれば通る。フロントは初回訪問者に guest を自動作成するため、実質的に guest でも素材作成可能である。`docs/roadmap.md` にも `Tighten material creation permission` があるため、ここは既知の弱点と見るべきである。
+
+### 注意: material_versions
+
+`material_versions` テーブルは schema にあるが、現行ソースで MaterialVersion model / recorder / controller 利用は確認できない。素材履歴は未実装または残骸候補である。
+
+# 上映会仕様
+
+## 概要
+
+`theatres` は共同視聴部屋である。
+
+| 属性 | 意味 |
+| --- | --- |
+| `name` | 部屋名 |
+| `opens_at` / `closes_at` | 開始/終了 |
+| `kind` | 種別 |
+| `current_post_id` | 現在再生中投稿 |
+| `current_post_started_at` | 再生開始時刻 |
+| `host_user_id` | 現在ホスト |
+| `next_comment_no` | コメント採番 |
+
+## 在席とホスト
+
+`PUT /theatres/:id/watching` はログイン済みユーザを在席として更新する。
+
+- `theatre_watching_users.expires_at` は 30 秒後。
+- 現在 host がいない、または host が active でなければ、呼び出しユーザが host になる。
+- フロントは約 1.5 秒ごとに watching を送る。
+
+返却:
+
+- `host_flg`
+- `post_id`
+- `post_started_at`
+- `post_elapsed_ms`
+- `watching_users`
+- `skip_vote`
+
+### 注意
+
+ホスト選出はかなり自動的で、悪意あるユーザが active host になる余地がある。公表後に荒らし耐性を考へるなら、host 権限の条件を仕様化する必要がある。
+
+## 次投稿選択
+
+`PATCH /theatres/:id/next_post` は host のみ実行可能。
+
+`TheatrePostSelector` は次の投稿を候補にする。
+
+- `url` に `nicovideo.jp`, `youtube.com/watch`, `youtu.be` を含む投稿。
+- 現在投稿は除外。
+
+重みは `1.0 / (1.0 + penalty)`。
+
+`penalty` は、active user が過去に skip した投稿のタグに基づく。つまり、視聴者が嫌がったタグを持つ投稿ほど選ばれにくい。
+
+## 番組表
+
+`theatre_programmes` は再生履歴/番組表である。
+
+| カラム | 意味 |
+| --- | --- |
+| `theatre_id` | 部屋 |
+| `position` | 連番位置 |
+| `post_id` | 投稿 |
+| `created_at` | 追加時刻 |
+
+`TheatrePostAdvancer` は次投稿へ進むたびに position を加算して programme を作る。
+
+## コメント
+
+`theatre_comments` は部屋内コメントである。
+
+- 主キーは `(theatre_id, no)`。
+- 投稿時に theatre を lock し、`next_comment_no` を採番する。
+- 削除は投稿者本人のみ。
+- 削除後は `discarded_at` を設定し、一覧では `content: null`, `deleted: true` として返す。
+- フロントは最新 20 件を中心に取得する。
+
+## スキップ投票
+
+`theatre_skip_votes` は `(theatre_id, post_id, user_id)` 複合主キー。
+
+`PUT /skip_vote`:
+
+1. ログイン必須。
+2. `post_id` 必須。
+3. watching を更新。
+4. 現在投稿と request post_id が違へば 409。
+5. vote 作成。
+6. active users の過半数に達したら skip 確定。
+
+必要票数:
+
+```txt
+required_count = floor(active_watching_users_count / 2) + 1
+```
+
+スキップ確定時:
+
+- `TheatreSkipFinalizer` が `theatre_skip_events` を作る。
+- voter 一覧を `theatre_skip_event_voters` に保存。
+- skip 時点の投稿タグを `theatre_skip_event_tags` に保存。
+- 当該投稿の skip vote を削除。
+- `TheatrePostAdvancer` で次投稿へ進む。
+
+`DELETE /skip_vote` で自分の投票を取り消せる。
+
+## 再生同期
+
+フロント `TheatreDetailPage` は server elapsed と player current time の差が 5 秒を超えた場合に seek する。
+
+- YouTube は `react-youtube`。
+- ニコニコは `NicoViewer` による JS API `postMessage`。
+- duration が 0 以下など再生不能っぽい場合、host が次投稿へ進める。
+
+## 埋め込み
+
+`PostEmbed` の対応:
+
+| URL | 表示 |
+| --- | --- |
+| NicoVideo | `NicoViewer` iframe + postMessage |
+| YouTube | `react-youtube` |
+| Twitter/X status | blockquote + widgets script |
+| その他 | 確認後 iframe |
+
+ニコニコ iframe の `loadComplete` timeout は 8000 ms。
+
+# Gekanator 仕様
+
+## 概要
+
+Gekanator は管理者専用の投稿当てゲームである。投稿群に関する質問を出し、回答から候補投稿を絞り、最終的に投稿を推測する。
+
+用途はゲームだけではない。終了後の質問追加・回答保存により、投稿間類似や識別質問を蓄積する学習機構でもある。
+
+## 権限
+
+バックエンド API は `current_user&.admin?` でなければ 404 を返す。フロントも `/gekanator` は admin 以外 `NotFound` を表示する。
+
+## 投稿カタログ
+
+`GET /gekanator/posts` は admin 専用。
+
+返却投稿:
+
+- 全投稿。
+- `tags` preload。
+- サムネイル付き。
+- 並びは `COALESCE(original_created_before - 1分, original_created_from, created_at) DESC, id DESC`。
+
+## 質問カタログ
+
+`GET /gekanator/questions` は admin 専用。
+
+質問 kind:
+
+| kind | 内容 |
+| --- | --- |
+| `tag` | 特定タグを含むか |
+| `source` | URL host/source |
+| `title` | タイトル性質 |
+| `original_date` | 年/月/月日 |
+| `post_similarity` | 特定投稿との近さ/例示回答 |
+
+質問 source:
+
+- `user_suggested`
+- `ai_generated`
+- `admin_curated`
+
+status:
+
+- `pending`
+- `accepted`
+- `rejected`
+- `disabled`
+
+現行 API は accepted questions を返す。
+
+## 回答値
+
+`GekanatorQuestionSuggestion::ANSWERS`:
+
+| 値 | 意味 |
+| --- | --- |
+| `yes` | はい |
+| `no` | いいえ |
+| `partial` | 部分的にそう |
+| `probably_no` | たぶん違ふ |
+| `unknown` | わからない |
+
+## フロント推測ロジック
+
+`frontend/src/lib/gekanator.ts` と `GekanatorPage.tsx` による。
+
+主な定数:
+
+| 定数 | 値 | 意味 |
+| --- | --- | --- |
+| `questionsBetweenGuesses` | 25 | 通常推測までの質問数 |
+| `minQuestionsBeforeCertainGuess` | 5 | 確信時の最低質問数 |
+| `certainGuessPercent` | 99.5 | ほぼ確定判定 |
+| `runnerUpMaxPercent` | 0.5 | 2位候補の上限 |
+| `hardMaxQuestions` | 80 | 最大質問数 |
+| `softenedAnswerWeight` | 0.35 | 答え緩和重み |
+| `confidenceTemperature` | 6 | 確率化温度 |
+| `maxQuestionSuggestionsPerGame` | 3 | 追加質問上限 |
+
+質問選択は、候補分割力、冗長性、排他条件、優先度、seed による決定性を組み合はせる。
+
+候補が潰れる場合、高難度・非 unknown の過去回答を `0.35` に緩和して復旧を試みる。
+
+## ゲーム保存
+
+`POST /gekanator/games` は admin 専用。
+
+入力:
+
+- `guessed_post_id`
+- `correct_post_id`
+- `answers` JSON
+
+保存値:
+
+- `won = guessed_post_id == correct_post_id`
+- `question_count = answers.length`
+
+## 質問追加
+
+終了後、ユーザは質問を最大 3 件追加できる。
+
+`POST /gekanator/question_suggestions`:
+
+- admin 専用。
+- `game_id`
+- `question_text` 1000 文字以内。
+- `answer` は enum。
+- `unknown` は promoter で質問化されない。
+
+`Gekanator::QuestionSuggestionPromoter` は unknown 以外を accepted な `post_similarity` question として即昇格させ、correct_post に対する example を作る。
+
+## 追加質問回答
+
+`GET /gekanator/games/:id/extra_questions` は、ゲーム後に correct_post へ未回答の `post_similarity` question を最大 2 件返す。
+
+`POST /extra_question_answers` は、それらへの回答を `GekanatorQuestionExample` として保存する。
+
+## AI 変換
+
+`POST /gekanator/question_suggestions/:id/ai_convert` は存在するが、`Gekanator::QuestionSuggestionAiConverter` は現行 `NotImplementedError` を投げる。
+
+AI 予算:
+
+| 項目 | 値 |
+| --- | --- |
+| 月上限 | 450 円 |
+| 1 run 見積 | 5 円 |
+| 超過見込み | 402 `blocked_budget` |
+
+現行では API 枠だけが先行してゐる。
+
+# プレビュー API
+
+## タイトル取得
+
+`GET /preview/title`:
+
+- current_user 必須。
+- URL 必須。
+- scheme がなければ `http://` を補完。
+- `URI.open` で HTML 取得。
+- Nokogiri で `` を返す。
+- open/read timeout は 5 秒。
+
+## サムネイル生成
+
+`GET /preview/thumbnail`:
+
+- current_user 必須。
+- URL 必須。
+- scheme がなければ `http://` を補完。
+- `node lib/screenshot.js ` を実行。
+- 生成画像を MiniMagick で 180x180 に resize。
+- PNG inline で返す。
+
+### 注意: SSRF/任意URLアクセスリスク
+
+この API は current_user 必須だが、guest は自動作成されるため実質的に広く使へる。任意 URL をサーバから取得・Node screenshot するので、SSRF/内部ネットワーク到達/リソース消費のリスクがある。roadmap に `Harden preview API` があるのは妥当で、これは公表前の赤信号である。
+
+# 類似度計算
+
+`Similarity::Calc` は汎用化されてゐる。
+
+| task | 対象 | 関係 |
+| --- | --- | --- |
+| `post_similarity:calc` | Post | tags による投稿類似 |
+| `tag_similarity:calc` | Tag | posts によるタグ類似 |
+
+計算仕様:
+
+- 対象集合 ID を sort。
+- 2集合の intersection size を計算。
+- `cos = intersection / sqrt(|a| * |b|)`。
+- 各 record につき上位 20 件を保存。
+- similarity table は全削除後に insert_all。
+
+# データベース仕様
+
+## 現行 schema の主要テーブル
+
+2026-06-10 migration まで反映された schema では、主に次のテーブルが存在する。
+
+| 領域 | テーブル |
+| --- | --- |
+| Active Storage | `active_storage_blobs`, `active_storage_attachments`, `active_storage_variant_records` |
+| ユーザ/BAN | `users`, `ip_addresses`, `user_ips`, `settings`, `user_post_views` |
+| 投稿 | `posts`, `post_tags`, `post_implications`, `post_versions`, `post_similarities` |
+| タグ | `tags`, `tag_names`, `tag_implications`, `tag_versions`, `tag_similarities`, `tag_name_sanitisation_rules` |
+| Nico | `nico_tag_relations`, `nico_tag_versions` |
+| ニジラー | `deerjikists` |
+| Wiki | `wiki_pages`, `wiki_revisions`, `wiki_lines`, `wiki_revision_lines`, `wiki_versions`, `wiki_assets` |
+| 素材 | `materials`, `material_versions` |
+| 上映会 | `theatres`, `theatre_comments`, `theatre_watching_users`, `theatre_programmes`, `theatre_skip_votes`, `theatre_skip_events`, `theatre_skip_event_tags`, `theatre_skip_event_voters` |
+| Gekanator | `gekanator_games`, `gekanator_questions`, `gekanator_question_suggestions`, `gekanator_question_examples`, `gekanator_ai_runs` |
+
+## 旧本番 DB ダンプの利用実態
+
+2026-04-25 ダンプは現行 schema より古い。Gekanator、上映会スキップ、投稿親子など一部は含まれてゐない。とはいへ、当時の利用規模を把握するには有用である。
+
+主な行数:
+
+| テーブル | 行数 |
+| --- | ---: |
+| `users` | 42805 |
+| `posts` | 893 |
+| `tags` | 5961 |
+| `tag_names` | 5996 |
+| `post_tags` | 45345 |
+| `post_versions` | 14351 |
+| `tag_versions` | 2026 |
+| `nico_tag_versions` | 3842 |
+| `nico_tag_relations` | 314 |
+| `deerjikists` | 99 |
+| `materials` | 37 |
+| `wiki_pages` | 51 |
+| `wiki_revisions` | 197 |
+| `theatres` | 1 |
+| `theatre_comments` | 98 |
+| `post_similarities` | 43140 |
+| `tag_similarities` | 116760 |
+
+解釈:
+
+- `users` が極端に多い。自動 guest 生成と bot/巡回アクセスの影響が濃い。
+- `post_tags` が 4.5 万件あり、投稿あたりタグ量はかなり多い。
+- `post_versions` が 1.4 万件あり、同期/編集履歴が大量に蓄積されてゐる。
+- 類似度テーブルは `posts * 20` / `tags * 20` に近い規模で、静的計算済みキャッシュとして動いてゐる。
+
+# フロントエンド仕様
+
+## API 通信
+
+`frontend/src/lib/api.ts`:
+
+- Axios base URL は `API_BASE_URL`。
+- 全リクエストに `X-Transfer-Code` を付与。
+- JSON レスポンスは deep camelCase 化。
+- `responseType: 'blob'` の場合は変換しない。
+
+## 型定義
+
+`frontend/src/types.ts` は API レスポンスの期待形を定義する。ただし、前述の `PostVersion.parentPosts` のやぅに、実 API とズレてゐる箇所がある。
+
+## 編集権限
+
+`canEditContent` は `admin` または `member` を true とする。
+
+## 画面設計上の特徴
+
+- `App.tsx` 初期化時に guest 自動作成。
+- route transition に `framer-motion`。
+- 上部ナビ `TopNav`。
+- MDX 利用規約。
+- Gekanator は admin only route。
+- Post detail は pathname を key にして再 mount。
+
+# テスト状況
+
+## バックエンド
+
+RSpec が整備されてゐる。確認できる主要テスト領域:
+
+- users / auth / BAN 系。
+- posts / post implications / versions / conflict。
+- tags / aliases / nico tags / tag children / deerjikists。
+- wiki / commit / diff / conflict / title collision / history integrity。
+- materials。
+- theatres / comments / programmes。
+- Gekanator games / learning。
+- YouTube/Nico sync tasks。
+- similarity calculation。
+
+## フロントエンド
+
+Vitest が導入され、かなり広範囲にテストがある。
+
+確認できる主な領域:
+
+- Post 系 components/pages。
+- Tag 系 components/pages。
+- Wiki pages。
+- Materials pages。
+- Theatre page。
+- Gekanator scoring lib。
+- API utility / error handling。
+- common UI components。
+- NicoViewer / PostEmbed / TwitterEmbed。
+
+2026-05-10 時点の「フロントテストなし」状態からは明確に改善してゐる。
+
+# 実装上の不整合・危険点
+
+この節は慰めではなく、仕様確定前に潰すべき箇所である。ここを曖昧にしたまま公表すると、後で設計負債が増える。
+
+## P0: Preview API の SSRF/リソース消費リスク
+
+`/preview/title` と `/preview/thumbnail` はサーバから任意 URL へアクセスする。guest 自動生成により、事実上かなり広い入口である。
+
+必要な対策候補:
+
+- private IP / localhost / link-local / metadata IP の拒否。
+- scheme allowlist を http/https に固定。
+- redirect 先検査。
+- content length 上限。
+- screenshot queue/timeout/concurrency 制限。
+- member 以上へ制限するか、rate limit を置く。
+
+## P0: guest 自動作成による users 肥大化
+
+旧 DB で `users = 42805`。実利用規模に比して異様に多い。bot が来るたび user を発行してゐる可能性が高い。
+
+機会費用:
+
+- BAN/権限/ユーザ管理がノイズで死ぬ。
+- 本物ユーザの分析ができない。
+- DB とバックアップが無駄に膨らむ。
+
+少なくとも「閲覧だけの初回アクセスで user を作る」設計は再検討対象。
+
+## P0: 素材作成権限が緩い
+
+`POST /materials` は current_user だけを要求する。guest 自動作成と合はせると、匿名同然で素材作成できる。
+
+roadmap にも `Tighten material creation permission` がある。これは既知の穴として扱ふべき。
+
+## P1: タグ親子編集権限が二重化
+
+- `TagChildrenController`: admin のみ。
+- `TagsController#update_all`: member 以上で `parent_tags` 更新可。
+
+どちらを正とするか決める必要がある。現状は仕様が二枚舌。
+
+## P1: `post_versions` の parent post 履歴 API が不足
+
+schema と snapshot には `parent_post_ids` があるが、`PostVersionsController` が返してゐない。フロント型には `parentPosts` がある。履歴画面で親投稿変更を出すなら API 追加が必要。
+
+## P1: `material_versions`, `wiki_assets` の幽霊テーブル
+
+schema にあるが、現行導線が薄い/見当たらない。将来機能なら仕様化、残骸なら削除か凍結判断が必要。
+
+## P1: inheritance_code の一意制約なし
+
+認証トークンである以上、DB 一意 index を張るべき。UUID 衝突は現実的に低くても、仕様としては弱い。
+
+## P2: Material のバリデーション文言と許可カテゴリのズレ
+
+文言は「素材カテゴリ」だが、実装は `character` も許可する。仕様として `character` も素材を持てるなら文言を直すべき。
+
+## P2: Gekanator AI 変換 API は未実装
+
+API と予算 model はあるが converter は NotImplemented。画面から到達できるなら UX 上は「未実装」と明示した方がよい。
+
+## P2: 上映会 host 乗っ取り耐性
+
+active host が切れたら次の watching ユーザが host になる。小規模内輪ではよいが、公表後は荒らしが再生制御を握る可能性がある。
+
+# 開発者ヒアリング
+
+以下は、仕様書へ確定反映する前に開発者確認が必要な事項である。回答を受けたら「回答済み」として本書の該当節へ反映する。
+
+## H-001: タグ親子編集の正しい権限
+
+現状、タグ親子 API は admin 限定だが、タグ full update は member でも `parent_tags` を更新できる。
+
+質問:
+
+1. タグ親子関係の編集は admin 限定にしたいのか。
+2. それとも member にも許可したいのか。
+3. UI のタグ編集フォームで親タグ欄を出す対象は誰か。
+
+## H-002: 素材作成権限
+
+`POST /materials` は current_user のみ要求してゐる。
+
+質問:
+
+1. guest に素材作成を許す意図か。
+2. member 以上に制限すべきか。
+3. URL-only 素材と file 素材で権限を分ける必要はあるか。
+
+## H-003: guest 自動作成の継続可否
+
+旧 DB で users が 42805 件ある。
+
+質問:
+
+1. 初回閲覧だけで guest user を作る仕様を継続するか。
+2. 閲覧者には user を作らず、編集/投票/コメント時に初めて作る形へ変更するか。
+3. bot 判定や cookie/localStorage なしアクセスへの扱ひをどうするか。
+
+## H-004: post_versions の親投稿履歴
+
+`post_versions.parent_post_ids` は保存されるが、履歴 API が返してゐない。
+
+質問:
+
+1. 投稿履歴画面で親投稿追加/削除を表示したいか。
+2. 表示するなら、当時の title snapshot が必要か、現在 title 参照でよいか。
+3. `parent_post_ids` だけでよいか、version API で `parentPosts` を返すべきか。
+
+## H-005: material_versions の扱ひ
+
+`material_versions` が schema に存在するが、利用実装が薄い。
+
+質問:
+
+1. 素材にも履歴を持たせる予定か。
+2. 予定があるなら snapshot 対象は URL、tag、parent、file blob、作成/更新者のどれか。
+3. 不要なら migration 残骸として扱ふか。
+
+## H-006: wiki_assets の扱ひ
+
+`wiki_assets` が存在する。
+
+質問:
+
+1. Wiki 内画像/添付ファイル機能を実装する予定か。
+2. `wiki_pages.next_asset_no` と連動する想定か。
+3. Active Storage と `wiki_assets.sha256` の関係はどう設計するか。
+
+## H-007: Gekanator の公開範囲
+
+Gekanator は admin-only かつ非 admin には 404。
+
+質問:
+
+1. 今後も完全 admin ツールか。
+2. member へ開放する予定はあるか。
+3. 一般ユーザ向けゲームとして公表する可能性はあるか。
+
+## H-008: Gekanator AI 変換
+
+AI converter は未実装。
+
+質問:
+
+1. 使用モデル/API は何を想定してゐるか。
+2. 月 450 円/1 run 5 円という予算は確定か仮置きか。
+3. AI は質問分類だけするのか、既存投稿への回答補完もするのか。
+
+## H-009: 上映会 host 権限
+
+host は active watching user から自動選出される。
+
+質問:
+
+1. 公表後も誰でも host になれる仕様でよいか。
+2. theatre ごとに host 候補を member/admin に限定するか。
+3. skip 投票と next_post の権限を分離するか。
+
+## H-010: Preview API の制限方針
+
+Preview API は危険度が高い。
+
+質問:
+
+1. preview は member 以上に制限してよいか。
+2. guest に許すなら rate limit はどこで実装するか。
+3. SSRF 対策として private IP 拒否・redirect 検査・content length 上限は必須でよいか。
+
+## H-011: 課題一覧の取得方法
+
+指定された Gitea API は、この実行環境では取得に失敗した。
+
+質問:
+
+1. issue JSON をファイルとして渡す運用にするか。
+2. read:issue token 付きの安全な取得方法を用意するか。
+3. ChatGPT へ渡すべき issue は全件か、open + P0/P1 のみか。
+
+# 次回更新方針
+
+1. 上記ヒアリング回答を反映する。
+2. Gitea issue 一覧を取得でき次第、仕様上の未実装/既知不具合/優先度と照合する。
+3. RSpec/Vitest/build/lint の実行結果を追記し、仕様ではなく検証報告として分離する。
+4. API レスポンス例を主要 endpoint ごとに追加する。
+5. DB ER 図またはテーブル関係図を別紙化する。
\ No newline at end of file