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 50307b2..2a1b853 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,565 +1,830 @@
---
-title: 'BTRC Hub / タグ広場 現行実装仕様書'
-subtitle: 'ソース / Wiki / 旧本番DB / 開発者ヒアリング / Gitea 課題一覧 反映版'
-date: '2026-06-11'
+title: 'BTRC Hub / タグ広場 製造仕様書'
+subtitle: '2026-07-09 差替資材反映版: 現行資材・Wiki・DB・未実装方針を統合した将来実装基準'
+date: '2026-07-09'
lang: ja-JP
toc: true
toc-depth: 3
-numbersections: true
+numbersections: false
---
-# 本書の位置づけ
+# 0. 本書の位置づけ
-本書は、添付された `btrc-hub-main.zip` の現行ソース、`btrc-hub.wiki.zip` の Wiki、`btrc_hub_20260425時点本番DBデータ.zip` の旧本番 DB ダンプ、既存仕様書 `BTRC_Hub_現行実装仕様書_2026-05-10_ヒアリング反映版.md`、開発者ヒアリング回答、添付 Gitea 課題一覧 JSON を確認し、2026-06-11 時点のタグ広場の実装事実と確定意図を再構成した仕様書である。
+本書は、BTRC Hub / タグ広場を **ゼロから製造開始できる水準** まで仕様化するための基準文書である。
-本書は願望ではなく、まずコード・スキーマ・画面・テストに現れてゐる事実を書く。その上で、開発者ヒアリング回答を仕様決定として本文へ反映する。実装が仕様決定に追いついてゐない箇所は `仕様決定済み・実装未反映` として明示する。
+既存実装の説明書ではない。現行ソース、Wiki、DB ダンプ、既存仕様書、直近の設計会話、未起票の構想を統合し、**実装済みか未実装かを本文上では区別せず**、今後作るべき完成形を仕様として定義する。
-## 確認対象
+ただし、製造判断に必要な危険点や未決定点は隠さない。未決定事項は「質問票」として末尾に隔離する。本文の仕様は、質問への回答が来るまでの暫定既定値である。
-| 区分 | 確認対象 |
+## 0.1 確認した資材
+
+| 区分 | 内容 |
| --- | --- |
-| バックエンド | 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 issue JSON 50 件。全件 open。P1/P2/P3・status・area・type を集計して反映 |
+| 現行ソース | `btrc-hub-main.zip`。2026-07-09 差替版。Rails API、React/Vite frontend、RSpec/Vitest、routes、schema、services |
+| Wiki | `btrc-hub.wiki.zip`。Home、実装説明書、テーブル定義書、環境構築手順、移行方針、グカネータ要件 |
+| DB | `btrc_hub_20260617時点本番DBデータ.zip`。本番 SQL dump |
+| 既存仕様書 | `BTRC_Hub_現行実装仕様書_2026-06-11_完成版.md` |
+| 直近設計 | タグ廃止、Gekanator、局所記載、素材管理、Git 管理、オブジェクト・ストレージ、索引抑止、除票 |
+| 課題管理 | Gitea Issues を源泉とする。今回の生成環境では API JSON の全件抽出は完全にはできなかったため、既存仕様書内の issue 反映分とリポジトリ文書を併用した |
-## 表記ルール
+## 0.2 2026-06-17 本番 DB 規模
+
+差替後の `btrc_hub_20260617時点本番DBデータ.zip` から確認した主要件数は次の通りである。小規模メモ帳ではなく、すでに履歴・類似度・同期・上映会・Gekanator のデータを持つ運用系である。
+
+| テーブル | 件数 | 読み取り |
+| --- | ---: | --- |
+| `users` | 15,620 | guest 自動作成の影響を受ける。bot・cookie なしアクセス対策が必要 |
+| `posts` | 1,167 | 投稿は千件規模。履歴・類似度・同期の影響がすでに無視できない |
+| `tags` | 6,243 | タグ体系は大きく、廃止・抑止・親子循環対策が必須 |
+| `tag_names` | 6,296 | 別名・Wiki 結合点まで含めた名称空間が大きい |
+| `post_tags` | 48,294 | 投稿あたり多数タグ。タグベース制御の影響範囲が広い |
+| `post_versions` | 15,822 | 履歴が中核データになっている |
+| `tag_versions` | 2,385 | タグ履歴も運用済み |
+| `nico_tag_versions` | 3,957 | Nico 連携の変更履歴も大きい |
+| `nico_tag_relations` | 317 | 外部タグ連携は仕様対象として扱う必要がある |
+| `deerjikists` | 190 | ニジラー紐づけは同期の基礎データ |
+| `post_similarities` | 48,900 | 投稿類似度は計算済みキャッシュとして利用 |
+| `tag_similarities` | 122,560 | タグ類似度も同様 |
+| `materials` | 37 | 素材機能は初期規模だが、storage 圧迫リスクが強い |
+| `wiki_pages` | 51 | Wiki は既に実データを持つ |
+| `wiki_revisions` | 201 | Wiki 履歴は運用済み |
+| `theatre_programmes` | 320 | 上映会は再生履歴を持つ運用機能 |
+| `theatre_comments` | 98 | コメントは既存データあり。SNS 化しない境界設計が必要 |
+| `gekanator_games` | 202 | Gekanator は試行データが蓄積開始済み |
+| `gekanator_questions` | 73 | 質問カタログは実体化済み |
+| `gekanator_question_examples` | 1,514 | 学習データはすでに仕様対象 |
+| `gekanator_question_suggestions` | 77 | 追加質問の承認/昇格フローが必要 |
+
+総行数は約 31 万件である。DB 規模を見れば、履歴・検索抑止・素材同期・設定移行を「後で何とかする」扱いにするのは危険である。
+
+
+## 0.5 2026-07-09 差替資材で追加反映した事項
+
+今回の差替資材では、前回版で記述が薄かった次を仕様本文へ追加した。
+
+| 領域 | 追加・補正内容 |
+| --- | --- |
+| 投稿 | `posts.video_ms` と `post_versions.video_ms`、局所記載の DB 形状、時間記法、動画長との整合性 |
+| タグ | `deprecated_at` の API/UI 反映、autocomplete・投稿表示・Gekanator からの除外、Nico タグ廃止禁止 |
+| 素材 | member+ 作成、素材履歴 snapshot、export items、ZIP download、同期抑止、同期元、Google Drive 同期、thumbnail 生成 |
+| 設定 | typed user settings、端末 localStorage 設定、ライト/ダーク各 3 個のユーザテーマスロット |
+| API | `/materials/download.zip`, `/materials/versions`, `/materials/suppressions`, `/users/theme_slots`, tag name 系 endpoint |
+| DB | `material_sync_sources`, `material_sync_suppressions`, `material_export_items`, `material_import_blocks`, `user_theme_slots` |
+| Frontend | 素材履歴・素材抑止・設定タブ・テーマ編集・ショートカット表・ローカル動作設定 |
+
+特に素材まわりは、単なる添付ファイル管理ではなく「外部同期、抑止、export path、ZIP 配布、履歴、thumbnail 生成」を含む標準素材基盤として扱う。
+
+## 0.3 仕様語彙
+
+| 用語 | 意味 |
+| --- | --- |
+| 公開 | URL を知っていれば到達できる状態 |
+| 公表 | 界隈へ明示的に案内し、利用者を呼び込む状態 |
+| 投稿 | 外部コンテンツへのリンク単位。本文コンテンツは原則として保持しない |
+| タグ | 投稿・Wiki・素材・Gekanator を結ぶ分類単位 |
+| Wiki | タグ名に結びつく説明ページ |
+| 素材 | タグに紐づくファイルまたは外部 URL。キャラクター立ち絵、差分、部品等を含む |
+| 除票 | 一般公開面から投稿を外す。通常フロントでは admin にも詳細を見せず、履歴・監査のみ残す状態 |
+| 索引抑止 | 外部検索エンジンに載せないため `noindex` 等を出す状態。閲覧禁止とは別 |
+| 公開抑止 | 一般ユーザから本文・詳細を見せない状態 |
+| 内部検索抑止 | タグ広場内検索・一覧・サジェストから外す状態 |
+| 廃止タグ | 通常タグとしての新規付与を禁止するタグ。外部検索 noindex とは別概念 |
+
+## 0.4 規範語
| 表記 | 意味 |
| --- | --- |
-| 現行仕様 | ソース・スキーマ・画面・テストで確認できる仕様 |
-| 実装あり・UI薄い | API/モデルはあるが画面導線が弱い、または管理導線が薄い |
-| 未実装・残骸候補 | スキーマだけある、または途中の設計痕跡はあるが呼び出し実装が薄いもの |
-| 注意 | 仕様として固定するには危険な実装差・不整合・セキュリティ上の穴 |
-| 開発者ヒアリング | コードだけでは判断不能だったが、開発者回答により仕様確定した事項 |
+| 必須 | 実装しなければならない |
+| 禁止 | 実装してはならない |
+| 推奨 | 原則として実装する。外す場合は理由を issue に残す |
+| 任意 | 実装してよいが必須ではない |
-## 本版で確定した設計判断
+# 1. システム概要
-開発者ヒアリングにより、次を仕様として確定する。
+BTRC Hub / タグ広場は、ぼざろクリーチャー関連コンテンツへのリンク、タグ、Wiki、素材、上映会、Gekanator を統合する共同編集型知識基盤である。
-| ID | 決定事項 | 実装状態 |
-| --- | --- | --- |
-| H-001 | タグ親子関係は member 以上が編集可能。UI の親タグ欄は全員に表示し、変更操作は member 以上に限る。D&D だけ admin 専用だったのは履歴管理前の安全策であり、恒久方針ではない | 一部未反映。`TagChildrenController` は admin 限定のまま |
-| H-002 | 素材作成は member 以上を原則とする。URL-only 素材だけ guest 許可の余地あり。file 素材は storage 圧迫防止のため guest 不可 | 未反映。現行 `POST /materials` は current_user のみ |
-| H-003 | 初回閲覧で guest user を作る単純設計は継続。ただし bot/cookie なしアクセス対策は未定 | 現行通り。ただし users 肥大化が残る |
-| H-004 | 投稿履歴には親投稿変更を必ず表示する。API は `parent_posts: [{ id, title }]` を返し、表示は現在 title に加へて当時 title を注記する方向 | 未反映。API が返してゐない |
-| H-005 | 素材履歴は必要。snapshot 対象は tag、URL、file blob、更新者。parent は廃止予定 | テーブルあり、実装薄い |
-| H-006 | Wiki asset は Wiki 内画像/添付として実装予定。`wiki_pages.next_asset_no` と連動し、バイト列 SHA256 をキー情報に使ふ | テーブルあり、導線未確認 |
-| H-007 | Gekanator は恒久 admin 専用ではない。管理者側で学習・調整後、一般ユーザ向けに公表する | 現行は admin 専用 |
-| H-008 | Gekanator AI は質問分類だけでなく、既存投稿への回答補完まで行ふ。初期モデルは低コスト structured output 対応モデルを環境変数で差し替へる | converter 未実装 |
-| H-009 | 上映会 host 制御は、本来サーバ側で担ふ方向。現行の active watching user 自動 host は暫定色が強い | 現行は自動 host |
-| H-010 | Preview API は guest 利用を直ちに禁止はしない。ただし private IP 拒否、redirect 検査、content length 上限等は必須。クリーンに守れないなら機能廃止も検討 | 未反映 |
-| H-011 | issue は全件共有が望ましい。今回は添付 JSON 50 件を反映する | 反映済み |
+## 1.1 目的
-この表と本文が衝突する場合、本文の「確定仕様」節を優先する。現行実装との差分は、修正対象であって仕様の揺れではない。
+1. 外部プラットフォーム上の関連コンテンツを横断的に発見できるようにする。
+2. ニコニコ等のタグ数制限を超え、より豊かなタグ体系を提供する。
+3. タグ・親子関係・別名・Wiki・素材を通じて、作品群の文脈を保持する。
+4. 上映会により共同視聴を支援する。
+5. Gekanator により投稿の識別質問・類似性・タグ付け知識を蓄積する。
+6. 問題化し得る語・ページ・投稿について、削除だけでなく索引抑止・公開抑止・除票を選べるようにする。
-## 用語: 公開と公表
+## 1.2 非目的
-現行のタグ広場は URL として到達可能であり、実質的にはすでに **公開** 状態である。一方、界隈へ明示的に告知し、利用導線を整へ、人を呼び込む状態を **公表** と呼ぶ。
-
-したがって「一般公開前」という古い表現は、本書では原則として **公表前** と読み替へる。
-
-# システム概要
-
-## 目的
-
-BTRC Hub / タグ広場は、ぼざろクリーチャー関連コンテンツへのリンクを収集し、タグ・Wiki・素材・上映会・推測ゲームを通じて、作品群と関連知識を整理・再発見しやすくする共同編集型基盤である。
-
-開発 Wiki の Home では、目的が次のやぅに整理されてゐる。
-
-- ぼざろクリーチャーシリーズ関連のあらゆるコンテンツへのリンクを保持する。
-- 各リンクにタグを付け、検索しやすくする。
-- ニコニコのタグ数制限への対応。
-- プラットフォームを超越し、あらゆるユーザによってぼざクリを一元管理できるやぅにする。
-- コンテンツ本体ではなくリンクを保持する。
-- SNS 性はなるべく排除する。
-
-## 中核ドメイン
-
-| ドメイン | 内容 |
-| --- | --- |
-| 投稿 | 外部 URL によるリンクデータ。タグ・サムネ・親子関係・閲覧済み状態・類似投稿を持つ |
-| タグ | カテゴリ、別名、親タグ、外部 Nico タグ連携、ニジラー紐づけ、素材紐づけを持つ分類単位 |
-| Wiki | タグ名と結びつく説明ページ。行単位ストアと改訂履歴を持つ |
-| 素材 | キャラクター/素材タグに紐づくファイルまたは URL |
-| 上映会 | 投稿を共同視聴し、在席・ホスト・コメント・番組表・スキップ投票を扱ふ |
-| Gekanator | 管理者向けの「投稿当て」推測ゲーム兼質問学習機構 |
-
-## 非目的
-
-現行実装は次を主目的にしてゐない。
+次は主目的ではない。
- 汎用 SNS。
- 雑談掲示板。
-- 外部コンテンツ本体の転載保存。
-- 本格的な動画配信基盤。
-- メール/パスワード式の通常アカウント管理。
-- 誰でも自由に高度編集できる完全オープン Wiki。
+- 外部コンテンツ本体の無制限な再配布。
+- 動画配信サービス。
+- メール/パスワード前提の一般的ログイン基盤。
+- 誰でも無制限に編集できる匿名 Wiki。
-ただし、上映会コメント・素材投稿・Gekanator 学習など、単なるリンク集より機能はかなり重い。仕様境界を曖昧にすると、SNS 化・荒らし対応・権限崩壊が一気に来る。ここは甘く見ないこと。
+ただし、コメント、素材、Gekanator、編集履歴を持つ以上、荒らし・ストレージ圧迫・検索流入・権利対応は避けられない。ここを軽視すると、仕様全体が砂上の楼閣になる。
-# 技術構成
+## 1.3 中核ドメイン
-## バックエンド
+| ドメイン | 概要 |
+| --- | --- |
+| Users | 引継ぎコード認証、guest/member/admin、BAN、設定 |
+| Posts | 外部 URL、タイトル、サムネイル、タグ、親投稿、元日時、閲覧済、除票、索引抑止 |
+| Tags | 名前、カテゴリ、別名、親子、廃止、索引抑止、Nico 連携、素材、Wiki |
+| Wiki | タグ名と結びつく説明ページ、行単位履歴、差分、本文検索、索引抑止 |
+| Materials | タグに紐づく素材、URL、ファイル、履歴、Git 配布、object storage |
+| Theatre | 共同視聴、ホスト、番組表、コメント、skip 投票、重み付け |
+| Gekanator | 投稿当てゲーム、質問、回答例、AI 変換、学習データ |
+| Sync | Nico/YouTube 等からの投稿同期、外部タグ連携 |
+| Moderation | BAN、除票、公開抑止、索引抑止、申請フォーム、操作履歴 |
-| 項目 | 現行 |
+# 2. 技術構成
+
+## 2.1 Backend
+
+| 項目 | 仕様 |
| --- | --- |
| 言語 | Ruby |
-| フレームワーク | Rails `~> 8.0.2` API |
-| DB | MySQL 8 系想定。Gemfile には sqlite3 も残る |
-| ファイル | Active Storage。S3/R2 互換想定あり |
+| Framework | Rails 8 API |
+| DB | MySQL 8 系を標準とする |
+| File | Active Storage。production は S3 互換 object storage、Cloudflare R2 を想定 |
| 画像処理 | image_processing / MiniMagick |
| HTML 解析 | Nokogiri |
-| Wiki 移行/旧資産 | gollum / gollum-lib |
| 差分 | diff-lcs |
-| テスト | RSpec |
-| BAN/soft delete | discard |
-| i18n | rails-i18n |
+| Test | RSpec |
+| Soft delete | `discarded_at` 系の論理削除を標準とする |
+| API 認証 | `X-Transfer-Code` による引継ぎコード認証 |
-## フロントエンド
+## 2.2 Frontend
-| 項目 | 現行 |
+| 項目 | 仕様 |
| --- | --- |
-| 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 |
+| UI | React 19 + Vite |
+| 言語 | TypeScript |
+| API | Axios。レスポンスは deep camelCase 化 |
+| State/Fetch | TanStack Query、必要に応じて localStorage |
+| Style | Tailwind CSS、ローカル UI components、Framer Motion |
+| Markdown | react-markdown、remark-gfm、Wiki autolink |
+| Head | react-helmet-async により title / meta robots を route ごとに制御 |
+| Test | Vitest、Testing Library |
-## 検証コマンド
-
-リポジトリ文書上の推奨コマンドは次である。
+## 2.3 製造時の標準検証
```sh
cd backend && bundle exec rspec
cd frontend && npm run test:run && npm run build && npm run lint
```
-# ルーティング概要
+マイグレーションを追加した場合は、少なくとも次を確認する。
-## バックエンド API
+```sh
+cd backend && bin/rails db:migrate RAILS_ENV=test
+cd backend && bin/rails db:schema:load RAILS_ENV=test
+```
-主な API は次である。
+# 3. 認証・ユーザ・権限
-| 領域 | 代表エンドポイント |
+## 3.1 認証方式
+
+通常ログインではなく、引継ぎコードを認証トークンとして用いる。
+
+1. 初回アクセス時、フロントは `localStorage.user_code` を確認する。
+2. 値があれば `POST /users/verify` で検証する。
+3. 無効または未存在なら `POST /users` で guest user を作成する。
+4. 以後、API 呼び出しでは `X-Transfer-Code` にコードを入れる。
+5. バックエンドは `users.inheritance_code` から `current_user` を設定する。
+
+`users.inheritance_code` には DB 一意制約を必須とする。認証トークンに一意制約がない設計は弱い。
+
+## 3.2 role
+
+| role | 権限 |
| --- | --- |
-| 投稿 | `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` |
+| guest | 閲覧、閲覧済み、上映会参加、コメント、skip 投票、限定的な利用 |
+| member | 投稿・タグ・Wiki・素材の編集 |
+| admin | 管理、BAN、抑止、Gekanator 調整、危険操作 |
-## フロントエンド画面
+`member` 以上を判定する共通述語を用意する。実装上は `gte_member?` のような名前でよい。
-| パス | 画面 |
-| --- | --- |
-| `/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` | その他 |
+## 3.3 権限表
+| 操作 | guest | member | admin |
+| --- | ---: | ---: | ---: |
+| 投稿閲覧 | 可 | 可 | 可 |
+| 投稿作成/更新 | 不可 | 可 | 可 |
+| 投稿除票/復帰 | 不可 | 不可 | 可 |
+| 投稿索引抑止 | 不可 | 申請可 | 可 |
+| タグ編集 | 不可 | 可 | 可 |
+| タグ親子編集 | 表示のみ | 可 | 可 |
+| タグ廃止 | 不可 | 可 | 可 |
+| タグ索引抑止 | 不可 | 申請可 | 可 |
+| Wiki 作成/更新 | 不可 | 可 | 可 |
+| Wiki 索引抑止 | 不可 | 申請可 | 可 |
+| 素材作成 | 原則不可 | 可 | 可 |
+| URL-only 素材作成 | 将来検討 | 可 | 可 |
+| file 素材作成 | 不可 | 可 | 可 |
+| 上映会コメント | 可 | 可 | 可 |
+| skip 投票 | 可 | 可 | 可 |
+| Gekanator 一般プレイ | 将来可 | 可 | 可 |
+| Gekanator 調整 | 不可 | 不可 | 可 |
+| Preview API | 可。ただし rate limit・SSRF 防御必須 | 可 | 可 |
-# 認証・ユーザ・BAN
+## 3.4 BAN
-## 認証方式
-
-通常のログインではなく、引継ぎコードによる軽量認証である。
-
-- `users.inheritance_code` が認証トークン。
-- フロントは `localStorage.user_code` に保存。
-- API 呼び出し時、`X-Transfer-Code` ヘッダに付与。
-- バックエンドは `ApplicationController#authenticate_user` で `current_user` を設定する。
-
-## 初回利用フロー
-
-フロント起動時に次を行ふ。
-
-1. `localStorage.user_code` があれば `POST /users/verify`。
-2. 有効なら返却ユーザを採用。
-3. 無効またはコードなしなら `POST /users` で guest ユーザを作成。
-4. 新規作成された `inheritance_code` を localStorage へ保存。
-
-## 確定仕様: guest 自動作成
-
-初回閲覧で guest user を作る仕様は継続する。理由は、仕様分岐を増やさず単純に保つためである。
-
-ただし、旧本番 DB で `users = 42805` 件あるため、bot または cookie/localStorage を保持しないアクセスにより users が肥大化してゐる可能性は高い。これは仕様として許容されたわけではなく、未決定の運用リスクである。
-
-### bot / cookie なしアクセスへの推奨方針
-
-現時点では次を推奨仕様とする。
-
-| 層 | 方針 |
-| --- | --- |
-| フロント | localStorage が使へない環境では編集系 UI を出さず、`POST /users` を連打しない |
-| API | `POST /users` に IP 単位・UA 単位の軽い rate limit を置く |
-| DB | `users` に `last_seen_at`, `created_ip_address_id`, `user_agent_hash` の追加を検討する |
-| 運用 | 一定期間一度も編集/投票/コメントしてゐない guest を掃除できる Rake task を用意する |
-| bot 対策 | 明確な bot UA は user を作らず 403/204 に寄せる。検索エンジン等に guest ID を発行しない |
-
-初回閲覧 guest 自動作成を維持するなら、最低限 `POST /users` の rate limit と掃除 task は必要である。ここを放置すると、BAN・分析・バックアップが砂嵐になる。
-
-## ロール
-
-| role | 意味 |
-| --- | --- |
-| `guest` | 自動生成される通常閲覧者。閲覧、閲覧済み、上映会在席、コメント、skip 投票など軽い参加行為の主体 |
-| `member` | 投稿・タグ・Wiki・素材等の編集者 |
-| `admin` | 管理者。管理者専用調整機能や未公開ツールを利用可能 |
-
-`User#gte_member?` は `member` または `admin` を許可する。
-
-## 権限原則
-
-| 操作 | 権限 |
-| --- | --- |
-| 投稿作成/更新 | member+ |
-| タグ編集 | member+ |
-| タグ親子編集 | member+。現行の D&D/admin 限定は暫定 |
-| Wiki 作成/更新 | member+ |
-| 素材作成 | member+。URL-only guest 許可は将来検討 |
-| file 素材作成 | member+ 必須 |
-| Gekanator 調整 | admin |
-| Gekanator 公開プレイ | 将来 public 化予定 |
-| Preview API | public/guest 相当を維持する可能性あり。ただし SSRF 対策必須 |
-
-## 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 | `ip_addresses.banned_at IS NOT NULL` | 403 |
+| User | `users.banned_at IS NOT NULL` | 403 |
-IP は `IPAddr.new(request.remote_ip).hton` により binary 化して `ip_addresses.ip_address` に保存する。
+IP は binary 16 bytes で保存する。IPv4/IPv6 を同一設計で扱う。
-### 注意
+## 3.5 guest 自動作成の制御
-`users.inheritance_code` はモデル上必須・64文字以内だが、現行 schema では一意 index が見当たらない。UUID なので衝突可能性は低いが、認証トークンに DB 一意制約がないのは設計として弱い。DB 一意 index を追加すべきである。
+guest 自動作成は継続する。ただし、bot がアクセスするだけで users が増える状態は避ける。
-# エラー応答
+必須対策:
-## バリデーションエラー
+- `POST /users` に IP + UA + subnet 単位の rate limit を設ける。
+- localStorage が使えない環境では `POST /users` を連打しない。
+- bot UA には user を発行しない。
+- `users.last_seen_at`、`users.user_agent_hash`、`users.created_ip_address_id` を追加し、掃除 task を可能にする。
+- 編集・投票・コメント等の実績がない古い guest は削除または圧縮できるようにする。
-`ApplicationController#render_validation_error` 系は概ね次の JSON を返す。
-```json
-{
- "type": "validation_error",
- "message": "入力内容を確認してください.",
- "errors": {},
- "base_errors": []
-}
-```
+# 3A. ユーザ設定・テーマ仕様
-フロントは `camelcase-keys` により `baseErrors` などへ変換して扱ふ。
+設定は、DB に保存する portable settings と、端末ごとに保存する client settings に分ける。
-## 競合
+## 3A.1 DB 保存設定
-投稿更新では version based optimistic concurrency があり、競合時は 409 を返す。
+`settings` は user ごとに 1 row とする。`GET /users/settings` は存在しなければ default を作って返す。`PATCH /users/settings` は指定された editable attribute だけ更新する。
-主なフィールド:
+| 属性 | 型 | 値 | 既定 |
+| --- | --- | --- | --- |
+| `theme` | string | `system`, `light`, `dark` | `system` |
+| `auto_fetch_title` | string | `auto`, `manual`, `off` | `manual` |
+| `auto_fetch_thumbnail` | string | `auto`, `manual`, `off` | `manual` |
+| `wiki_editor_mode` | string | `split`, `write`, `preview` | `split` |
-- `error: 'conflict'`
-- `message`
-- `post_id`
-- `base_version_no`
-- `current_version_no`
-- `base`
-- `current`
-- `mine`
-- `changes`
-- `conflicts`
-- `mergeable`
+画面に露出していない typed settings も schema には残す。画面に出すかどうかと DB schema は分けて考える。
-# 投稿仕様
+## 3A.2 client settings
-## 投稿モデル
+端末依存の動作設定は `localStorage` の `btrc_hub.client_settings` に保存する。
-`posts` は外部 URL を中心とするリンク記録である。
+| 設定 | 値 | 用途 |
+| --- | --- | --- |
+| `animationMode` | `off`, `reduced`, `normal` | 画面遷移・タグ移動 animation |
+| `linkPreloadMode` | `off`, `intent` | link hover/touch 時の prefetch |
+| `tagRelationDisplay` | `flat`, `grouped` | タグ親子の表示方式 |
+| `embedAutoLoad` | `off`, `manual`, `auto` | 外部 embed の自動読込 |
+| `thumbnailMode` | 実装定義 | thumbnail 表示挙動 |
+| `activeLightThemeSlotNo` | 1, 2, 3 | light theme の使用 slot |
+| `activeDarkThemeSlotNo` | 1, 2, 3 | dark theme の使用 slot |
+| keyboard bindings | action => key | ショートカット |
+
+sidebar 幅、一覧件数、並び順など端末・画面ごとに意味が変わるものも原則 localStorage とする。
+
+## 3A.3 ユーザテーマスロット
+
+`user_theme_slots` は user ごとのテーマ token 保存枠である。
| 属性 | 仕様 |
| --- | --- |
-| `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 以上 |
+| `base_theme` | `light` または `dark` |
+| `slot_no` | 1 から 3 |
+| `tokens` | JSON object。色 token、tagColours 等 |
-## URL 正規化
+API:
-保存前に次を行ふ。
-
-- 前後空白の除去。
-- URI として parse できる場合、host を小文字化。
-- path 末尾の `/` を除去。
-- HTTP/HTTPS 以外は不正。
-
-## 元コンテンツ日時
-
-`original_created_from` と `original_created_before` の両方がある場合、`from < before` が必須である。
-
-同期系では動画公開時刻を `from = 公開時刻の秒切捨て`, `before = from + 1分` として持つ。
-
-## 投稿作成
-
-`POST /posts` は `member` 以上が必要。
-
-主要入力:
-
-| 入力 | 仕様 |
+| endpoint | 仕様 |
| --- | --- |
-| `title` | 任意 |
-| `url` | 必須 |
-| `thumbnail` | 任意。添付時は 180x180 JPEG へ変換 |
-| `thumbnail_base` | 任意 |
-| `tags` | 空白区切りタグ文字列 |
-| `parent_post_ids` | 必須。空でも送る必要あり |
-| `original_created_from` / `before` | 任意 |
+| `GET /users/theme_slots` | current user の slot を `base_theme`, `slot_no` 順で返す |
+| `PUT /users/theme_slots/:base_theme/:slot_no` | 指定 slot を upsert する |
-タグは `Tag.normalise_tags!` で正規化され、別名解決、カテゴリ prefix 解釈、タグ希望/ニジラー不詳の自動付与、親タグ展開が行はれる。
+UI 仕様:
-## 投稿更新
+- light 3 枠、dark 3 枠を持つ。
+- 選択中 slot へ上書き保存する。
+- 既定に戻す操作を持つ。
+- TopNav、背景、リンク、タグ色など主要 token を編集対象にする。
+- 未保存変更がある状態で離脱しようとした場合は確認する。
-`PUT/PATCH /posts/:id` は `member` 以上が必要。
+## 3A.4 設定画面
-現行では version based optimistic concurrency が実装されてゐる。
+設定画面は次の tab を持つ。
-| パラメータ | 仕様 |
+| tab | 内容 |
| --- | --- |
-| `base_version_no` | `force` でない限り必須。正整数 |
-| `force` | 強制上書き |
-| `merge` | 競合がなければ自動マージ |
+| アカウント | user code、role、引継ぎ情報 |
+| 動作 | animation、prefetch、embed、自動取得、親子 grouping |
+| テーマ | system/light/dark、theme slot、色 token |
+| キーボード | shortcut 表示と編集 |
-`force` と `merge` の同時指定は禁止。
+PC は左縦 tab、SP は横 tab を標準とする。
-競合判定対象:
+# 4. 公開・除票・抑止
-- `title`
-- `original_created_from`
-- `original_created_before`
-- `tag_names`
-- `parent_post_ids`
+この章が今後の製造で最も重要である。投稿やタグは、単に「存在する/消す」では足りない。外部検索、内部検索、一覧、詳細、履歴、サムネイル、sitemap、Wiki 本文への波及まで制御する必要がある。
-スカラー値は、base から current と mine が別々に変更され、かつ値が違ふ場合に競合する。集合値は、同じ要素について片方が追加し片方が削除してゐる場合に競合する。
+## 4.1 状態の分離
-`merge` 可能なら、現在状態と自分の変更を統合する。競合があれば 409。
+| 状態 | 意味 | 閲覧 | 内部検索 | 外部検索 | 履歴 | 代表用途 |
+| --- | --- | --- | --- | --- | --- | --- |
+| 通常 | 通常公開 | 可 | 出る | index 可 | 残る | 一般投稿 |
+| 索引抑止 | noindex 対象 | 可 | 原則出る | noindex | 残る | 検索流入だけ避けたい |
+| 内部検索抑止 | 広場内検索から外す | 直リンク可 | 出ない | 設定次第 | 残る | ノイズ・暫定非表示 |
+| 公開抑止 | 一般閲覧不可 | 不可 | 出ない | noindex | 残る | 権利・個人情報・問題語 |
+| 除票 | 投稿を一般面から外す | 不可 | 出ない | noindex | 残る | 誤登録・リンク先問題・削除相当 |
+| 廃止タグ | タグ新規付与不可 | タグページ可 | 設定次第 | 設定次第 | 残る | 分類体系の整理 |
-## タグ処理
+`deprecated_at` はタグ分類上の廃止であって、`noindex` ではない。ここを混ぜると仕様が壊れる。
-投稿タグは `post_tags` に保持される。
+## 4.2 投稿の除票
-- 物理削除ではなく `discarded_at` による論理削除。
-- 同一投稿・同一タグの active 重複は禁止。
-- `PostTag#destroy` は読み取り専用例外を投げる。
-- `discard_by!` により `discarded_at` と `deleted_user` を設定し、`tags.post_count` を減算する。
+投稿は物理削除しない。投稿除票は次を意味する。
-投稿に手動で `nico:` タグを入れることは通常禁止される。Nico タグは同期/連携経由で扱ふ。
+- 一般の投稿一覧から除外する。
+- 投稿検索から除外する。
+- 通常フロントの `GET /posts/:id` は role にかかわらず 404 を返す。
+- admin フロントでも除票済み投稿の通常詳細は表示しない。削除と同等に扱う。
+- sitemap から除外する。
+- HTML 詳細ページを返す場合は必ず `noindex` を付ける。
+- 類似度計算、上映会抽選、Gekanator 候補から除外する。
+- `post_versions` には `discard` / `restore` を記録する。
+- `post_tags` は削除しない。復帰時の再構築と監査のため保持する。
-## 親子投稿
+推奨カラム:
-`post_implications` は投稿間の多対多親子関係である。
+| テーブル | カラム | 内容 |
+| --- | --- | --- |
+| `posts` | `discarded_at` | 除票日時 |
+| `posts` | `discarded_by_user_id` | 除票者 |
+| `posts` | `discard_reason` | 管理用理由 |
+| `posts` | `restored_at` | 任意。最後の復帰日時 |
+| `post_versions` | `event_type = discard/restore` | 履歴 |
-| カラム | 意味 |
+public 応答は 410 Gone が意味としては正しい。ただし、URL 存在推測を避けたい場合は 404 に寄せる。既定は **404** とする。admin フロントでも通常詳細は非表示にし、監査・復帰が必要な場合だけ専用の管理導線で扱う。
+
+## 4.3 索引抑止
+
+索引抑止は「ページは見られるが、外部検索に載せない」ための状態である。
+
+必須動作:
+
+- HTML には `` を出す。
+- sitemap から除外する。
+- 必要に応じて HTTP header `X-Robots-Tag: noindex` を出す。
+- `robots.txt` の `Disallow` で塞ぐことは原則禁止する。crawler がページを読めないと noindex を認識できないためである。
+- route 遷移後も React Helmet で robots meta を更新する。
+- noindex 判定は API の payload に含め、frontend だけの推測にしない。
+
+`nofollow` は既定では付けない。リンク先への評価伝搬を止めたい場合だけ個別指定する。
+
+## 4.4 索引抑止の対象
+
+| 対象 | noindex 対象ページ |
| --- | --- |
-| `post_id` | 子投稿 |
-| `parent_post_id` | 親投稿 |
+| 投稿 | `/posts/:id`、投稿を含む静的 detail route |
+| タグ | `/tags/:id`、`/tags/name/:name`、関連する Wiki、素材、タグ検索結果 |
+| Wiki | `/wiki/:title`、diff、history |
+| 素材 | `/materials/:id`、タグ素材一覧 |
+| route | `/theatres/:id`、settings、エラー画面等 |
+| phrase | 本文・タイトル・タグリスト等に指定語が現れる任意ページ |
-制約:
+## 4.5 タグベース索引抑止
-- 複合主キー。
-- 自己親は禁止。
-- 存在しない親 ID はエラー。
+タグに索引抑止を設定した場合、次を noindex とする。
-フロントの PostList は親子投稿の存在に応じてカード枠を変へる。
+1. 当該タグの詳細ページ。
+2. 当該タグに対応する Wiki ページ。
+3. 当該タグに対応する素材ページまたは素材一覧。
+4. 当該タグが直接付与された投稿。
+5. 親タグ展開により当該タグを含むとみなされる投稿。
+6. 当該タグ名または別名がページの文章中に現れる Wiki ページ。
+7. 当該タグ名または別名がタイトル、説明、タグチップ、親子タグ表示、関連タグ表示に現れるページ。
-## 関連投稿
+この仕様は、単に投稿タグを見るだけでは不足する。ページ本文に文字列として現れる場合も検索エンジンには拾われるためである。
-`post_similarities` は、投稿同士の類似度を保持する。
+## 4.6 タグ名・別名・本文一致ルール
-- `Similarity::Calc.call(Post, :tags)` により生成。
-- 各投稿につき上位 20 件を保存。
-- 類似度はタグ集合の cosine similarity。
-- `Post#related(limit:)` は `cos DESC` で取得する。
+索引抑止の文字列判定は次を使う。
-## 投稿一覧検索
+- tag_name の canonical name。
+- aliases。
+- tag_name_sanitisation_rules 適用後の同定形。
+- Unicode 正規化後の表記。
+- 完全一致を既定とする。
+- 部分一致は phrase rule として明示された場合だけ行う。
-`GET /posts` は次の絞り込みを持つ。
+禁止:
-| パラメータ | 仕様 |
-| --- | --- |
-| `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 へ補正 |
+- 何でも substring match にすること。誤爆が増えすぎる。
+- HTML 文字列へ正規表現を直接かけること。Markdown/HTML/React 表示単位の token 化後に判定する。
-`updated_at` sort は、投稿本体の `updated_at` と `post_tags.updated_at` の最大値を使ふ。
+## 4.7 IndexPolicyResolver
+索引抑止は各画面が勝手に判断しない。backend に `IndexPolicyResolver` を置き、API payload と sitemap 生成で共通利用する。
-## 投稿履歴
+入力例:
-`post_versions` は immutable snapshot である。
-
-| 属性 | 内容 |
-| --- | --- |
-| `post_id` | 対象投稿 |
-| `version_no` | 投稿内連番 |
-| `event_type` | `create`, `update`, `discard`, `restore` |
-| `title`, `url`, `thumbnail_base` | 投稿本体 |
-| `tags` | タグ名の空白区切り snapshot。将来 `tags_json` へ移行予定 |
-| `parent_post_ids` | 親投稿 ID の空白区切り snapshot |
-| `original_created_from/before` | 元日時範囲 |
-| `created_by_user_id` | 操作者 |
-
-`VersionRecorder` は次を保証する。
-
-- 永続化済み version は readonly。
-- 初回イベントは `create` でなければならない。
-- 同一 snapshot の update は版を増やさない。
-- 作成後、対象 record の `version_no` を更新する。
-
-## 確定仕様: 親投稿変更履歴
-
-投稿履歴画面では、親投稿の追加・削除を必ず表示する。現行 API が返してゐないのはバグである。
-
-`GET /posts/versions` は各 version に少なくとも次を返すべきである。
-
-```ts
-parentPosts: Array<{
- id: number
- title: string | null
- historicalTitle?: string | null
-}>
+```txt
+route: posts/show
+subject: post_id = 123
+rendered_text_sources:
+ - title
+ - tag_names
+ - parent_post_titles
+ - remarks
+ - wiki_excerpt
```
-API の snake_case 原型は次を想定する。
+出力例:
```json
{
- "parent_posts": [
- {
- "id": 123,
- "title": "現在のタイトル",
- "historical_title": "履歴作成当時のタイトル"
- }
+ "robots": "noindex",
+ "sitemap": false,
+ "reasons": [
+ { "type": "tag", "id": 456, "name": "センシティブタグ" },
+ { "type": "phrase", "phrase": "センシティブ語" }
+ ],
+ "internal_search_hidden": false,
+ "public_hidden": false
+}
+```
+
+frontend はこの結果を受け取り、Helmet に反映する。frontend 側だけで推測しない。
+
+## 4.8 推奨データ構造
+
+抑止は今後増えるため、単純な boolean 列だけでなく policy table を持つ。
+
+```txt
+index_policies
+- id
+- target_type post / tag / wiki_page / material / route / phrase
+- target_id nullable
+- phrase nullable
+- noindex boolean
+- nofollow boolean
+- sitemap_excluded boolean
+- internal_search_hidden boolean
+- public_hidden boolean
+- propagation json
+- reason text
+- created_by_user_id
+- created_at
+- updated_at
+- discarded_at
+```
+
+頻繁に絞り込む対象には materialized column を持ってよい。
+
+```txt
+posts.index_suppressed_at
+tags.index_suppressed_at
+wiki_pages.index_suppressed_at
+materials.index_suppressed_at
+```
+
+ただし、materialized column は cache であり、真の根拠は `index_policies` とする。
+
+## 4.9 公開抑止
+
+公開抑止は、一般ユーザに内容を見せない状態である。索引抑止より強い。
+
+- guest/member の詳細 API は 404 を返す。
+- admin は理由付きで閲覧できる。
+- 一覧・内部検索・Gekanator・上映会・類似度から除外する。
+- sitemap から除外する。
+- HTML を返す場合は noindex。
+- 履歴は admin のみ参照可能。
+
+公開抑止は法務・個人情報・権利・荒らし対応に使う。単なる検索流入避けに使うとデータが死ぬので、通常は索引抑止を使う。
+
+## 4.10 内部検索抑止
+
+内部検索抑止は、広場内の通常検索・一覧から外すが、直リンク閲覧は許可する状態である。
+
+用途:
+
+- 重複候補。
+- bot 操作タグ付きで未整理の投稿。
+- 一時的に検索ノイズとなる同期投稿。
+- 人手確認待ち。
+
+内部検索抑止の対象は、投稿、タグ、Wiki、素材ごとに個別に持つ。
+
+# 5. 投稿仕様
+
+## 5.1 投稿の基本属性
+
+投稿は外部 URL を中心に保持するリンクレコードである。
+
+| 属性 | 仕様 |
+| --- | --- |
+| `url` | 必須。一意。HTTP/HTTPS のみ |
+| `title` | 任意。外部タイトルまたは手動タイトル |
+| `thumbnail_base` | 外部サムネイル URL。後から変更可能 |
+| `thumbnail` | Active Storage 添付。手動アップロード可 |
+| `uploaded_user_id` | 作成者。同期投稿では bot/admin を使うか null 許容 |
+| `original_created_from` | 元コンテンツ作成日時の下限 |
+| `original_created_before` | 元コンテンツ作成日時の上限。`from < before` 必須 |
+| `version_no` | 投稿内版番号。1 以上 |
+| `video_ms` | 動画長。ミリ秒単位。`動画` タグを持つ投稿で意味を持つ |
+| `discarded_at` | 除票日時 |
+| `index_suppressed_at` | 索引抑止 cache |
+
+## 5.2 URL 正規化
+
+保存前に次を行う。
+
+- 前後空白を除去する。
+- scheme は `http` / `https` のみ許可する。
+- host は小文字化する。
+- path 末尾の `/` は原則除去する。
+- URL 全体は DB index 長に収まるよう制限する。
+- 同一動画の URL 揺れは platform normalizer で正規化する。
+
+YouTube と Nico は専用 normalizer を持つ。
+
+## 5.3 作成
+
+`POST /posts` は member 以上が実行できる。
+
+入力:
+
+| 入力 | 必須 | 説明 |
+| --- | --- | --- |
+| `url` | 必須 | 外部 URL |
+| `title` | 任意 | タイトル |
+| `thumbnail_base` | 任意 | 外部サムネイル URL |
+| `thumbnail` | 任意 | 添付サムネイル |
+| `tags` | 必須 | 空白区切り。空の場合も文字列として扱う |
+| `parent_post_ids` | 必須 | 空配列可 |
+| `original_created_from` | 任意 | 元作成日時下限 |
+| `original_created_before` | 任意 | 元作成日時上限 |
+| `video_ms` / `duration` | 任意 | 動画長。`video_ms` はミリ秒、`duration` は時刻文字列 |
+
+作成時は `post_versions` に `create` snapshot を記録する。
+
+## 5.4 更新と競合制御
+
+`PUT/PATCH /posts/:id` は member 以上が実行できる。
+
+- `base_version_no` を要求する。
+- 現在版と異なる場合は 409 conflict を返す。
+- `force` は admin または明示権限者のみ許可する。
+- `merge` は競合のない集合差分のみ自動統合する。
+- `force` と `merge` の同時指定は禁止する。
+
+競合判定対象:
+
+- title
+- thumbnail_base
+- original_created_from
+- original_created_before
+- tag_names
+- parent_post_ids
+- video_ms
+- local tag sections
+- remarks
+
+## 5.5 タグ付与
+
+投稿タグは `post_tags` で保持する。
+
+仕様:
+
+- 論理削除で履歴を残す。
+- 同一投稿・同一タグの active 重複は禁止する。
+- タグ名は別名解決・カテゴリ prefix 解釈・sanitisation を通す。
+- nico タグは通常手入力禁止。
+- 廃止タグは新規付与禁止。
+- 親タグは再帰的に展開する。
+- 展開後に廃止タグは除外する。
+
+## 5.6 局所記載
+
+局所記載は、投稿全体ではなく動画・音声・長尺コンテンツの一部にタグを付ける機能である。投稿全体タグと同じタグ体系を使うが、範囲情報を `post_tag_sections` に分離して保持する。
+
+### 5.6.1 動画長
+
+`posts.video_ms` は動画長をミリ秒で保持する。`post_versions.video_ms` にも snapshot する。
+
+- `video_ms` は正の整数または `NULL` とする。
+- UI では `動画` タグが付いた場合に動画時間欄を表示する。
+- `動画` タグを削除して再入力した場合でも、編集中の `video_ms` は可能な限り保持する。
+- API は `video_ms` の直接指定と、`duration` 文字列指定の双方を受け付ける。
+- `video_ms` は optimistic concurrency の競合判定対象である。
+- `動画` タグを持たない投稿では UI に動画時間欄を表示しない。
+- `動画` タグを持たない投稿に `video_ms` / `duration` が渡された場合、API は DB に保存しない。
+- 動画属性は将来的に `video_posts` 等の別テーブルへ切り出す余地がある。
+
+### 5.6.2 時刻文字列
+
+局所範囲と `duration` は次の形式を受け付ける。
+
+```txt
+12
+12.345
+1:23
+1:23.456
+1:02:03
+1:02:03.456
+```
+
+- 単独数値は秒として解釈する。
+- `分:秒`、`時:分:秒` を許可する。
+- 秒・分の範囲は 0 から 59 を原則とする。
+- 内部保存はミリ秒に正規化する。
+
+### 5.6.3 タグ記法
+
+記法:
+
+```txt
+タグ名[開始-終了]
+タグ名[開始-]
+タグ名[-終了]
+タグ名[-]
+タグ名[0:00-0:12]
+タグ名[1:23-2:34]
+```
+
+解釈:
+
+- `タグ名[-]` は局所記載なしとして `タグ名` と等価に扱う。
+- `タグ名[0-]` や `タグ名[0:00-]` は全範囲指定であり、通常タグと等価に扱う。
+- 開始省略は `0` とみなす。
+- 終了省略は `NULL` とし、動画末尾までを表す。
+- 開始と終了が逆の場合は並べ替える。
+- 開始と終了が同値の場合は、最小 1 ms の範囲として扱う。
+- 同一タグの重複・隣接・包含する section は merge する。
+- section が全範囲に merge された場合、その tag の section は保存せず通常タグとして扱う。
+
+### 5.6.4 DB 形状
+
+`post_tag_sections` は次の複合主キーで保持する。
+
+```txt
+post_tag_sections
+- post_id
+- tag_id
+- begin_ms
+- end_ms
+- created_at
+- updated_at
+primary key: post_id, tag_id, begin_ms
+check: begin_ms >= 0
+check: end_ms is null or begin_ms < end_ms
+```
+
+現行実装は投稿タグ同期時に対象投稿の section を削除して再作成する。したがって、section 単体の soft delete や作成者履歴は持たない。監査が必要な場合は `post_versions.tags` または将来の `post_versions.tags_json` により復元する。
+
+### 5.6.5 整合性
+
+- `video_ms` がある場合、`begin_ms < video_ms` を必須とする。
+- `end_ms` がある場合、`end_ms <= video_ms` を必須とする。
+- `video_ms` が未設定の場合、section の上限検査は行わない。
+- 局所タグの親タグ展開は、同一 section に対して行う。
+- 投稿表示では、通常タグと局所タグを視覚的に混ぜない。混ぜると「動画全体に出る」のか「数秒だけ出る」のかが壊れる。
+
+### 5.6.6 履歴表示
+
+投稿履歴では、タグ名だけでなく局所範囲も表示対象にする。
+
+- 旧: `伊地知ニジカ`
+- 新: `伊地知ニジカ[0:12-0:18]`
+
+`post_versions.tags_json` を導入する場合、局所範囲は文字列ではなく次の構造で保持する。
+
+```json
+{
+ "tag_id": 123,
+ "name": "伊地知ニジカ",
+ "sections": [
+ { "begin_ms": 12000, "end_ms": 18000 }
]
}
```
-表示仕様:
+## 5.7 親子投稿
-- 通常表示は現在 title を使ふ。
-- 当時 title が現在 title と異なる場合、`(当時タイトル:{historical_title})` を付記する。
-- 対象投稿が削除済みまたは参照不能なら、`#123(現在参照不可)` のやぅに ID を残す。
+`post_implications` は投稿間の親子関係を表す。
-タグ履歴についても同様に、現在名だけでなく当時の名前・カテゴリを表示できる形が望ましい。添付 issue #354 により、`post_versions.tags_json` を作り、旧 `post_versions.tags` を廃止する方向が課題化されてゐる。
+- 子 `post_id` から親 `parent_post_id` への関係。
+- 自己親は禁止。
+- 存在しない親は禁止。
+- 可能なら循環も禁止する。
+- 投稿履歴には親投稿変更を必ず記録・表示する。
-### 実装差分
+表示:
-フロント `PostVersion` 型には `parentPosts` がある。しかし現行 `PostVersionsController#index` は `parent_post_ids` を select/serialize しておらず、`parentPosts` を返してゐない。これは仕様決定済みの実装バグである。
+- 子投稿は親投稿を参照表示する。
+- 親投稿のタイトルは現在タイトルを表示する。
+- 履歴画面では当時タイトルも表示できるようにする。
-# タグ仕様
+## 5.8 投稿検索
-## タグ名とタグ実体
+`GET /posts` は次の条件を持つ。
-タグは `tag_names` と `tags` に分離される。
+| 条件 | 仕様 |
+| --- | --- |
+| `url` | 部分一致 |
+| `title` | 部分一致 |
+| `tags` | 空白区切り。別名解決あり |
+| `match` | `all` / `any` |
+| `not:` | 除外タグ |
+| `original_created_from/to` | 元作成日時 |
+| `created_from/to` | 作成日時 |
+| `updated_from/to` | 更新日時。タグ更新も考慮 |
+| `visibility` | 通常/索引抑止/内部検索抑止/公開抑止。除票は通常検索対象外 |
+| `order` | title/url/original_created_at/created_at/updated_at |
+| `page`, `limit` | ページネーション。無限ローディングに依存しない |
+
+通常検索は除票・公開抑止・内部検索抑止を除外する。索引抑止・内部検索抑止・公開抑止は admin の明示 filter で確認できるが、除票済み投稿は通常検索には role にかかわらず出さない。
+
+## 5.9 投稿履歴
+
+`post_versions` は immutable snapshot である。
+
+必須 snapshot:
+
+- title
+- url
+- thumbnail_base
+- tags_json
+- parent_posts_json
+- local_sections_json
+- remarks_json
+- original_created_from
+- original_created_before
+- visibility state
+- created_by_user_id
+
+`post_versions.tags` の空白区切り形式は将来的に `tags_json` へ移行する。タグ名・カテゴリ・当時名・現在 ID を持てる構造にする。
+
+# 6. タグ仕様
+
+## 6.1 タグ構造
+
+タグは `tag_names` と `tags` に分離する。
| テーブル | 役割 |
| --- | --- |
-| `tag_names` | 名前文字列、canonical/alias 関係、Wiki ページとの結合点 |
-| `tags` | カテゴリ、投稿件数、実体 ID、version_no |
+| `tag_names` | 名前文字列、canonical/alias、Wiki との結合点 |
+| `tags` | カテゴリ、投稿件数、廃止、除票、索引抑止、version |
-`Tag#name` は `tag_name.name` の delegate である。
+`tag_names.name` は全体で一意とする。
-## タグカテゴリ
+## 6.2 カテゴリ
-| category | 表示/用途 |
+| category | 意味 |
| --- | --- |
| `deerjikist` | ニジラー |
-| `meme` | 原作・ネタ元・ミーム等 |
+| `meme` | 原作・ネタ元・ミーム |
| `character` | キャラクター |
| `general` | 一般 |
-| `material` | 素材 |
+| `material` | 素材タグ |
| `meta` | メタタグ |
-| `nico` | ニコニコタグ |
+| `nico` | ニコニコ外部タグ |
-`nico` タグは名前が必ず `nico:` で始まる必要があり、非 nico タグが `nico:` で始まることも禁止される。
+`nico` は外部情報なので、内部タグと同じ廃止・除票運用に混ぜない。
-## システムタグ
+## 6.3 システムタグ
-`Tag` には次の作成ヘルパがある。
+| タグ | 用途 |
+| --- | --- |
+| `タグ希望` | タグ不足 |
+| `bot操作` | bot/sync 由来 |
+| `ニジラー情報不詳` | 投稿者/作者情報不足 |
+| `動画` | 動画投稿 |
+| `ニコニコ` | ニコニコ由来 |
+| `YouTube` | YouTube 由来 |
-| メソッド | タグ名 | 用途 |
-| --- | --- | --- |
-| `Tag.tagme` | `タグ希望` | タグ不足の印 |
-| `Tag.bot` | `bot操作` | bot/sync 由来の印 |
-| `Tag.no_deerjikist` | `ニジラー情報不詳` | ニジラー不明 |
-| `Tag.video` | `動画` | 動画投稿 |
-| `Tag.niconico` | `ニコニコ` | ニコニコ由来 |
-| `Tag.youtube` | `YouTube` | YouTube 由来 |
+システムタグの名称変更は禁止する。別名追加は可能だが、canonical name は固定する。
-`タグ希望`, `bot操作`, `ニジラー情報不詳`, `動画`, `ニコニコ` は名称変更が明示的に禁止される。
+## 6.4 正規化
-## タグ正規化
+タグ入力は次を通す。
-`Tag.normalise_tags!` の仕様:
+1. 空白・空文字除去。
+2. Unicode 正規化。
+3. `tag_name_sanitisation_rules` 適用。
+4. prefix 解析。
+5. 別名解決。
+6. 存在しない tag_name/tag の作成。
+7. 廃止タグ拒否。
+8. nico 手入力拒否。
+9. `タグ希望` / `ニジラー情報不詳` の自動付与。
+10. 親タグ展開。
-- 入力はタグ名配列。
-- 空白・空文字を除去。
-- カテゴリ prefix を解釈する。
-- `TagName.canonicalise` により別名を正規名へ解決する。
-- 存在しない tag_name/tag は作成する。
-- `with_tagme` が true かつタグ数が 10 未満で `タグ希望` がなければ追加。
-- `with_no_deerjikist` が true かつ deerjikist タグがなければ `ニジラー情報不詳` を追加。
-- `deny_nico` が true なら `nico:` prefix はエラー。
-
-対応 prefix:
+prefix:
| prefix | category |
| --- | --- |
@@ -570,1140 +835,1177 @@ API の snake_case 原型は次を想定する。
| `material:`, `mtr:` | material |
| `meta:` | meta |
-## 別名
+## 6.5 別名
-`tag_names.canonical_id` により別名を表現する。
+- alias は `tag_names.canonical_id` で表す。
+- alias の参照先は canonical tag_name でなければならない。
+- alias 名に `:` は含めない。
+- タグ実体または Wiki を持つ tag_name は alias 化できない。
+- タグ名変更時、旧名は alias として残す。
-- `canonical_id = NULL`: 正規名。
-- `canonical_id != NULL`: 別名。
-- 別名の参照先は正規名でなければならない。
-- 別名名に `:` は含められない。
-- タグまたは Wiki ページを持つ tag_name は別名化できない。
+## 6.6 親子タグ
-タグ詳細の full update では、名称変更時に旧名が aliases に追加される。
+`tag_implications` は子タグから親タグへの関係である。
+仕様:
-## タグ親子
-
-`tag_implications` は子タグから親タグへの関係を表す。
-
-| カラム | 意味 |
-| --- | --- |
-| `tag_id` | 子タグ |
-| `parent_tag_id` | 親タグ |
-
-- 同一組合せは一意。
+- member 以上が編集可能。
+- UI では guest にも親タグ欄を表示するが編集不可。
- 自己親は禁止。
-- 投稿側では、付与タグの親タグが再帰的に展開される。
+- 循環は禁止。
+- 親タグ展開は再帰的に行う。
+- 廃止タグが中間に挟まる場合、表示上は貫通する。
-## 確定仕様: タグ親子編集権限
+例:
-タグ親子関係の編集は **member 以上** が可能である。
+```txt
+A -> B -> C
+B が廃止済み
+表示: A -> C
+```
-UI 仕様:
+## 6.7 タグ廃止
-| ユーザ | 親タグ欄の表示 | 変更 |
-| --- | --- | --- |
-| guest | 表示する | 不可 |
-| member | 表示する | 可 |
-| admin | 表示する | 可 |
+タグ廃止は `tags.deprecated_at` で表す。
-現行の D&D 操作が admin 限定だった理由は、履歴管理がなかった時期に誤操作を避けるための安全策である。現在は履歴管理があるため、admin 限定を続ける必然性は薄い。
+- 廃止タグは新規投稿に付与できない。
+- 既存投稿から即時剥がさない。
+- 投稿表示では廃止タグを通常タグ一覧から除外する。
+- 投稿検索では、廃止タグを理由に既存投稿を除外しない。検索条件として廃止タグを指定した場合も、既存投稿との対応は保持する。
+- タグ検索では廃止状態で絞り込める。
+- タグ履歴には状態差分を表示する。
+- Gekanator のタグ質問・特徴量には廃止タグを使わない。廃止タグを含むだけで投稿候補そのものを除外しない。
+- 類似度計算では廃止タグを除外する。
+- `nico` タグは廃止不可。
-### 実装差分
+廃止と索引抑止は別である。廃止しても noindex にはならない。noindex が必要な場合は別途 index policy を設定する。
-`TagChildrenController` の親子追加/削除 API は admin 限定である。一方、`TagsController#update_all` は member 以上で `parent_tags` を更新できる。正しい仕様は member+ なので、専用 API 側の権限を揃へる必要がある。
+### 6.7.1 API/UI 反映
-### 追加制約
+差替後の資材では、タグ廃止は単なる DB カラムではなく API と画面に反映する対象である。
-添付 issue #332 により、上位タグの循環登録禁止が P1 バグとして課題化されてゐる。タグ親子編集を member+ に開くなら、循環検出は必須である。
-
-## タグ一覧/検索
-
-`GET /tags` は次を持つ。
-
-| パラメータ | 仕様 |
+| 場所 | 仕様 |
| --- | --- |
-| `post` | 投稿 ID で絞り込み |
-| `name` | 名前部分一致 |
-| `category` | カテゴリ |
-| `post_count_gte/lte` | 投稿件数範囲 |
-| `created_from/to` | 作成日時 |
-| `updated_from/to` | 更新日時 |
-| `order` | `name`, `category`, `post_count`, `created_at`, `updated_at` |
+| `GET /tags` | `deprecated=1` / `deprecated=0` による絞り込みを持つ |
+| `GET /tags/autocomplete` | 廃止タグを候補から除外する |
+| `PUT /tags/:id` | full update では `deprecated` を必須入力として扱う |
+| `PATCH /tags/:id` | `deprecated` が指定された場合だけ廃止状態を更新する |
+| 投稿作成/更新 | `deny_deprecated: true` により廃止タグの新規付与を拒否する |
+| 投稿表示 | 廃止タグを通常タグ一覧から除外する |
+| Wiki 表示 | 対応タグが廃止済みの場合、タイトル付近に `(廃止)` を表示する |
+| Gekanator | tag question と特徴量から廃止タグを除外する。廃止タグを含むだけで投稿候補そのものは除外しない |
+| 素材一覧 | グルーピングやタグ候補で廃止タグを通常候補にしない |
-カテゴリ順は独自順で、`deerjikist`, `meme`, `character`, `general`, `material`, `meta`, `nico` の順を使ふ。
+Nico タグは外部情報の mirror であるため、廃止は禁止する。`tags.category = nico` かつ `deprecated_at IS NOT NULL` は DB check constraint でも拒否する。
-## タグ autocomplete
-`GET /tags/autocomplete`:
+# 7. Nico タグ・外部連携
-| パラメータ | 仕様 |
-| --- | --- |
-| `q` | 検索語 |
-| `nico` | nico タグを含めるか。既定 true |
-| `present` | 投稿件数 > 0 のものに絞るか。既定 true |
+## 7.1 Nico タグ
-別名 hit も見て、`matched_alias` を返す。
+Nico タグは `tags.category = 'nico'` とし、名前は必ず `nico:` で始める。
-## タグ履歴
+- 通常入力からは拒否する。
+- Nico 同期または管理 UI でのみ作成する。
+- nico タグ同士の連携は禁止する。
+- nico タグは外部情報であり廃止不可。
+- `nico_tag_versions` には連携状態を記録する。
-`tag_versions` は immutable snapshot。
+## 7.2 Nico タグ連携
-| 属性 | 内容 |
-| --- | --- |
-| `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` | 操作者 |
+`nico_tag_relations` は nico タグと内部タグを結びつける。
-`TagVersioning` は更新前 snapshot と更新後記録を制御する。
+- member 以上が編集可能。
+- 連携先は非 nico タグのみ。
+- 連携先タグの廃止状態を見て警告する。
+- 連携変更は nico_tag_versions と tag_versions の双方に必要な履歴を残す。
-# Nico タグ仕様
+## 7.3 連携抑止
-## 概要
+外部タグから内部タグへの自動付与を止めたい場合、連携抑止ルールを持つ。
-ニコニコ由来のタグは内部タグとは別性質である。
+用途:
-- `tags.category = 'nico'`
-- `tag_names.name` は必ず `nico:` で始まる。
-- 通常の手動入力では `nico:` は拒否される。
-- 内部タグとの連携は `nico_tag_relations` で行ふ。
+- 外部タグが荒れている。
+- 同名だが意味が違う。
+- 特定投稿だけ連携したくない。
+- 権利・検索流入上の懸念がある。
-## Nico タグ連携
+推奨構造:
-`nico_tag_relations`:
+```txt
+external_tag_suppression_rules
+- id
+- platform nico / youtube / pixiv / etc
+- external_key
+- target_type tag / post / source / global
+- target_id
+- reason
+- created_by_user_id
+- discarded_at
+```
-| カラム | 意味 |
-| --- | --- |
-| `nico_tag_id` | nico カテゴリタグ |
-| `tag_id` | 内部タグ |
+# 8. Wiki 仕様
-`NicoTagsController#update` は member 以上が利用可能。
+## 8.1 基本
-- 対象 tag は nico でなければならない。
-- 連携先は通常タグで、nico から nico への連携は禁止。
-- 連携先タグも version snapshot される。
-- `nico_tag_versions` へ連携状態が記録される。
+Wiki ページは `tag_names` に一対一で紐づく。
-## Nico 同期
+- タイトルは `tag_name.name`。
+- タグ実体がなくても Wiki は存在し得る。
+- タグが廃止されている場合、Wiki 表示上も廃止状態を示す。
+- Wiki 本文が空の場合は noindex とする。
-`backend/lib/tasks/sync_nico.rake` は外部 `NIZIKA_NICO_PATH` の Python スクリプトから動画情報を取得する。
-
-処理概要:
-
-1. 動画 code/title/uploaded_at/tags を取得。
-2. 既存投稿を `nicovideo.jp/watch/` で検索。
-3. なければ投稿作成。
-4. サムネイルを HTML meta から取得して Active Storage 添付。
-5. `タグ希望`, `bot操作`, `ニコニコ`, `動画` を付与。
-6. ニコニコの生タグを `nico:` として `nico` タグ化。
-7. Nico タグに連携された内部タグも付与。
-8. 投稿版/Nico タグ版を記録。
-
-## Nico 逆連携
-
-`backend/lib/tasks/export_nico.rake` は、タグ広場上のニコニコ動画 ID を外部 `tracked_videos.put_bulk_upsert` へ渡す。
-
-# YouTube 同期仕様
-
-`backend/lib/tasks/sync_posts.rake` は `Youtube::Sync.new.sync!` を呼ぶ。
-
-## 検出対象
-
-- 検索語:
- - `ぼざろクリーチャーシリーズ`
- - `伊地知ニジカ`
- - `伊地知虹鹿`
-- プレイリスト ID 3 件。
-- 検索対象は直近 14 日。
-
-## 同期処理
-
-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
-
-`deerjikists` は外部プラットフォーム上のユーザ/チャンネルとタグを紐づける。
-
-| カラム | 意味 |
-| --- | --- |
-| `platform` | `nico` または `youtube` |
-| `code` | 外部 ID |
-| `tag_id` | deerjikist カテゴリのタグ |
-
-複合主キーは `(platform, code)`。
-
-`Deerjikist` モデルは紐づけ先タグが `deerjikist` カテゴリであることを要求する。
-
-## API
-
-| API | 権限 | 内容 |
-| --- | --- | --- |
-| `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 へ正規化する処理がある。
-
-### 注意
-
-YouTube handle 正規化は外部 HTML 取得に依存してゐる。失敗時は `nil` になり得るため、UI 側のエラー誘導が重要。
-
-# Wiki 仕様
-
-## Wiki ページとタグ名
-
-`wiki_pages` は `tag_name_id` と一対一に紐づく。
-
-- Wiki のタイトルは `tag_name.name`。
-- タグ実体ではなく tag_name に紐づくため、タグがなくても Wiki ページは存在し得る。
-- タイトル変更時は tag_name を変更する。
-
-## 現行ストレージ
-
-Wiki は本文を `wiki_pages.body` に持つだけではない。現行は行単位の改訂履歴を持つ。
-
-| テーブル | 役割 |
-| --- | --- |
-| `wiki_pages` | ページ本体/現在情報 |
-| `wiki_revisions` | 改訂ヘッダ。kind/content/redirect、tree_sha 等 |
-| `wiki_lines` | 行本文を SHA256 で重複排除 |
-| `wiki_revision_lines` | 改訂と行の順序 |
-| `wiki_versions` | ページ title/body の version snapshot |
-
-## 作成/更新
+## 8.2 作成・更新
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。
+- CRLF を LF へ統一する。
+- 不正 UTF-8 は置換して保存する。
+- 行単位で SHA256 を計算し、`wiki_lines` に dedup 保存する。
+- `wiki_revision_lines` に順序を保存する。
+- `wiki_versions` に title/body snapshot を保存する。
+- `base_revision_id` が現在 revision と違う場合は conflict とする。
-## 差分
+## 8.3 本文検索
-`GET /wiki/:id/diff` は `diff-lcs` による行差分を返す。
+Wiki 検索は title だけでなく body も検索対象にする。
-返却 type:
+必須:
-- `context`
-- `added`
-- `removed`
+- title 完全一致を最優先。
+- title 前方一致。
+- title 部分一致。
+- body 部分一致。
+- 廃止タグ Wiki は検索結果に表示するが状態表示する。
+- index policy により内部検索抑止されている Wiki は通常検索から除外する。
-content revision 同士のみ差分対象。
+## 8.4 自動リンク
-## redirect
+Wiki 本文中のタグ名は自動リンクできる。ただし次は禁止する。
-`Wiki::Commit.redirect!` は現行では `raise '廃止しました.'` で無効化されてゐる。ただし schema と controller には redirect revision の痕跡がある。
+- Markdown のリンク先 URL 部分を勝手に autolink すること。
+- code span / code block 内を autolink すること。
+- 既存リンクの label 内を破壊すること。
+## 8.5 Wiki asset
-## Wiki asset
+Wiki には画像・添付 asset を持てる。
-`wiki_assets` は Wiki 内画像/添付ファイルのための将来機能である。
+仕様:
-確定仕様:
+- `wiki_pages.next_asset_no` によりページ内番号を採番する。
+- `wiki_assets.sha256` は添付バイト列の SHA256。
+- 同一ページ内の同一 SHA256 重複は禁止する。
+- Active Storage に実体を保存する。
+- alt_text を必須または強く推奨する。
+- asset 追加も revision/version に残す。
+- noindex 対象 Wiki の asset には `X-Robots-Tag: noindex` を返す。
-- Wiki ページ内に画像または添付ファイルを持たせる。
-- `wiki_pages.next_asset_no` と連動し、ページ内 asset 番号を採番する。
-- `wiki_assets.sha256` は添付バイト列を SHA256 に通した値を保持する。
-- 同一バイト列の重複検出・再利用・整合性確認に `sha256` を使ふ。
-- 本体保存は Active Storage を前提にする。
+# 9. 素材仕様
-推奨カラム意味:
+## 9.1 基本
-| カラム | 意味 |
-| --- | --- |
-| `wiki_page_id` | 所属 Wiki ページ |
-| `no` | ページ内 asset 番号 |
-| `sha256` | 添付バイト列の SHA256 |
-| `filename` | 表示/ダウンロード用ファイル名 |
-| `content_type` | MIME type |
-| `byte_size` | サイズ |
-| `created_by_user_id` | 追加者 |
-
-現行ソースでは明確な model/controller/画面導線は確認できない。したがって、schema は将来機能として正当化されたが、実装は未完である。
-
-
-# 素材仕様
-
-## Material
-
-`materials` はタグに紐づく素材である。
+素材は tag に紐づく URL または file である。標準素材管理基盤として、単なる添付ファイルではなく、同期元、同期抑止、履歴、export path、ZIP 配布、thumbnail 生成を含めて扱う。
| 属性 | 仕様 |
| --- | --- |
-| `tag_id` | 必須・一意 |
+| `tag_id` | 任意。ただし手動作成では必須。`character` または `material` のタグだけ許可 |
| `url` | 任意。file がなければ必須 |
-| `file` | Active Storage 添付。url がなければ必須 |
-| `parent_id` | 親素材。任意だが廃止予定 |
+| `file` | 任意。url がなければ必須。Active Storage 添付 |
+| `thumbnail` | 任意。画像・動画素材から生成した 180px 級 thumbnail |
+| `version_no` | 素材内版番号。1 以上 |
+| `source_kind` | `uri`, `google_drive_path`, `google_drive_file`, `legacy_drive_path` |
+| `source_uri` | URI 同期元 |
+| `source_path` | Drive/legacy drive 内の相対 path |
+| `source_file_id` | Google Drive file id |
+| `normalized_source_key` | 同期元を一意化した key |
| `created_by_user_id` | 作成者 |
| `updated_by_user_id` | 更新者 |
| `discarded_at` | 論理削除 |
-`Material` は `MyDiscard` ではなく独自に `default_scope -> { kept }` を持つ。
+`character` タグも素材を持てる。文言上「素材カテゴリのみ」としない。
-## タグ制約
+手動作成ではタグ必須とする。同期取り込みでは未分類素材を許容し、耕作員が後から分類する。
-素材に紐づくタグは `character` または `material` カテゴリでなければならない。
+## 9.2 素材タグ体系
-注意: バリデーション文言は「素材カテゴリのタグ」と言ってゐるが、実装上は `character` も許可してゐる。仕様としては **character も素材を持てる** で確定し、文言を直すべきである。
+派生差分は親タグを使って表す。
-## API
+```txt
+伊地知ニジカ category: character
+伊地知ニジカ_泣き category: material, parent: 伊地知ニジカ
+伊地知ニジカ_笑顔 category: material, parent: 伊地知ニジカ
+```
-| API | 現行権限 | 確定仕様 | 内容 |
-| --- | --- | --- | --- |
-| `GET /materials` | public | public | 素材一覧 |
-| `GET /materials/:id` | public | public | 素材詳細。関連 Wiki 本文も返す |
-| `POST /materials` | current_user 必須 | member+ | 素材作成 |
-| `PUT/PATCH /materials/:id` | member+ | member+ | 素材更新 |
-| `DELETE /materials/:id` | member+ | member+ | 論理削除 |
+この設計により「キャラクター本体」と「素材差分」を同じタグ親子体系で扱える。
-## 確定仕様: 素材作成権限
+ただし、同期時の自動分類は行わない。ファイル名や path がニジカらしく見えても、勝手に `伊地知ニジカ` タグを付けない。`material_sync_sources.default_tag_name` は互換・将来検討用の項目であり、標準同期仕様では使わない。
-素材作成は member 以上を原則とする。現行の guest 作成可能状態は設計意図ではなく実装バグとして扱ふ。
+## 9.3 権限
-ただし、URL-only 素材については guest への開放余地がある。理由は、file 付き素材と違ひ、オブジェクト・ストレージを直接圧迫しないためである。
+| 入力 | guest | member/admin |
+| --- | ---: | ---: |
+| URL-only | 当面不可。将来検討 | 可 |
+| file-only | 不可 | 可 |
+| URL + file | 不可 | 可 |
+| 同期抑止登録 | 不可 | 可 |
+| 同期元登録/編集 | 不可 | admin 推奨 |
+| ZIP download | 可。ただし rate limit | 可 |
-推奨仕様:
+file 素材は object storage を直接圧迫するため guest に開けない。差替後の実装では `POST /materials` も member 以上に閉じる。
-| 入力 | guest | member+ | 理由 |
-| --- | --- | --- | --- |
-| URL-only | 将来検討 | 可 | storage 圧迫がない。荒らし URL への moderation は別途必要 |
-| file-only | 不可 | 可 | storage 圧迫・危険ファイル・著作権対応が重い |
-| URL + file | 不可 | 可 | file を含むため member+ |
+## 9.4 作成・更新
-現時点では単純に `POST /materials` を member+ へ制限するのが最優先である。URL-only guest 開放は、rate limit、通報/削除導線、URL preview の安全化後に検討する。
+`POST /materials` と `PUT/PATCH /materials/:id` は次を行う。
-## 確定仕様: material_versions
+1. current user が member 以上であることを確認する。
+2. tag 名を解決し、存在しなければ `material` category の tag を作る。
+3. file がある場合、SHA256 を計算する。
+4. `material_import_blocks` に該当する SHA256 は拒否する。
+5. Active Storage blob に SHA256 metadata を持たせる。
+6. material 本体を保存する。
+7. export path が指定されていれば `material_export_items` を upsert する。
+8. `material_versions` を記録する。
+9. 画像または動画なら thumbnail を生成する。
-`material_versions` は必要なテーブルであり、残骸ではない。素材にも履歴を持たせる。
+更新時に URL だけへ切り替える場合、file は detach できる。
+
+## 9.5 一覧・検索
+
+`GET /materials` は次を持つ。
+
+| パラメータ | 仕様 |
+| --- | --- |
+| `q` | タグ名、URL、ファイル名の部分一致 |
+| `tag_state` | `all`, `tagged`, `untagged` |
+| `unclassified` | legacy alias。true の場合 `tag_state=untagged` |
+| `media_kind` | `all`, `image`, `video`, `audio`, `file_other`, `url_only` |
+| `tag_id` | 指定タグで絞り込み |
+| `include_descendants` | 指定タグの子孫タグも含める |
+| `group_by` | `none`, `parent_tag` |
+| `created_from/to` | 作成日時範囲 |
+| `updated_from/to` | 更新日時範囲 |
+| `sort` | `created_at`, `updated_at`, `tag_name`, `media_kind`, `file_byte_size`, `version_no`, `id` |
+| `direction` | `asc`, `desc` |
+| `page`, `limit` | ページネーション |
+
+`group_by=parent_tag` の場合は親タグ単位の group 情報を返す。廃止タグは通常の group 候補にしない。
+
+## 9.6 表現
+
+素材レスポンスは少なくとも次を返す。
+
+| フィールド | 内容 |
+| --- | --- |
+| `id` | material id |
+| `version_no` | material version |
+| `tag` | nullable。id, name, category, deprecated state |
+| `url` | 外部 URL |
+| `file_url` | 添付 file の URL |
+| `thumbnail_url` | thumbnail URL |
+| `media_kind` | image/video/audio/file_other/url_only |
+| `content_type` | file MIME type |
+| `file_byte_size` | file size |
+| `source_kind/source_uri/source_path/source_file_id/normalized_source_key` | 同期元情報 |
+| `export_paths` | profile ごとの export path |
+| `created_by_user/updated_by_user` | 操作者 |
+| `wiki_page_body` | 詳細画面用。対応タグの Wiki 本文 |
+
+タグがない素材は、同期取り込み直後の未分類素材として UI に表示する。
+
+## 9.7 履歴
+
+`material_versions` は必須である。
snapshot 対象:
-| 対象 | 必要性 |
+| 対象 | 内容 |
| --- | --- |
-| tag | 素材が何に紐づいてゐたかを復元するため必要 |
-| URL | URL-only / 参照先変更履歴として必要 |
-| file blob | 添付ファイル差替え履歴として必要 |
-| 更新者 | 誰が変更したかを追跡するため必要 |
-| parent | 廃止予定のため新仕様では対象外 |
+| tag | `tag_id`, `tag_name`, `tag_category` |
+| URL | `url` |
+| file | `file_blob_id`, filename, content_type, byte_size, checksum, sha256 |
+| source | source_kind, source_uri, source_path, source_file_id, normalized_source_key |
+| export | `export_paths_json` |
+| state | discarded_at |
+| actor | created_by_user_id, updated_by_user_id |
+| parent | 現行互換の snapshot として残す。新仕様では親素材構造を拡張しない |
-推奨 `material_versions` 返却形:
+`GET /materials/versions` は `material_id`, `tag`, `event_type` で絞り込める。並びは `created_at DESC, id DESC` とする。
-```ts
-interface MaterialVersion {
- id: number
- materialId: number
- versionNo: number
- eventType: 'create' | 'update' | 'discard' | 'restore'
- tag: { id: number, name: string | null, category: string | null } | null
- url: string | null
- fileBlobId: number | null
- fileUrl: string | null
- createdByUser: { id: number, name: string | null } | null
- createdAt: string
-}
+## 9.8 object storage と thumbnail
+
+素材ファイルの本体は object storage に置く。Cloudflare R2 など S3 互換 service を想定する。
+
+必須:
+
+- sha256 を保存する。
+- byte_size を保存する。
+- content_type を保存する。
+- original filename を保存する。
+- 同一 sha256 の重複アップロードは blob 再利用を検討する。
+- public URL は policy により制御する。
+- noindex/公開抑止対象素材には header または署名 URL で制御する。
+
+Thumbnail 生成:
+
+| 対象 | 処理 |
+| --- | --- |
+| image/* | MiniMagick で 180px 級 JPEG へ変換 |
+| video/* | ffmpeg で 1 秒地点、失敗時 0 秒地点を抽出し JPEG 化 |
+| audio/* | thumbnail は生成しない |
+| URL-only | thumbnail は生成しない |
+
+thumbnail 生成失敗は material 保存失敗にしない。ログに残し、UI は fallback 表示を行う。
+
+## 9.9 export path と ZIP download
+
+`material_export_items` は、素材を旧素材集互換の ZIP へ出すための path を管理する。
+
+| カラム | 仕様 |
+| --- | --- |
+| `material_id` | 素材 |
+| `profile` | 初期値 `legacy_drive` |
+| `export_path` | ZIP 内の相対 path |
+| `enabled` | false の場合 export しない |
+| `created_by_user_id` | 登録者 |
+
+制約:
+
+- 1 material につき profile ごとに 1 export item。
+- profile + export_path は一意。
+- absolute path、drive path、backslash、NUL、`.`、`..`、連続 slash、末尾 slash は禁止。
+
+`GET /materials/download.zip` は有効な export item をもとに ZIP を生成する。
+
+- file 添付がない素材は ZIP に入れない。
+- export 対象が空なら validation error を返す。
+- export path 重複は validation error を返す。
+- file が Active Storage 上で失われている場合は missing file error を返す。
+- tag subtree 単位で export できる。
+- 公開抑止・除票素材は ZIP から除外する。
+- ダウンロードには rate limit を設ける。
+
+## 9.10 同期抑止
+
+`material_sync_suppressions` は、外部同期から特定素材を取り込まないためのルールである。
+
+| source_kind | 意味 |
+| --- | --- |
+| `uri` | source_uri 完全一致 |
+| `google_drive_path` | Google Drive 相対 path 完全一致 |
+| `google_drive_path_prefix` | Google Drive 相対 path prefix |
+| `google_drive_file` | Google Drive file id |
+| `legacy_drive_path` | legacy drive 相対 path 完全一致 |
+| `legacy_drive_path_prefix` | legacy drive 相対 path prefix |
+
+理由:
+
+- `copyright_high_risk`
+- `copyright_takedown`
+- `adult_or_sensitive`
+- `personal_information`
+- `malware_or_dangerous_file`
+- `duplicate_or_low_quality`
+- `source_owner_request`
+- `other`
+
+path は同期元フォルダからの相対 path だけ許可する。`My Drive/`、`マイドライブ/`、absolute path、`.`、`..`、NUL、連続 slash、末尾 slash は禁止する。
+
+`GET/POST /materials/suppressions` は member 以上が使える。
+
+## 9.11 取込ブロック
+
+`material_import_blocks` は、アップロードまたは同期取込自体を拒否するためのルールである。
+
+| match_kind | 仕様 |
+| --- | --- |
+| `sha256` | file hash 一致で拒否 |
+| `exact_path` | 外部 path 完全一致で拒否 |
+| `path_prefix` | 外部 path prefix 一致で拒否 |
+| `manual` | 管理者の手動記録 |
+
+手動アップロード時は SHA256 block を確認する。同期時も SHA256 block と同期抑止を確認する。
+
+## 9.12 同期元
+
+`material_sync_sources` は外部素材集の同期元を管理する。
+
+| カラム | 仕様 |
+| --- | --- |
+| `name` | 表示名 |
+| `source_kind` | `uri`, `google_drive_path`, `google_drive_file`, `legacy_drive_path` |
+| `source_uri` | URI 同期元 |
+| `source_path` | Drive/legacy drive の相対 path |
+| `source_file_id` | Google Drive file id |
+| `profile` | 初期値 `legacy_drive` |
+| `enabled` | 同期対象か |
+| `default_tag_name` | 標準仕様では使わない。既定タグ自動付与は禁止 |
+| `export_path_prefix` | export path 生成時の prefix |
+| `last_synced_at` | 最終同期日時 |
+
+Google Drive 同期は service account を用いる。必要な環境変数は次を標準とする。
+
+- `GOOGLE_DRIVE_SERVICE_ACCOUNT_EMAIL`
+- `GOOGLE_DRIVE_PRIVATE_KEY` または `GOOGLE_DRIVE_PRIVATE_KEY_PATH`
+- `GOOGLE_DRIVE_SUBJECT` は必要時のみ
+
+Drive API scope は readonly に限定する。Google Docs などの native document は file として直接取り込まない。
+
+`legacy_drive_path` は DB 上の source_kind として保持できるが、runner 実装は段階的対応とする。未対応 source_kind は同期失敗として明示する。
+
+## 9.13 Git 管理と配布
+
+素材は ZIP ダウンロードだけでなく、知識のある利用者が Git で最新化できるようにする。
+
+ただし、大容量ファイルを通常 Git に直接入れると repository が死ぬ。Git は索引と manifest を管理し、実体は object storage に置く。
+
+生成される repository 例:
+
+```txt
+materials.git
+- README.md
+- manifest.json
+- tags/
+ - 伊地知ニジカ.json
+ - 伊地知ニジカ_泣き.json
+- files/
+ - ab/cd/.asset.json
+- scripts/
+ - sync-assets
```
-添付 issue #306 にも `material_versions` 実装が明記されてゐる。素材管理は UI だけでなく履歴まで含めて完了とみなすべきである。
+`git pull` で manifest と差分情報を取得する。実体ファイルは script が object storage から取得する。
+禁止:
-# 上映会仕様
+- 任意の Git push をそのまま production storage に反映すること。
+- MIME/type/size/sha256 検証なしで取り込むこと。
+- Git repository に巨大 binary を無制限に保持すること。
-## 概要
+# 10. 上映会仕様
-`theatres` は共同視聴部屋である。
+## 10.1 基本
-| 属性 | 意味 |
+上映会は共同視聴 room である。
+
+| 属性 | 仕様 |
| --- | --- |
| `name` | 部屋名 |
| `opens_at` / `closes_at` | 開始/終了 |
| `kind` | 種別 |
-| `current_post_id` | 現在再生中投稿 |
+| `current_post_id` | 現在投稿 |
| `current_post_started_at` | 再生開始時刻 |
-| `host_user_id` | 現在ホスト |
+| `host_user_id` | 暫定制御者 |
| `next_comment_no` | コメント採番 |
-## 在席とホスト
+## 10.2 在席
-`PUT /theatres/:id/watching` はログイン済みユーザを在席として更新する。
+`PUT /theatres/:id/watching` は現在ユーザを在席として更新する。
-- `theatre_watching_users.expires_at` は 30 秒後。
-- 現在 host がいない、または host が active でなければ、呼び出しユーザが host になる。
-- フロントは約 1.5 秒ごとに watching を送る。
+- `expires_at` は短時間、現行目安 30 秒。
+- active user だけを人数・skip 判定に使う。
+- watching は荒らし対策の rate limit 対象にする。
-返却:
+## 10.3 host
-- `host_flg`
-- `post_id`
-- `post_started_at`
-- `post_elapsed_ms`
-- `watching_users`
-- `skip_vote`
+host は暫定制御者であり、長期的にはサーバ主導進行へ寄せる。
-## 確定仕様: host 制御の方向性
+必須:
-現行の active watching user から自動で host を選ぶ仕様は暫定である。開発者意図としては、上映会の進行制御はできるだけサーバ側で担ひたい。
+- host 交替は監査対象。
+- 連続 next に rate limit を設ける。
+- 再生不能申告は複数条件で検証する。
+- admin は host を移譲または解除できる。
-したがって、長期仕様では次を目標にする。
+## 10.4 次投稿選択
-| 領域 | 方針 |
-| --- | --- |
-| 次投稿決定 | サーバが theatre 状態・番組表・skip イベント・重みを見て決定 |
-| 再生不能検知 | クライアント通知は使ふが、最終判断はサーバ状態に集約 |
-| host | 手動操作が必要な暫定制御者。恒久的な絶対権限者ではない |
-| 荒らし耐性 | 公表前に host 乗っ取り・連続 next・skip 連打への制限を入れる |
+候補:
-`skip 投票と next_post の権限を分離するか` という問いは、現時点では仕様語彙が曖昧だった。ここでは次の意味に定義する。
-
-- skip 投票: 視聴者全員ができる意思表示。
-- next_post: 現行では host だけが実行できる即時進行操作。
-- 将来仕様: next_post も単なる host 命令ではなく、サーバの進行判断 API に寄せる。
-
-## 次投稿選択
-
-`PATCH /theatres/:id/next_post` は現行 host のみ実行可能。
-
-`TheatrePostSelector` は次の投稿を候補にする。
-
-- `url` に `nicovideo.jp`, `youtube.com/watch`, `youtu.be` を含む投稿。
+- YouTube/Nico 等、埋め込み再生可能な投稿。
+- 除票・公開抑止・内部検索抑止投稿は除外。
+- noindex のみなら上映可能。ただし room policy で除外可能。
- 現在投稿は除外。
-重みは `1.0 / (1.0 + penalty)`。
+重み:
-`penalty` は、active user が過去に skip した投稿のタグに基づく。つまり、視聴者が嫌がったタグを持つ投稿ほど選ばれにくい。
+- active user が skip した投稿のタグに基づき penalty を増やす。
+- 上位タグ・親タグも重みに反映する。
+- 同じ投稿が短期間に再選出されないよう cooldown を持つ。
-添付 issue #360 により、上映会で上位タグを持つタグが表示されないバグが P1 として課題化されてゐる。上映会はタグ継承・親タグ展開と強く結びつくため、タグ表示は単純な直接付与タグだけでは不足する。
+## 10.5 skip 投票
-## 番組表
+- active users の過半数で skip 確定。
+- 必要票数は `floor(active_count / 2) + 1`。
+- 投票は `(theatre_id, post_id, user_id)` で一意。
+- skip event には voters と当時タグ snapshot を保存する。
-`theatre_programmes` は再生履歴/番組表である。
+## 10.6 コメント
-| カラム | 意味 |
+- guest も投稿可能。
+- 削除は投稿者本人または admin。
+- 削除済みコメントは no と時刻を残し、本文は null にする。
+- comment spam rate limit を設ける。
+
+# 11. Gekanator 仕様
+
+## 11.1 目的
+
+Gekanator は投稿当てゲームである。同時に、質問・回答例・類似投稿補正を蓄積する学習機構である。
+
+## 11.2 公開段階
+
+| 段階 | 対象 | 目的 |
+| --- | --- | --- |
+| 調整段階 | admin | データ作成、質問品質確認、パラメータ調整 |
+| 試験公開 | member または限定 guest | UX と負荷確認 |
+| 公表版 | 一般 | ゲームとして公開 |
+
+公表版ではスコアや内部候補を見せない。
+
+## 11.3 質問 kind
+
+| kind | 条件 |
| --- | --- |
-| `theatre_id` | 部屋 |
-| `position` | 連番位置 |
-| `post_id` | 投稿 |
-| `created_at` | 追加時刻 |
+| `tag` | タグを含むか |
+| `source` | URL host/source |
+| `title` | タイトル文字数・構造 |
+| `original_year` | 元投稿年 |
+| `original_month` | 元投稿月 |
+| `original_month_day` | 月日 |
+| `title_length` | タイトル長 |
+| `post_similarity` | 特定投稿との類似 |
+| `material` | 素材・キャラクター差分 |
-`TheatrePostAdvancer` は次投稿へ進むたびに position を加算して programme を作る。
+曖昧表現は禁止する。「タイトルが長め」ではなく「タイトルが N 文字以上」とする。
-## コメント
+## 11.4 回答値
-`theatre_comments` は部屋内コメントである。
+| 値 | 意味 | score |
+| --- | --- | --- |
+| `yes` | はい | 強い正 |
+| `partial` | 部分的にそう | 弱い正 |
+| `unknown` | わからない | スコアに混ぜない |
+| `probably_no` | たぶん違う | 弱い負 |
+| `no` | いいえ | 強い負 |
-- 主キーは `(theatre_id, no)`。
-- 投稿時に theatre を lock し、`next_comment_no` を採番する。
-- 削除は投稿者本人のみ。
-- 削除後は `discarded_at` を設定し、一覧では `content: null`, `deleted: true` として返す。
-- フロントは最新 20 件を中心に取得する。
+`unknown` を `no` 側に混ぜてはいけない。ここを間違えると候補集合が壊れる。
-## スキップ投票
+## 11.5 質問数
-`theatre_skip_votes` は `(theatre_id, post_id, user_id)` 複合主キー。
+- 原則 25 問までは推測しない。
+- 「続けますか → はい」後も 25 問ルールを維持する。
+- 例外は、1 投稿がほぼ 100% で、2 位以下がほぼ 0% の場合のみ。
+- 例外でも最低数問は出す。
+- 最大 80 問程度で打ち切りを設ける。
-`PUT /skip_vote`:
+## 11.6 候補・スコア
-1. ログイン必須。
-2. `post_id` 必須。
-3. watching を更新。
-4. 現在投稿と request post_id が違へば 409。
-5. vote 作成。
-6. active users の過半数に達したら skip 確定。
+- 全投稿スコアは常に保持する。
+- 候補から drop しても score は捨てない。
+- `maxScore >= 20` かつ `score < 0` の投稿は候補から hard drop してよい。
+- `post_similarities.cos` は類似投稿補正に必ず乗算する。
+- metadata 条件には類似度補正を掛けない。年・月・タイトル長は直接一致のみ。
+- `tag_similarities` はタグ質問の類似補助に使うが、確定回答を上書きしない。
-必要票数:
+## 11.7 候補復旧
-```txt
-required_count = floor(active_watching_users_count / 2) + 1
+矛盾で候補が 0 になる場合、暗黙に復旧してよい。
+
+- 次質問の全回答選択肢が 0 件になる場合だけ事前に広げる。
+- ある選択肢だけ 0 件なら、実際に選ばれるまで広げない。
+- 復旧は 6 件、12 件、24 件のように段階的に行う。
+- 復旧したことをユーザに大きく表示しない。
+
+## 11.8 ユーザ追加質問
+
+- 追加学習は 2 件単位。
+- 1 play 中、有効なユーザ追加質問を 2/3 程度出す。
+- 無効質問も 1/3 程度は出題し、次学習に使う。
+- `unknown` の追加質問は即昇格しない。
+- 回答例には user/game/source/weight/answer_counts を保存する。
+
+## 11.9 AI 変換
+
+AI は質問分類だけでなく、既存投稿への回答補完も行う。
+
+必須:
+
+- model は `GEKANATOR_AI_MODEL` 環境変数で差し替え可能。
+- structured output を使う。
+- 低 confidence は pending に留める。
+- AI 生成結果は `ai_generated` として監査可能にする。
+- 予算上限を持つ。
+- 大量補完は batch または夜間 rake task に寄せる。
+
+半年 500 円程度を守るなら、play ごと即時 AI 実行は高すぎる。cache と差分処理が必須である。
+
+
+## 11.10 回答例の統計
+
+`gekanator_question_examples` は単一回答だけでなく、回答統計を持つ。
+
+| 属性 | 仕様 |
+| --- | --- |
+| `answer_counts` | answer ごとの集計 JSON |
+| `sample_count` | 集計対象 sample 数。既定 1 |
+
+同じ question/post/user の回答は一意とし、後続の回答で統計を更新できる。Gekanator の scoring は、単発回答だけでなく集計済み回答の偏りを使えるようにする。
+
+# 12. Preview API
+
+Preview API は便利だが、サーバから任意 URL にアクセスするため危険である。公表前 P0 として扱う。
+
+## 12.1 `/preview/title`
+
+- URL を受け取り title を取得する。
+- scheme は http/https のみ。
+- redirect 先も検査する。
+- content length 上限を設ける。
+- timeout を短くする。
+- HTML 以外は拒否する。
+- cache を使う。
+
+## 12.2 `/preview/thumbnail`
+
+- headless browser による screenshot を生成する。
+- `/preview/title` より厳しい rate limit と concurrency 制限を設ける。
+- private IP、loopback、link-local、metadata IP を拒否する。
+- browser context は sandbox 的に隔離する。
+- 出力画像サイズ・生成時間・メモリ使用を制限する。
+
+## 12.3 SSRF 防御
+
+必須拒否:
+
+- localhost / loopback
+- private address
+- link-local
+- multicast
+- cloud metadata IP
+- file scheme
+- ftp scheme
+- gopher 等の非 http(s)
+- redirect 後に上記へ到達する URL
+
+## 12.4 rate limit 初期値
+
+| 単位 | title | thumbnail |
+| --- | ---: | ---: |
+| IP | 60 / 10 min | 10 / 10 min |
+| user | 120 / 10 min | 20 / 10 min |
+| URL host | 30 / 10 min | 10 / 10 min |
+
+# 13. 同期仕様
+
+## 13.1 Nico sync
+
+- 外部スクリプトから動画情報を取得する。
+- 投稿がなければ作成する。
+- `タグ希望`, `bot操作`, `ニコニコ`, `動画` を付与する。
+- 生 Nico タグは `nico:` で nico tag 化する。
+- nico tag relation に基づき内部タグを付与する。
+- 投稿版と nico tag 版を記録する。
+- 除票済み投稿は自動復帰しない。
+
+## 13.2 YouTube sync
+
+- 検索語と playlist から候補動画を取得する。
+- 既存 URL normalizer で重複検出する。
+- 新規投稿に `タグ希望`, `bot操作`, `YouTube`, `動画` を付与する。
+- channel ID が deerjikists にあれば deerjikist tag を付与する。
+- 不明なら `ニジラー情報不詳` を付与する。
+
+## 13.3 bot 操作タグ整理
+
+`bot操作` のみ、または未整理の bot 投稿を人手で整理する運用を持つ。
+
+改善仕様:
+
+- `bot操作` 投稿の一覧に専用作業画面を持つ。
+- 別投稿からタグ import できる。
+- AI/ルールで楽曲・投稿者・source タグ候補を出す。
+- 作業済みで `bot操作` を外す。
+- 進捗件数を dashboard に出す。
+
+# 14. 検索・sitemap・SEO
+
+## 14.1 sitemap
+
+sitemap は frontend build 後に生成する。
+
+含める:
+
+- 通常投稿詳細。
+- 通常タグ詳細。
+- 通常 Wiki。
+- 通常素材。
+
+除外する:
+
+- 除票。
+- 公開抑止。
+- 索引抑止。
+- 空本文 Wiki。
+- settings、theatre、Gekanator、error pages。
+- query search pages。
+
+## 14.2 robots meta
+
+route ごとの既定:
+
+| route | 既定 |
+| --- | --- |
+| `/posts/:id` | index。ただし policy により noindex |
+| `/tags/:id` | index。ただし policy により noindex |
+| `/wiki/:title` | index。ただし空本文・policy により noindex |
+| `/materials/:id` | index。ただし policy により noindex |
+| `/theatres/:id` | noindex |
+| `/settings` | noindex |
+| `/gekanator` | noindex または公開後 index 判断 |
+| error pages | noindex |
+
+## 14.3 内部検索
+
+内部検索と外部検索は分ける。
+
+- noindex だけなら内部検索には出してよい。
+- 内部検索抑止なら出さない。
+- 公開抑止・除票は出さない。
+- admin は明示 filter で抑止対象を確認できる。
+
+# 15. API 仕様概要
+
+## 15.1 共通
+
+- JSON は snake_case を返す。frontend は camelCase に変換してよい。
+- validation error は `type`, `message`, `errors`, `base_errors` を返す。
+- conflict は 409 とし、base/current/mine/conflicts を返す。
+- forbidden は 403。
+- not found は 404。
+- 除票・公開抑止は public には 404 を既定とする。
+
+## 15.2 代表 endpoint
+
+| 領域 | endpoint |
+| --- | --- |
+| posts | `GET /posts`, `GET /posts/:id`, `POST /posts`, `PUT/PATCH /posts/:id`, `GET /posts/random`, `GET /posts/versions`, `POST/DELETE /posts/:id/viewed` |
+| post moderation | `POST /posts/:id/discard`, `POST /posts/:id/restore`, `PUT /posts/:id/index_policy` |
+| tags | `GET /tags`, `GET /tags/:id`, `GET /tags/name/:name`, `GET /tags/name/:name/deerjikists`, `GET /tags/name/:name/materials`, `PUT/PATCH /tags/:id`, `GET /tags/autocomplete`, `GET /tags/with-depth`, `GET /tags/versions` |
+| tag moderation | `PUT /tags/:id/index_policy`, `POST /tags/:id/deprecate`, `POST /tags/:id/undeprecate` |
+| nico tags | `GET /tags/nico`, `PUT /tags/nico/:id` |
+| wiki | `GET/POST /wiki`, `GET/PUT /wiki/:id`, `GET /wiki/search`, `GET /wiki/changes`, `GET /wiki/:id/diff`, `GET /wiki/title/:title`, `GET /wiki/title/:title/exists` |
+| materials | `GET /materials`, `GET /materials/:id`, `POST /materials`, `PUT/PATCH /materials/:id`, `DELETE /materials/:id`, `GET /materials/download.zip`, `GET /materials/versions` |
+| material sync | `GET /materials/suppressions`, `POST /materials/suppressions`, sync source 管理 API, sync runner job |
+| material git | `POST /materials/imports`, `GET /materials/git/manifest` |
+| deerjikists | `GET/PUT/DELETE /deerjikists/:platform/:code` |
+| preview | `GET /preview/title`, `GET /preview/thumbnail` |
+| users | `POST /users`, `POST /users/verify`, `POST /users/code/renew`, `GET /users/me`, `PUT/PATCH /users/:id`, `GET/PATCH /users/settings`, `GET /users/theme_slots`, `PUT /users/theme_slots/:base_theme/:slot_no` |
+| theatres | `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`, question suggestions, ai convert |
+
+# 16. DB ターゲット設計
+
+## 16.1 追加が必要な主要テーブル/カラム
+
+| 対象 | 追加 | 理由 |
+| --- | --- | --- |
+| `users` | `last_seen_at`, `created_ip_address_id`, `user_agent_hash` | guest 掃除・bot 対策 |
+| `users` | unique index on `inheritance_code` | 認証トークン一意性 |
+| `posts` | `discarded_at`, `discarded_by_user_id`, `discard_reason` | 除票 |
+| `posts` | `index_suppressed_at`, `public_suppressed_at`, `internal_search_suppressed_at` | policy cache |
+| `tags` | `index_suppressed_at`, `public_suppressed_at`, `internal_search_suppressed_at` | tag policy cache |
+| `wiki_pages` | `index_suppressed_at`, `public_suppressed_at`, `internal_search_suppressed_at` | wiki policy cache |
+| `materials` | `index_suppressed_at`, `sha256`, `byte_size`, `content_type` | 素材管理 |
+| `materials` | `version_no`, source fields, `normalized_source_key` | 同期・履歴・復元 |
+| `material_versions` | file/source/export snapshot | 素材履歴 |
+| `material_export_items` | profile, export_path, enabled | ZIP/Git export |
+| `material_sync_suppressions` | source_kind, normalized_source_key, reason | 同期抑止 |
+| `material_sync_sources` | source_kind, source info, enabled | 外部素材集同期 |
+| `material_import_blocks` | sha256/path block | 取込拒否 |
+| `settings` | typed settings | user portable settings |
+| `user_theme_slots` | base_theme, slot_no, tokens | user theme slots |
+| `posts`, `post_versions` | `video_ms` | 動画長と局所記載履歴 |
+| `index_policies` | 新規 | noindex/抑止の根拠 |
+| `post_tag_sections` | 新規 | 局所記載 |
+| `post_remarks` | 新規 | 備考欄 |
+| `post_remark_versions` | 新規 | 備考履歴 |
+| `external_tag_suppression_rules` | 新規 | 連携抑止 |
+| `material_imports` | 新規 | Git import 監査 |
+| `material_manifest_entries` | 任意 | Git/export manifest cache |
+
+## 16.2 index policy propagation JSON
+
+例:
+
+```json
+{
+ "tag": {
+ "posts": "direct_and_expanded",
+ "wiki_page": true,
+ "materials": true,
+ "text_mentions": true,
+ "aliases": true
+ },
+ "phrase": {
+ "match": "exact_token",
+ "targets": ["wiki_body", "post_title", "tag_name", "material_title"]
+ }
+}
```
-スキップ確定時:
+# 17. Frontend 画面仕様
-- `TheatreSkipFinalizer` が `theatre_skip_events` を作る。
-- voter 一覧を `theatre_skip_event_voters` に保存。
-- skip 時点の投稿タグを `theatre_skip_event_tags` に保存。
-- 当該投稿の skip vote を削除。
-- `TheatrePostAdvancer` で次投稿へ進む。
+## 17.1 route
-`DELETE /skip_vote` で自分の投票を取り消せる。
-
-## 再生同期
-
-フロント `TheatreDetailPage` は server elapsed と player current time の差が 5 秒を超えた場合に seek する。
-
-- YouTube は `react-youtube`。
-- ニコニコは `NicoViewer` による JS API `postMessage`。
-- duration が 0 以下など再生不能っぽい場合、現行では host が次投稿へ進める。
-
-## 埋め込み
-
-`PostEmbed` の対応:
-
-| URL | 表示 |
+| path | 仕様 |
| --- | --- |
-| NicoVideo | `NicoViewer` iframe + postMessage |
-| YouTube | `react-youtube` |
-| Twitter/X status | blockquote + widgets script |
-| その他 | 確認後 iframe |
+| `/posts` | 投稿一覧。ページネーション |
+| `/posts/new` | 投稿作成 |
+| `/posts/search` | 投稿検索 |
+| `/posts/:id` | 投稿詳細・編集。policy により noindex |
+| `/posts/changes` | 投稿履歴 |
+| `/tags` | タグ一覧。廃止・抑止 filter |
+| `/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` | 素材一覧。grid/list、tag filter、media filter、grouping |
+| `/materials/new` | 素材作成 |
+| `/materials/:id` | 素材詳細 |
+| `/materials/changes` | 素材全体履歴 |
+| `/materials/:id/changes` | 素材別履歴 |
+| `/materials/suppressions` | 素材同期抑止 |
+| `/theatres/:id` | 上映会。noindex |
+| `/gekanator` | Gekanator |
+| `/users/settings`, `/settings` | 設定。noindex |
-ニコニコ iframe の `loadComplete` timeout は 8000 ms。
+## 17.2 UI 共通要件
+
+- 抑止状態・廃止状態は本文タイトルとは別表示する。
+- noindex は UI 上にも管理者向けに理由を表示する。
+- guest に編集欄は見せても disabled にするか、説明付きで隠す。
+- validation error は field ごとに表示する。
+- 競合時は current/mine/base の差分を表示する。
+- 「保存できたように見えて失敗」は禁止。
-# Gekanator 仕様
+## 17.3 設定 UI
-## 概要
+設定画面は保存対象を明確に分ける。
-Gekanator は投稿当てゲームである。投稿群に関する質問を出し、回答から候補投稿を絞り、最終的に投稿を推測する。
-
-用途はゲームだけではない。終了後の質問追加・回答保存により、投稿間類似や識別質問を蓄積する学習機構でもある。
-
-## 確定仕様: 公開範囲
-
-Gekanator は恒久的な admin-only ツールではない。
-
-段階:
-
-| 段階 | 公開範囲 | 目的 |
+| 対象 | 保存先 | 例 |
| --- | --- | --- |
-| 現行 | admin のみ | 学習データ作成、質問品質確認、パラメータ調整 |
-| 調整後 | member または限定ユーザ | 追加学習、UX 確認 |
-| 公表版 | 一般ユーザ | おたのしみゲームとして公開 |
+| portable | DB `settings` | theme, auto fetch, wiki editor mode |
+| theme slot | DB `user_theme_slots` | light/dark 各 3 slot の tokens |
+| device-local | localStorage | animation, prefetch, tag grouping, active slot, shortcut |
-公表版では、内部パラメータ・候補スコア・調整用情報は非表示にする。添付 issue #361 により「グカネータ公開」が課題化されてゐる。
+未保存変更がある場合、別 tab や別 route への移動前に確認する。保存ボタンが必要な設定は、変更即反映と保存済み状態を UI 上で区別する。
-## 現行権限
+## 17.4 素材 UI
-バックエンド API は `current_user&.admin?` でなければ 404 を返す。フロントも `/gekanator` は admin 以外 `NotFound` を表示する。
+素材画面は次を持つ。
-これは現行実装であって最終仕様ではない。
+- 一覧/追加/抑止/全体履歴/素材別履歴への導線。
+- grid/list 切替。
+- `tag_state`, `media_kind`, `tag_id`, `include_descendants`, `group_by` filter。
+- 未分類素材を目立たせる表示。
+- export path の編集。
+- file missing、thumbnail 生成失敗、同期抑止適用を区別して表示。
-## 投稿カタログ
+## 17.5 スマホタグ D&D
-`GET /gekanator/posts` は現行 admin 専用。
+スマホのタグ D&D は縦スクロールと競合しやすい。
-返却投稿:
+必須:
-- 全投稿。
-- `tags` preload。
-- サムネイル付き。
-- 並びは `COALESCE(original_created_before - 1分, original_created_from, created_at) DESC, id DESC`。
+- touch では長押し後に drag 準備へ入る。
+- 縦方向移動が強い場合は drag 準備をキャンセルし、スクロールを優先する。
+- draggable node と droppable node を分け、横幅・baseline 表示を壊さない。
+- 空カテゴリにも drop slot を出す。
+- animation off の場合、drop animation も出さない。
+- drag 中は元タグを非表示にし、drag 解除後に再表示する。
-## 質問カタログ
+# 18. 管理・監査
-`GET /gekanator/questions` は現行 admin 専用。
+## 18.1 管理画面 MVP
-質問 kind:
+公表前に最低限必要:
-| kind | 内容 |
-| --- | --- |
-| `tag` | 特定タグを含むか |
-| `source` | URL host/source |
-| `title` | タイトル性質 |
-| `original_date` | 年/月/月日 |
-| `post_similarity` | 特定投稿との近さ/例示回答 |
+- users 一覧、BAN、role 変更。
+- IP BAN。
+- index policies 管理。
+- 除票/復帰。
+- 公開抑止。
+- bot 操作投稿一覧。
+- material uploads 使用量。
+- preview API rate limit 状況。
+- 最近の version/activity。
-質問 source:
+## 18.2 監査ログ
-- `user_suggested`
-- `ai_generated`
-- `admin_curated`
+履歴テーブルがあるものは version でよい。version がない管理操作は audit log を持つ。
-status:
+```txt
+audit_logs
+- id
+- actor_user_id
+- action
+- target_type
+- target_id
+- before_json
+- after_json
+- reason
+- ip_address_id
+- created_at
+```
-- `pending`
-- `accepted`
-- `rejected`
-- `disabled`
+対象:
-現行 API は accepted questions を返す。
+- BAN。
+- role 変更。
+- index policy。
+- 除票/復帰。
+- 公開抑止。
+- material import。
+- AI batch 実行。
-## 回答値
+# 19. テスト仕様
-`GekanatorQuestionSuggestion::ANSWERS`:
+## 19.1 Backend 必須テスト
-| 値 | 意味 |
-| --- | --- |
-| `yes` | はい |
-| `no` | いいえ |
-| `partial` | 部分的にそう |
-| `probably_no` | たぶん違ふ |
-| `unknown` | わからない |
+- 認証・guest 作成・inheritance_code unique。
+- BAN by IP/user。
+- posts CRUD、競合、除票、復帰、index policy。
+- post tag 正規化、廃止タグ拒否、局所記載 parse。
+- tags CRUD、alias、親子循環禁止、廃止、noindex propagation。
+- wiki conflict、body search、autolink、noindex text mention。
+- materials permission、file/url validation、version、Git import。
+- theatre watching、host、skip、comment spam limit。
+- Gekanator score、unknown handling、candidate recovery、similarity cos multiplier。
+- preview SSRF、redirect、private IP、content length、rate limit。
+- sitemap exclusion。
-## フロント推測ロジック
+## 19.2 Frontend 必須テスト
-`frontend/src/lib/gekanator.ts` と `GekanatorPage.tsx` による。
+- route robots meta。
+- post search pagination。
+- conflict UI。
+- tag deprecated display。
+- noindex reason display。
+- local section input and duration number field。
+- material upload validation。
+- Gekanator 25 問 rule。
+- theatre sync and skip UI。
+- validation error rendering。
-主な定数:
+# 20. 実装優先順位
-| 定数 | 値 | 意味 |
+## P0 公表前に必須
+
+1. Preview API SSRF / resource abuse 防御。
+2. 素材作成権限を member+ に閉じる。
+3. 投稿除票と通常フロント 404。
+4. 索引抑止基盤 `index_policies` と route robots meta。
+5. tag-based noindex propagation。
+6. sitemap から noindex/除票を除外。
+7. `users.inheritance_code` unique index。
+8. `POST /users` rate limit と bot 対策。
+9. タグ親子循環禁止。
+10. 投稿検索のページネーション安定化。
+
+## P1 中核 UX
+
+1. 投稿履歴 parent posts 表示。
+2. `post_versions.tags_json` 移行。
+3. 局所記載。
+4. 別投稿からタグ import。
+5. Wiki 本文検索。
+6. material_versions 完成。
+7. Wiki asset。
+8. Gekanator unknown handling / score 引継ぎ / 質問多様化。
+9. 上映会上位タグ表示。
+10. bot 操作タグ整理画面。
+
+## P2 拡張
+
+1. 素材 Git mirror/export/import。
+2. URL-only 素材 guest 開放検討。
+3. AI による Gekanator 回答補完。
+4. 外部タグ連携抑止。
+5. 個人用メモ。
+6. 限定公開。
+7. 申請フォーム。
+8. Pixiv / bilibili / TikTok / ニジカ投稿局埋め込み。
+
+# 21. 質問票
+
+この章は、実装前に確認したい事項である。未回答でも本文の暫定仕様で製造は始められる。
+
+## 21.1 除票・抑止
+
+| ID | 質問 | 暫定仕様 |
| --- | --- | --- |
-| `questionsBetweenGuesses` | 25 | 通常推測までの質問数 |
-| `minQuestionsBeforeCertainGuess` | 5 | 確信時の最低質問数 |
-| `certainGuessPercent` | 99.5 | ほぼ確定判定 |
-| `runnerUpMaxPercent` | 0.5 | 2位候補の上限 |
-| `hardMaxQuestions` | 80 | 最大質問数 |
-| `softenedAnswerWeight` | 0.35 | 答え緩和重み |
-| `confidenceTemperature` | 6 | 確率化温度 |
-| `maxQuestionSuggestionsPerGame` | 3 | 追加質問上限 |
+| Q-DEL-001 | 除票済み投稿の public 詳細は 404 と 410 のどちらにするか | 404 |
+| Q-DEL-002 | member は除票済み投稿を見られるべきか | 不可。admin もフロント上は非表示。削除と同等 |
+| Q-DEL-003 | 除票理由を一般に表示するか | 表示しない |
+| Q-IDX-001 | noindex 対象ページを内部検索には出すか | 出す。内部検索抑止は別指定 |
+| Q-IDX-002 | tag noindex は子タグ・親タグへ伝播するか | 投稿判定では direct + expanded。タグ自体の親子ページへは伝播しない |
+| Q-IDX-003 | tag 名の本文一致は alias も対象か | 対象 |
+| Q-IDX-004 | phrase rule は substring も許可するか | admin 明示時のみ |
+| Q-IDX-005 | noindex 理由を admin UI 以外に表示するか | admin UI のみ |
-質問選択は、候補分割力、冗長性、排他条件、優先度、seed による決定性を組み合はせる。
+## 21.2 投稿
-候補が潰れる場合、高難度・非 unknown の過去回答を `0.35` に緩和して復旧を試みる。
-
-## 確定仕様: 質問数ルール
-
-通常は 25 問を経過するまで推測しない。「続けますか → はい」の流れでも 25 問ルールを維持する。
-
-例外は、100% 近似に近い確信状態で、候補が実質的に一意になった場合のみである。ただし、この例外も最低質問数・2 位候補との差・誤答時の UX を見て慎重に扱ふ。
-
-## ゲーム保存
-
-`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 は質問分類だけでなく、既存投稿への回答補完も行ふ。
-
-担当範囲:
-
-| 処理 | 内容 |
-| --- | --- |
-| 質問分類 | ユーザ文を `tag`, `source`, `title`, `original_date`, `post_similarity` 等へ分類 |
-| 正規化 | 曖昧な質問文をゲームで扱へる構造へ変換 |
-| 回答補完 | 既存投稿群に対して yes/no/partial/probably_no/unknown を推定 |
-| 信頼度 | AI 推定には confidence を持たせ、低信頼は pending に留める |
-| 監査 | AI が作った質問・回答は `ai_generated` として追跡可能にする |
-
-## AI モデル選定
-
-初期実装は OpenAI Responses API + Structured Outputs を前提とする。
-
-推奨初期値:
-
-| 項目 | 値 |
-| --- | --- |
-| 環境変数 | `GEKANATOR_AI_MODEL` |
-| 初期モデル | `gpt-5.4-mini` |
-| 出力形式 | JSON Schema strict structured output |
-| reasoning | `none` または `low` から開始 |
-| 大量補完 | Batch API または夜間 Rake task |
-| 予算 | 仮置き。半年 500 円程度を目標に、run 数・token 数・batch 割引で調整 |
-
-`gpt-5.4-mini` は価格と品質の妥協点としての初期値であり、固定ではない。分類だけなら将来 `gpt-5.4-nano` 等のさらに低価格モデルへ落とせるやぅに、モデル ID は必ず環境変数化する。
-
-旧メモの AI 予算:
-
-| 項目 | 値 |
-| --- | --- |
-| 月上限 | 450 円 |
-| 1 run 見積 | 5 円 |
-| 超過見込み | 402 `blocked_budget` |
-
-この見積は仮置きであり、確定予算ではない。現在の希望は「半年で 500 円くらゐ」であるため、1 run 5 円の設計は高すぎる。AI 補完は per game 即時実行ではなく、batch 化・cache 化・差分実行に寄せるべきである。
-
-
-# プレビュー 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 で返す。
-
-## 確定仕様: セキュリティ方針
-
-Preview API は guest 相当にも提供したい機能である。ただし、危険性は高い。クリーンに守れないなら機能廃止も検討対象である。
-
-最低必須対策:
-
-| 対策 | 必須度 | 内容 |
+| ID | 質問 | 暫定仕様 |
| --- | --- | --- |
-| scheme allowlist | 必須 | `http`, `https` のみ |
-| DNS 解決後 IP 検査 | 必須 | private, loopback, link-local, multicast, metadata IP を拒否 |
-| redirect 検査 | 必須 | redirect 先 URL も同じ検査を繰り返す |
-| content length 上限 | 必須 | title HTML 取得、画像、screenshot 出力に上限 |
-| timeout | 必須 | connect/read/browser 全て短めに制限 |
-| concurrency 制限 | 必須 | screenshot は特に重いので同時実行数を絞る |
-| rate limit | 必須 | IP + user + URL host 単位で制限 |
-| user agent | 推奨 | 明示 UA を使ひ、必要なら deny されても落ちない UX にする |
-| cache | 推奨 | 同一 URL の title/thumbnail を短時間 cache |
+| Q-POST-001 | `thumbnail_base` と添付 thumbnail の優先順位 | 添付 thumbnail 優先、なければ thumbnail_base |
+| Q-POST-002 | 親投稿の循環禁止を入れるか | 入れる |
+| Q-POST-003 | 局所記載の時刻単位 | API は ms、UI は `m:ss` 入力可 |
+| Q-POST-004 | `動画` タグ以外でも duration を持てるか | UI 非表示、API は保存しない。ただね、かぅいふのは `video_posts` みたいな別テーブル切りたいとこなんだよなぁ……検討中 |
-## rate limit 推奨仕様
+## 21.3 タグ
-Preview API を guest に残すなら、Rails 単体の before_action ではなく、Rack middleware または reverse proxy で一次防衛するのがよい。
-
-推奨値の初期案:
-
-| 単位 | `/preview/title` | `/preview/thumbnail` |
+| ID | 質問 | 暫定仕様 |
| --- | --- | --- |
-| IP | 60 req / 10 min | 10 req / 10 min |
-| user | 120 req / 10 min | 20 req / 10 min |
-| URL host | 30 req / 10 min | 10 req / 10 min |
+| Q-TAG-001 | 廃止タグを既存投稿から自動除去するか | しない。表示で除外(検索は除外しない) |
+| Q-TAG-002 | nico タグに索引抑止は可能か | 可能。廃止とは別 |
+| Q-TAG-003 | tag_name_sanitisation_rules は nico タグにも適用するか | 適用する。ただし `nico:` prefix 構造は保つ |
-thumbnail は headless browser を起動するため、title より厳しくする。
+## 21.4 素材
-## 実装差分
-
-現行 API は current_user 必須だが、guest は自動作成されるため実質的に広く使へる。任意 URL をサーバから取得・Node screenshot するので、SSRF/内部ネットワーク到達/リソース消費のリスクがある。これは公表前の赤信号である。
-
-# 類似度計算
-
-`Similarity::Calc` は汎用化されてゐる。
-
-| task | 対象 | 関係 |
+| ID | 質問 | 暫定仕様 |
| --- | --- | --- |
-| `post_similarity:calc` | Post | tags による投稿類似 |
-| `tag_similarity:calc` | Tag | posts によるタグ類似 |
+| Q-MAT-001 | Git mirror は public にするか | public。ただし公開抑止素材は含めない |
+| Q-MAT-002 | Git に実体ファイルを含めるか | 原則含めない。manifest + sync script |
+| Q-MAT-003 | contributor push の権限は member 全員か | member 以上。ただし import job で検証 |
+| Q-MAT-004 | ZIP には noindex 素材を含めるか | 含める。noindex は検索制御であり閲覧禁止ではない |
-計算仕様:
+## 21.5 Gekanator
-- 対象集合 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 自動生成により、事実上かなり広い入口である。
-
-仕様としては guest 提供の余地を残すが、次は必須である。
-
-- private IP / localhost / link-local / metadata IP の拒否。
-- scheme allowlist を http/https に固定。
-- redirect 先検査。
-- content length 上限。
-- screenshot queue/timeout/concurrency 制限。
-- IP + user + host rate limit。
-
-守れないなら機能ごと廃止を検討する。便利さよりサーバ防衛が優先である。
-
-## P0: guest 自動作成による users 肥大化
-
-旧 DB で `users = 42805`。実利用規模に比して異様に多い。bot が来るたび user を発行してゐる可能性が高い。
-
-guest 自動作成は継続するが、以下は未実装リスクとして残る。
-
-- `POST /users` rate limit。
-- bot UA / localStorage 不可環境への抑制。
-- 編集実績のない guest 掃除 task。
-- user 分析用の `last_seen_at` 等。
-
-## P0: 素材作成権限が緩い
-
-`POST /materials` は current_user だけを要求する。guest 自動作成と合はせると、匿名同然で素材作成できる。
-
-確定仕様は member+ 作成である。file 素材は storage を食ふため、ここは早めに閉じるべきである。
-
-## P1: タグ親子編集権限が二重化
-
-- `TagChildrenController`: admin のみ。
-- `TagsController#update_all`: member 以上で `parent_tags` 更新可。
-
-確定仕様は member+ なので、専用 API 側を修正する。あはせて issue #332 の循環登録禁止を入れないと、member 開放は危ない。
-
-## P1: `post_versions` の parent post 履歴 API が不足
-
-schema と snapshot には `parent_post_ids` があるが、`PostVersionsController` が返してゐない。フロント型には `parentPosts` がある。
-
-確定仕様として、親投稿変更履歴は表示必須である。`parent_posts` の返却を追加する。
-
-## P1: `material_versions`, `wiki_assets` は必要だが未実装
-
-開発者回答により、どちらも将来機能として必要と確定した。
-
-- `material_versions`: tag, URL, file blob, 更新者の履歴。
-- `wiki_assets`: Wiki 内画像/添付、`next_asset_no`、sha256 連動。
-
-したがって「残骸として削除」ではなく「仕様化して実装」が正しい。
-
-## P1: inheritance_code の一意制約なし
-
-認証トークンである以上、DB 一意 index を張るべき。UUID 衝突は現実的に低くても、仕様としては弱い。
-
-## P1: 上映会 host 乗っ取り耐性
-
-active host が切れたら次の watching ユーザが host になる。小規模内輪ではよいが、公表後は荒らしが再生制御を握る可能性がある。
-
-長期方針はサーバ主導進行である。現行 host は暫定制御者と見るべきで、連続 next、短時間 host 交替、再生不能偽装への防御が必要である。
-
-## P2: Material のバリデーション文言と許可カテゴリのズレ
-
-文言は「素材カテゴリ」だが、実装は `character` も許可する。仕様として `character` も素材を持てるため、文言を直すべきである。
-
-## P2: Gekanator AI 変換 API は未実装
-
-API と予算 model はあるが converter は NotImplemented。公表版では質問構成 AI を稼動させる予定なので、未実装表示ではなく実装対象である。
-
-## P2: issue 管理との接続が手動
-
-今回の issue 一覧は添付 JSON で取り込んだ。継続的な同期手段は未確定である。全件共有方針は妥当だが、手動添付だけだと仕様書がすぐ腐る。
-
-
-# 開発者ヒアリング反映結果
-
-本章は、前版で未確定だった H-001 から H-011 への回答を記録する。回答内容は本文へ反映済みである。
-
-| ID | 状態 | 反映先 |
+| ID | 質問 | 暫定仕様 |
| --- | --- | --- |
-| H-001 | 回答済み | タグ仕様 / タグ親子 |
-| H-002 | 回答済み | 素材仕様 / 素材作成権限 |
-| H-003 | 回答済み | 認証・ユーザ・BAN / guest 自動作成 |
-| H-004 | 回答済み | 投稿仕様 / 投稿履歴 |
-| H-005 | 回答済み | 素材仕様 / material_versions |
-| H-006 | 回答済み | Wiki 仕様 / Wiki asset |
-| H-007 | 回答済み | Gekanator 仕様 / 公開範囲 |
-| H-008 | 回答済み | Gekanator 仕様 / AI 変換 |
-| H-009 | 回答済み | 上映会仕様 / host 制御 |
-| H-010 | 回答済み | Preview API / セキュリティ方針 |
-| H-011 | 回答済み | Gitea 課題一覧反映 |
+| Q-GEK-001 | 公表版で質問追加を guest に許すか | 許すが rate limit と moderation 後昇格 |
+| Q-GEK-002 | AI 補完を即時実行するか | しない。batch/cached |
+| Q-GEK-003 | noindex 投稿を Gekanator 候補に含めるか | 含める。公開抑止・除票は除外 |
-## 残る未決定事項
+## 21.6 上映会
-回答後も、次はまだ設計決定が必要である。
-
-| 項目 | 未決定内容 | 推奨 |
+| ID | 質問 | 暫定仕様 |
| --- | --- | --- |
-| guest bot 対策 | cookie/localStorage なしアクセス、bot UA、掃除 task の詳細 | `POST /users` rate limit と未使用 guest 掃除 task から着手 |
-| URL-only 素材の guest 開放 | いつ・どの条件で許可するか | 当面は member+ に閉じる |
-| Gekanator AI 予算 | 半年 500 円程度をどう守るか | batch 化、cache 化、差分実行、model env 化 |
-| 上映会 host | 完全サーバ主導へどう移行するか | host を暫定権限に格下げし、server advancer を育てる |
-| Preview API | 防御実装不能な場合の廃止判断 | SSRF 防御を実装できなければ thumbnail 生成から止める |
-| issue 同期 | 今後どう ChatGPT と共有し続けるか | 当面は JSON 添付。将来は read-only token + 手元 export script |
+| Q-TH-001 | noindex 投稿を上映会候補に含めるか | 含める |
+| Q-TH-002 | 公開抑止投稿を上映会候補に含めるか | 含めない |
+| Q-TH-003 | host を完全廃止するか | すぐには廃止しない。サーバ主導へ漸進 |
-# Gitea 課題一覧反映
+# 22. 受入条件チェックリスト
-添付された Gitea issue JSON を取り込み、2026-06-11 時点の仕様書へ反映した。今回の添付には 50 件が含まれ、全件 `open` である。
+公表可能と判断するには、最低限次を満たす。
-## 集計
+- [ ] Preview API が private IP / redirect / large response / browser abuse を防ぐ。
+- [ ] 投稿除票が一覧・検索・詳細・sitemap・Gekanator・上映会から一貫して除外される。
+- [ ] noindex が post/tag/wiki/material に対して API と frontend の両方で効く。
+- [ ] tag-based noindex が本文中の tag name/alias 出現にも効く。
+- [ ] `robots.txt` で noindex 対象を塞いでいない。
+- [ ] sitemap から noindex 対象が消える。
+- [ ] タグ廃止と索引抑止が別状態として扱われる。
+- [ ] nico タグは廃止できない。
+- [ ] 素材 file upload は member+ 限定。
+- [ ] material_versions が tag/url/blob/user を記録する。
+- [ ] 局所記載 parse が仕様通り。
+- [ ] Gekanator で unknown が no 扱いにならない。
+- [ ] Gekanator 類似補正に `post_similarities.cos` が乗る。
+- [ ] `users.inheritance_code` が unique。
+- [ ] タグ親子循環禁止。
+- [ ] RSpec / Vitest / build / lint が通る。
-### Priority
+# 23. Codex 作業分解案
-| priority | 件数 |
-| --- | ---: |
-| `P1` | 8 |
-| `P2` | 26 |
-| `P3` | 15 |
-| `優先度なし` | 1 |
+## 23.1 P0-1: IndexPolicyResolver
-### Status
+目的:
-| status | 件数 |
-| --- | ---: |
-| `status/blocked` | 8 |
-| `status/in-progress` | 1 |
-| `status/ready` | 39 |
-| `status/review` | 1 |
-| `statusなし` | 1 |
+- `index_policies` を導入し、post/tag/wiki/material/phrase の noindex 判定を backend で一元化する。
-### Area
+成果物:
-| area | 件数 |
-| --- | ---: |
-| `area/backend` | 26 |
-| `area/frontend` | 25 |
+- migration。
+- model。
+- service `IndexPolicyResolver`。
+- post/tag/wiki/material repr に `index_policy` を追加。
+- sitemap generator が API または manifest から除外できるようにする。
+- frontend Helmet が `robots` を反映。
+- tests。
-### Type
+## 23.2 P0-2: 投稿除票
-| type | 件数 |
-| --- | ---: |
-| `type/bug` | 8 |
-| `type/enhancement` | 24 |
-| `type/task` | 17 |
-| `typeなし` | 1 |
+目的:
-## P1 課題
+- posts を論理削除できるようにし、通常面から除外する。
-P1 は公表前または主要 UX に直撃するものとして扱ふ。
+成果物:
-| issue | title | status | area | type |
-| --- | --- | --- | --- | --- |
-| #360 | 上映会で上位タグを持つタグが表示されないバグ | status/ready | area/backend, area/frontend | type/bug |
-| #356 | 天保暦対応 | status/ready | area/frontend | type/enhancement |
-| #353 | 局所記載 (#351) | | | |
-| #344 | 今後の課題整理 | status/ready | | type/task |
-| #334 | posts.thumbnail_base を後からでも変更できるやぅにする | status/ready | area/backend, area/frontend | type/enhancement |
-| #332 | 上位タグの循環を登録できないやぅにする | status/ready | area/backend | type/bug |
-| #306 | 素材管理(#99 の続き) | status/ready | area/backend, area/frontend | type/enhancement |
-| #170 | 別の投稿からタグ・インポート | status/ready | area/backend, area/frontend | type/enhancement |
+- migration。
+- routes/controller actions。
+- admin UI。
+- post_versions discard/restore。
+- search/list/detail/similarity/theatre/gekanator/sitemap 除外。
+- tests。
-## 仕様へ反映した主な issue
+## 23.3 P0-3: Preview API hardening
-| issue | 仕様上の扱ひ |
-| --- | --- |
-| #361 グカネータ公開 | Gekanator は恒久 admin-only ではなく、公表予定機能として定義した |
-| #360 上映会で上位タグを持つタグが表示されないバグ | 上映会のタグ表示は親タグ/継承タグを含むべき既知バグとして記載した |
-| #356 天保暦対応 | 日付表示/旧暦対応の未実装拡張として課題一覧に反映した |
-| #354 `post_versions.tags_json` | 投稿履歴タグ snapshot の構造化予定として記載した |
-| #353/#351 局所記載 | 投稿タグに対する sections 構造の未完機能として扱ふ |
-| #337 Wiki 履歴 | Wiki 履歴は `wiki_versions` を根拠にする方針として課題一覧に反映した |
-| #336 Wiki 本文検索 | Wiki 検索仕様の既知バグとして扱ふ |
-| #334 `posts.thumbnail_base` 変更 | 投稿更新対象の拡張課題として扱ふ |
-| #332 タグ親子循環禁止 | タグ親子 member 開放前の必須制約として記載した |
-| #306 素材管理 | `material_versions` を必要機能として確定した根拠に含めた |
-| #301 備考欄 | `post_remarks`, `post_remark_versions` の将来拡張として扱ふ |
-| #227 限定公開 | 投稿閲覧権限モデルの将来拡張として扱ふ |
-| #123 同定文字 | タグ正規化/検索同定の将来仕様として扱ふ |
+目的:
-## 全 issue 一覧
+- SSRF と resource abuse を潰す。
-| issue | kind | title | priority | status | type | area | milestone | due |
-| --- | --- | --- | --- | --- | --- | --- | --- | --- |
-| #361 | Issue | 【おたのしみ】グカネータ公開 | `P3` | `status/ready` | `type/enhancement` | area/backend, area/frontend | | |
-| #360 | Issue | 上映会で上位タグを持つタグが表示されないバグ | `P1` | `status/ready` | `type/bug` | area/backend, area/frontend | | |
-| #356 | Issue | 天保暦対応 | `P1` | `status/ready` | `type/enhancement` | area/frontend | | |
-| #354 | Issue | `post_versions.tags_json` の作成 → `post_versions.tags` を廃止 | `P2` | `status/ready` | `type/enhancement` | area/backend, area/frontend | | |
-| #353 | PR | 局所記載 (#351) | `P1` | `` | `` | | | |
-| #352 | Issue | サムネつきで手動投稿した際にエラーとなる | `P2` | `status/ready` | `type/bug` | area/backend | | |
-| #351 | Issue | 局所記載のしくみ作り | `` | `status/review` | `type/enhancement` | area/backend, area/frontend | | |
-| #344 | Issue | 今後の課題整理 | `P1` | `status/ready` | `type/task` | | | |
-| #337 | Issue | Wiki 履歴,wiki_versions を根拠にする | `P2` | `status/ready` | `type/enhancement` | area/backend, area/frontend | | |
-| #336 | Issue | Wiki 検索で本文が検索できないバグ | `P2` | `status/ready` | `type/bug` | area/backend | | |
-| #335 | Issue | API の直発行をやめる | `P2` | `status/ready` | `type/task` | area/frontend | | |
-| #334 | Issue | posts.thumbnail_base を後からでも変更できるやぅにする | `P1` | `status/ready` | `type/enhancement` | area/backend, area/frontend | | |
-| #332 | Issue | 上位タグの循環を登録できないやぅにする | `P1` | `status/ready` | `type/bug` | area/backend | | |
-| #322 | Issue | ニジラー情報の履歴管理 | `P2` | `status/ready` | `type/enhancement` | area/backend | | |
-| #320 | Issue | ニコニコ連携画面の TextArea に対するタグ補完 | `P2` | `status/ready` | `type/enhancement` | area/frontend | | |
-| #306 | Issue | 素材管理(#99 の続き) | `P1` | `status/ready` | `type/enhancement` | area/backend, area/frontend | | |
-| #301 | Issue | 備考欄の作成 | `P2` | `status/ready` | `type/enhancement` | area/backend, area/frontend | | |
-| #291 | Issue | “閲覧済” を押しても反映されないバグ | `P2` | `status/ready` | `type/bug` | area/frontend | | |
-| #285 | Issue | モデルに対する Spec | `P3` | `status/ready` | `type/task` | area/backend | | |
-| #283 | Issue | `deerjikists.tag_id` に外部キー制約 | `P2` | `status/ready` | `type/bug` | area/backend | | |
-| #279 | Issue | 検索のワイルドカード | `P2` | `status/in-progress` | `type/enhancement` | area/backend | | |
-| #273 | Issue | タグ補完コンポーネント共通化 | `P2` | `status/ready` | `type/task` | area/frontend | | |
-| #272 | Issue | タグ補完,前方検索を優先表示するが,後方検索も行ふやぅにする(末尾に追記) | `P2` | `status/ready` | `type/enhancement` | area/backend | | |
-| #270 | Issue | 個人用メモを作成可能に | `P2` | `status/ready` | `type/enhancement` | area/backend, area/frontend | | |
-| #268 | Issue | タグのドラッグがスマホのスクロールと干渉する問題 | `P2` | `status/ready` | `type/bug` | area/frontend | | |
-| #266 | Issue | Wiki 新規作成直後に内容表示されないバグ対応 | `P2` | `status/ready` | `type/bug` | area/frontend | | |
-| #242 | Issue | `tags.post_count` にインデクス | `P2` | `status/ready` | `type/task` | area/backend | | |
-| #235 | Issue | フロントのビルド軽量化 | `P2` | `status/ready` | `type/task` | area/frontend | | |
-| #227 | Issue | 限定公開用のしくみ作り | `P3` | `status/ready` | `type/enhancement` | area/backend | | |
-| #225 | Issue | GET /tag_names/name/:name により,タグや Wiki を含む情報を返す | `P2` | `status/ready` | `type/enhancement` | area/backend | | |
-| #221 | Issue | Wiki 排他 | `P2` | `status/ready` | `type/enhancement` | area/backend, area/frontend | | |
-| #213 | Issue | 広場追加の自動チェックボックス,ボタンにして押したタイミングでの取得にする | `P2` | `status/ready` | `type/enhancement` | area/frontend | | |
-| #205 | Issue | 申請フォーム | `P2` | `status/ready` | `type/enhancement` | area/backend | | |
-| #172 | Issue | 上位タグ設定画面 | `P3` | `status/ready` | `type/enhancement` | area/backend, area/frontend | | |
-| #170 | Issue | 別の投稿からタグ・インポート | `P1` | `status/ready` | `type/enhancement` | area/backend, area/frontend | 一般公開 | |
-| #164 | Issue | 【運用】bot 操作タグの投稿を 0 件までに減らす | `P3` | `status/blocked` | `type/task` | | 一般公開 | 2026-06-30 |
-| #163 | Issue | 【運用】bot 操作タグの投稿を 100 件までに減らす | `P3` | `status/blocked` | `type/task` | | | 2026-06-15 |
-| #162 | Issue | 【運用】bot 操作タグの投稿を 200 件までに減らす | `P3` | `status/blocked` | `type/task` | | | 2026-05-31 |
-| #161 | Issue | 【運用】bot 操作タグの投稿を 300 件までに減らす | `P3` | `status/blocked` | `type/task` | | | 2026-05-15 |
-| #160 | Issue | 【運用】bot 操作タグの投稿を 400 件までに減らす | `P3` | `status/blocked` | `type/task` | | | 2026-04-30 |
-| #159 | Issue | 【運用】bot 操作タグの投稿を 500 件までに減らす | `P3` | `status/blocked` | `type/task` | | | 2026-04-15 |
-| #158 | Issue | 【運用】bot 操作タグの投稿を 600 件までに減らす | `P3` | `status/blocked` | `type/task` | | | 2026-03-31 |
-| #152 | Issue | Pixiv の埋込み | `P2` | `status/ready` | `type/enhancement` | area/frontend | | |
-| #151 | Issue | ニジカ投稿局の埋込み | `P2` | `status/ready` | `type/enhancement` | area/frontend | | |
-| #150 | Issue | bilibili の埋込み | `P2` | `status/ready` | `type/enhancement` | area/frontend | | |
-| #149 | Issue | Tiktok の埋込み | `P2` | `status/ready` | `type/enhancement` | area/frontend | | |
-| #147 | Issue | コントローラをサービスに分離 | `P3` | `status/ready` | `type/task` | area/backend | | |
-| #138 | Issue | bot 操作タグのみに限定し,自動で楽曲に関するタグを付与するバッチ作成 | `P3` | `status/ready` | `type/task` | area/backend | | |
-| #123 | Issue | 同定文字の制定 | `P3` | `status/ready` | `type/task` | | 一般公開 | |
-| #122 | Issue | 【運用】bot 操作タグの投稿を 700 件までに減らす | `P3` | `status/blocked` | `type/task` | | | 2026-03-15 |
+成果物:
+- URL validator。
+- DNS resolve 後 IP allow/deny。
+- redirect chain validation。
+- timeout/size/concurrency/rate limit。
+- tests。
-# 次回更新方針
+## 23.4 P1: 局所記載
-1. 本書を実装作業用に分解し、P0/P1 から Codex 向け issue prompt を作る。
-2. `POST /materials` 権限修正、`PostVersionsController` の `parent_posts` 返却、Preview API 防御を先に潰す。
-3. RSpec/Vitest/build/lint を実行し、仕様ではなく検証報告として別章へ追記する。
-4. API レスポンス例を主要 endpoint ごとに追加する。
-5. DB ER 図またはテーブル関係図を別紙化する。
-6. Gitea issue の次回添付または export script により、仕様書を腐らせない運用を決める。
+目的:
-# 外部参照
+- `タグ[開始-終了]` を保存・表示・履歴化する。
-Gekanator AI モデル選定では、2026-06-11 時点の OpenAI 公式ドキュメントを参照した。モデル ID は変動し得るため、仕様では `GEKANATOR_AI_MODEL` による差し替へを必須とする。
+成果物:
-- OpenAI Models: https://developers.openai.com/api/docs/models
-- OpenAI Structured Outputs: https://developers.openai.com/api/docs/guides/structured-outputs
-- OpenAI API Pricing: https://openai.com/api/pricing/
\ No newline at end of file
+- parser。
+- table。
+- post edit UI。
+- duration field。
+- post version snapshot。
+- tests。
+
+# 24. 付録: 現行資材からの主な読み取り
+
+この章は本文仕様の根拠メモである。本文は実装済み/未実装を区別しないが、製造時の迷子防止のため現行傾向を残す。
+
+- Rails routes には posts, tags, wiki, materials, theatres, gekanator, preview, users, deerjikists が揃っている。
+- schema version は `2026_07_05_000000` 相当まで進んでいる。
+- `tags.deprecated_at` と `tag_versions.deprecated_at` は schema に入っている。
+- `tags` には `deprecated_at IS NULL OR category <> 'nico'` の check constraint がある。
+- `posts.video_ms` と `post_versions.video_ms` は schema に入っている。
+- `post_tag_sections` は複合主キー `post_id, tag_id, begin_ms` で存在する。
+- `post_versions` は `discard` / `restore` event_type を許容しているが、posts 本体の除票 column は未整備である。
+- `materials` は `version_no`, source fields, `normalized_source_key` を持つ。
+- `material_versions` は file/source/export snapshot を持つ。
+- `material_sync_sources`, `material_sync_suppressions`, `material_export_items`, `material_import_blocks` は schema 上存在する。
+- `settings` は typed user settings として再構築されている。
+- `user_theme_slots` は light/dark 各 3 slot の JSON token 保存枠として存在する。
+- `wiki_assets` は schema 上存在する。
+- frontend は `react-helmet-async` を使うため route ごとの meta robots 制御が可能である。
+- `ErrorScreen`, theatre, settings, 空 Wiki など一部 route は既に noindex を出す傾向がある。
+- frontend sitemap generator が存在するため、index policy と接続すべきである。
+- DB 上、users と versions と similarities の件数が大きく、掃除・履歴・抑止を後回しにすると運用で詰まる。