---
title: "バッチ設計書"
---

最終更新: 2026-08-09

## 概要

MorningStatusApp は、最新情報を収集するために定期的に実行するバッチと、状況に応じて手動実行するバッチを GitHub Actions で運用している。これに加えて、GitHub Actions を介さず `scripts/console/`・`scripts/patch/` 配下で手動実行するスクリプト群がある。

### ワークフロー一覧

| バッチ名                   | スクリプト                             | ワークフロー                                          |
| -------------------------- | -------------------------------------- | ----------------------------------------------------- |
| メンバー近況同期           | `scripts/workflow/sync-status.ts`     | `.github/workflows/sync-members.yml`                  |
| ディスコグラフィー同期     | `scripts/workflow/sync-discography.ts` | `.github/workflows/sync-discography.yml`             |
| プレイリストリンク同期     | `scripts/workflow/sync-playlist-links.ts` 他 | `.github/workflows/sync-playlist-links.yml`     |
| Genius リンク同期          | `scripts/workflow/sync-genius-links.ts` | `.github/workflows/sync-genius-links.yml`           |
| Instagram 投稿収集         | `scripts/workflow/collect-instagram-posts.ts` | `.github/workflows/collect-instagram-posts.yml` |
| TikTok 投稿収集            | `scripts/workflow/collect-tiktok-posts.ts` | `.github/workflows/collect-tiktok-posts.yml`     |
| TikTok OGチャンネル投稿収集 | `scripts/workflow/collect-tiktok-og.ts` | `.github/workflows/collect-tiktok-og.yml` |
| モーニング女学院放送データ収集 | `scripts/workflow/collect-onair-morning.ts` | `.github/workflows/collect-onair-morning.yml` |
| ヤングタウン土曜日収集     | `scripts/workflow/collect-onair-yando.ts` | `.github/workflows/collect-onair-yando.yml`        |
| メイボンソワ選曲データ収集 | `scripts/workflow/collect-bonsoir-songs.ts` | `.github/workflows/collect-bonsoir-songs.yml` |
| デイリーダイジェスト配信   | `scripts/workflow/send-daily-digest.ts` | `.github/workflows/send-daily-digest.yml`           |
| イベント同期               | `scripts/workflow/sync-events.ts`    | `.github/workflows/sync-events.yml`                   |
| YouTube 公式チャンネル投稿収集 | `scripts/workflow/collect-youtube-official.ts` | `.github/workflows/collect-youtube-official.yml` |
| YouTube OG チャンネル投稿収集 | `scripts/workflow/collect-youtube-og.ts` | `.github/workflows/collect-youtube-og.yml` |
| メンバー関係性マップ 動的エッジ同期 | `scripts/workflow/sync-relationships.ts` | `.github/workflows/sync-events.yml`（イベント同期の後続ステップ） |

### それ以外のスクリプト一覧

`scripts/console/`・`scripts/patch/` 配下のスクリプト。フル仕様（O1〜O3）で記載する3件を除き、[それ以外のスクリプト](#それ以外のスクリプト) セクションの一覧表に概要のみ記載する。

| スクリプト | 概要 | 分類 |
| --- | --- | --- |
| `scripts/console/build-comparison-index.ts` | 曲対比インデックス生成（詳細: O1） | 定常運用 |
| `scripts/patch/patch-delete-non-song-tracks.ts` | 非楽曲トラック削除パッチ（詳細: O2、#1057） | 一回限りパッチ（実行済み） |
| `scripts/console/seed-member-relationships.ts` | メンバー関係性マップ 静的エッジシード（詳細: O3） | 定常運用 |

---

## ワークフロー

### W1. メンバー近況同期バッチ

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | 毎日 JST 20:30（UTC 11:30）               |
| 手動実行     | GitHub Actions の workflow_dispatch で可能 |

> OGメンバーの同期は `OG_SYNC_WEEKDAYS` 環境変数で実行曜日を制限できる。
> デフォルト（空文字・未設定）は月曜のみ（曜日番号: `1`）。
> 曜日判定（`shouldSyncOG`）は `scripts/lib/og-sync.ts` の共通モジュールで、
> YouTube OG チャンネル投稿収集バッチ（W8）と共用している（#1104）。
>
> 公式SNS未登録（または全 active:false）のメンバーに対する Exa 一般Web検索は、
> `WEB_SEARCH_SYNC_WEEKDAYS` 環境変数で実行曜日を制限できる。
> デフォルト（空文字・未設定）は月曜のみ（曜日番号: `1`）。
> 曜日判定（`shouldSearchWebForNoSns`）は `scripts/lib/web-search-schedule.ts` に実装している（#1307）。

#### 処理フロー

```mermaid
flowchart TD
  A[Vercel Blob からメンバーデータ取得] --> B{メンバーループ}
  B --> C{Active or OG?}
  C -- Active --> D[毎日実行]
  C -- OG --> E{OG_SYNC_WEEKDAYS に含まれる曜日?}
  E -- Yes --> D
  E -- No --> SKIP[スキップ]
  D --> F{officialSns に Ameba/Instagram の active エントリあり?}
  F -- Yes --> G["Ameba RSS → Instagram Blob チェック（#1307: 排他）"]
  G --> H{更新あり?}
  H -- Yes --> I[近況を蓄積]
  H -- No --> J["SNS更新なし / snsCheck: ok + hasRecentUpdate=false（その他SNS・Web検索へのフォールバックなし）"]
  F -- No --> F2{その他SNSの active エントリあり?}
  F2 -- Yes --> G2[SNS ドメイン限定検索（Exa）]
  G2 --> H2{日本語コンテンツ取得できた?}
  H2 -- Yes --> I
  H2 -- No --> J
  F2 -- No / 全 inactive --> K{WEB_SEARCH_SYNC_WEEKDAYS に含まれる曜日?}
  K -- No --> N["Web検索スキップ / snsCheck: skipped + hasRecentUpdate=false（#1307）"]
  K -- Yes --> K2[Web 全体検索（Exa）]
  K2 --> L{日本語コンテンツ取得できた?}
  L -- Yes --> M[近況を蓄積 / snsCheck: skipped + hasRecentUpdate=true]
  L -- No --> N
  I --> O[latestStatus・statusHistory に反映]
  M --> O
  J --> P[次のメンバーへ]
  N --> P
  O --> P
  P --> Q{全メンバー完了?}
  Q -- No --> B
  Q -- Yes --> R[members.json を Vercel Blob に書き戻し]
  R --> U[終了]
```

#### Ameba RSS 解析（#1328）

officialSns に active な Ameba Blog エントリを持つメンバーは、Exa 検索を使わず Ameba の RSS フィードから直接近況を取得する（エンドポイント: `https://rssblog.ameba.jp/{amebaId}/rss20.xml`）。

RSS抽出仕様（CDATA改行対応・content=タイトルのみ格納・グループブログの誤帰属防止・新着蓄積・重複防止・pubDate変換等）の詳細は「[Amebaブログ機能 データ設計書](/design/sns/ameba-data-design)」§「RSS取得仕様」を参照（#1367）。

#### 検索エンジン

- **Exa Search API**（`exa-js` SDK）
- `SEARCH_API_KEY` 未設定の場合はドライラン（検索なし・Blob 書き込みなし）

##### Exa Search API の料金体系（#1307）

- 従量課金制: Search 約 $7 / 1,000 リクエスト（出典: https://exa.ai/pricing）
- 新規登録時に $10 分の無料クレジットを付与（一度のみ、出典: https://exa.ai/docs/reference/billing）
- 支払い方法登録済みの場合、毎月 $7 分の無料クレジットを付与（当月末で失効・繰越不可、出典: https://exa.ai/docs/reference/billing）

> 2026-07-10 にクレジット枯渇で検索が失敗する事象が発生したため、公式SNS登録状況に応じて検索範囲を排他化し、
> 公式SNS未登録メンバーへの一般Web検索を `WEB_SEARCH_SYNC_WEEKDAYS` で週1回に制限している（#1307）。

#### 日本語判定

ひらがな（U+3040-U+309F）の出現率が `HIRAGANA_THRESHOLD`（0.08 = 8%）以上の場合に日本語と判定。
日本語でないコンテンツは `statusHistory` に蓄積しない。

#### SNS チェック（snsCheck）の更新ロジック

| 条件                                        | status  | hasRecentUpdate |
| ------------------------------------------- | ------- | --------------- |
| SNS 検索成功 + 公式 SNS ソースあり           | ok      | true            |
| SNS 検索成功 + 公式 SNS ソースなし / 更新なし | ok     | false           |
| officialSns 未設定 + Web 検索でコンテンツあり | skipped | true            |
| officialSns 未設定 + Web 検索でコンテンツなし | skipped | false           |
| officialSns 全 active:false + Web 検索でコンテンツあり | skipped | true   |
| officialSns 全 active:false + Web 検索でコンテンツなし | skipped | false  |
| snsCheck データなし（既存データ）            | unknown | false           |

> **注意**: バッチは `active: false` への自動変更を行わない（#326）。SNS 検索で日本語コンテンツが取れなかった場合は「更新なし」として扱い、`active` は変更しない。

#### 環境変数

| 変数名              | 必須 | 説明                                              |
| ------------------- | ---- | ------------------------------------------------- |
| `SEARCH_API_KEY`    | ○    | Exa Search API キー（未設定でドライラン）          |
| `BLOB_READ_WRITE_TOKEN` | ○ | Vercel Blob 読み書きトークン                    |
| `MEMBERS_BLOB_URL`  | ○    | Vercel Blob のメンバーデータ URL                  |
| `OG_SYNC_WEEKDAYS`  | -    | OG 同期する曜日番号（カンマ区切り、デフォルト: `1`）。Repository variable（非機密な設定値のため、#1347）|
| `WEB_SEARCH_SYNC_WEEKDAYS` | - | 公式SNS未登録メンバーの一般Web検索（Exa）を実行する曜日番号（カンマ区切り、デフォルト: `1`）（#1307）。Repository variable（非機密な設定値のため、#1347） |
| `GITHUB_EVENT_NAME` | ○ | GitHub Actions がすべてのステップに自動的に注入するデフォルト環境変数で、トリガー種別（`schedule`/`workflow_dispatch`等）を表す。Ameba同期ステータスの定期実行判定に使用（#1387）。ワークフロー側での明示的な受け渡しは不要 |

> **注意**: 秘匿すべき値（APIキー・トークン等）は Repository secrets に統一すること（Environment secrets は `environment:` 指定がないと注入されない）。曜日番号等の非機密な設定値は Repository variables に登録する（#1347）。

#### Vercel Blob への書き込み

```typescript
await put(MEMBERS_BLOB_FILENAME, JSON.stringify({ members }), {
  access: 'public',
  addRandomSuffix: false,   // 固定 URL での上書き保存
  allowOverwrite: true,     // 既存ファイルを上書き
  cacheControlMaxAge: 60,   // CDNエッジキャッシュを最小値に短縮（未指定だとデフォルト1ヶ月キャッシュされる、#1442）
  token: process.env.BLOB_READ_WRITE_TOKEN,
});
```

`members.json`・デイリーダイジェスト（`daily-digest/{日付}/blog.json`。データ構造はW14参照）に続けて、Topページ新着投稿セクションのAmeba新着ウィンドウ判定に使う `ameba/sync-status.json` も書き込む。詳細（`AmebaSyncStatus`型・定期実行/手動実行での更新ロジック）は「[Amebaブログ機能 データ設計書](/design/sns/ameba-data-design)」§「新着表示の判定基準」を参照（#1387）。

---

### W2. ディスコグラフィー同期バッチ

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | なし（手動実行のみ）                      |
| 手動実行     | GitHub Actions の workflow_dispatch で可能（`refetch_artist_credits` 入力に `1` を指定すると artist 未設定トラックを強制再取得） |

#### 処理フロー

```
1. Vercel Blob（`UNITS_BLOB_URL`）からユニット一覧を読み込み、MBID を持つユニットの重複排除済み MBID 一覧を取得
   → MBID を持つユニットが0件の場合は処理を中断
2. Neon DB から現在のリリースデータを取得（`getReleases()`、フェイルセーフ）
3. 各 MBID に対して MusicBrainz API からリリースグループを取得し、結果を結合
   → 全 MBID で取得0件の場合は既存データを維持して終了
4. リリースデータを正規化・重複排除
5. トラックリストを付与（既存キャッシュ再利用 / MusicBrainz から新規取得）
   → artist-credit から artist・artistMbid を取得
   → REFETCH_MISSING_ARTIST_CREDITS=1 の場合は artist 未設定トラックを持つリリースを強制再取得
   → 非楽曲トラックの自動除外（isNonSongTrack）: recording.video=true の映像トラック、インスト・カラオケ・MC・Opening・VTR 等
   → シングル（format: 'single'）は全エディション（Type A・Type B・限定盤等）のトラックを取得して recording UUID でマージし、
     エディション固有のカップリング曲（例: Nature is good!）が漏れなく取り込まれるようにする
   → アルバム・ライブアルバム等は最初のエディションのみ取得（#1058 で調査・判断予定）
6. Track.artistMbid / Track.artist をもとにユニット照合（assignUnitIds）
   → 照合優先度: MBID 一致 → 名前・年ベース（フォールバック）
7. 全トラックが同一 unitId のリリースを Unit.releaseIds に自動集約（assignReleaseIdsToUnits）
8. Track 単位の YouTube リンクを付与（`YOUTUBE_API_KEY` 設定時のみ）
   → プレイリスト掲載曲 ID を取得し、優先処理対象として `syncTrackLinks` に渡す
   → `syncTrackLinks`: YouTube MV URL（`youtube`）・YouTube Music URL（`youtube-music`）を track に付与
   → `syncTrackShortLinks`: シングル曲の YouTube Shorts URL（`youtube-short`）を付与（#774）
   → 1 回の実行あたりの上限: `YOUTUBE_MAX_CALLS_PER_RUN`（100 件）
   → リリース単位の YouTube MV URL 取得は廃止（Neon に保存先がなくクォータを空振りするため、#1123）
9. data/inputs/manual-releases.json から手動登録リリースを読み込み、MusicBrainz 取得分とマージ（mergeManualReleases）
   → ID 重複チェックを行い、既存リリースと ID が衝突する手動登録はスキップ
10. リリースデータ（MusicBrainz 取得分 + 手動登録分）を Neon DB に書き込み（`saveDiscographyToNeon`）
    → `release` テーブル: `createMany(skipDuplicates: true)`
    → `track` テーブル: `upsert` per track
    → `track_link` テーブル: `deleteMany` + `createMany` per track
11. units.json（releaseIds 更新済み）を Vercel Blob（`UNITS_BLOB_URL`）に書き戻し
12. 同期ステータス（`discography-status.json`）を Vercel Blob に書き戻し
    → リリース件数・YouTube 取得対象数・取得済み数・Shorts 取得対象数・Shorts 取得済み数を日次スナップショットとして保存（最大 30 件）
```

#### MusicBrainz API

- エンドポイント: `https://musicbrainz.org/ws/2/`
- 対象: Vercel Blob 上のユニット一覧の `mbid` フィールドを持つ全ユニット（重複排除済み MBID 一覧）
- レート制限: 秒間1リクエスト未満に制御（429 エラー時は指数バックオフで再試行）
- トラックリスト取得時に `inc=recordings+artist-credits` を指定し、`Track.artist` および `Track.artistMbid` を取得
- `recording.video` フラグが `true` のトラックは映像として除外（#1055）
- シングルはリリースグループ内の全リリース ID を取得し、全エディションをマージする（#1055）

#### ユニット照合（assignUnitIds）

各トラックの `artist` / `artistMbid` をもとに `units.json` と照合し `Track.unitId` を設定する。

| 優先順位 | 照合条件 |
| --- | --- |
| 1 | `Track.artistMbid === Unit.mbid`（MBID 一致・確実） |
| 2 | 名前完全一致 / ベース名 + 活動期間での絞り込み（フォールバック） |

既存の `unitId`（手動設定）は上書きしない。

#### releaseIds 自動集約（assignReleaseIdsToUnits）

全トラックが同一 `unitId` を持つリリース（コンピレーション等の混在リリースを除く）を、
対応するユニットの `releaseIds` に自動追加する。既存の `releaseIds` は保持し重複は除去する。

#### リリース正規化・重複排除

- `releaseDate` の空白・ゼロ詰め正規化（`normalizeDate()`）
- タイトル + 正規化リリース日をキーとした重複排除（`deduplicateReleases()`）

#### 環境変数

| 変数名                           | 必須 | 説明                                                              |
| -------------------------------- | ---- | ----------------------------------------------------------------- |
| `DATABASE_URL`                   | ○    | Neon PostgreSQL 接続文字列（未設定でドライラン）                  |
| `BLOB_READ_WRITE_TOKEN`          | ○    | Vercel Blob 読み書きトークン（units.json・同期ステータス書き込みに必要） |
| `DISCOGRAPHY_STATUS_BLOB_URL`    | ○    | Vercel Blob の同期ステータス保存先 URL（`/sync-status` ページで参照） |
| `YOUTUBE_API_KEY`                | -    | YouTube Data API v3 キー（未設定の場合は YouTube リンク同期をスキップ） |
| `PLAYLISTS_BLOB_URL`             | -    | Vercel Blob のプレイリストデータ URL（設定するとプレイリスト掲載曲を優先処理） |
| `UNITS_BLOB_URL`                 | ○    | Vercel Blob のユニットデータ URL（ユニット照合・releaseIds 自動集約に使用） |
| `REFETCH_MISSING_ARTIST_CREDITS` | -    | `1` に設定すると artist 未設定トラックを持つリリースを強制再取得  |
| `REFETCH_ALL_SINGLES`            | -    | `1` に設定すると全シングルの全エディションを強制再取得し映像・非楽曲トラックを削除する。`workflow_dispatch` の入力には未配線のため、ローカル実行（`.env.local` 等）でのみ指定可能 |

---

### W3. プレイリスト集計・インデックス生成スクリプト

#### 概要

`playlists.json` が更新された際、メンバー詳細やリリース詳細での高速なデータ表示を可能にするための派生インデックス JSON を生成し、Vercel Blob に保存する。

#### 実行タイミング

手動（プレイリスト更新時）。`bun run build-playlist-index` を実行。

#### 選曲リリースの自動解決（Resolution）ロジック

プレイリストに登録された楽曲（`trackId`）が複数のリリースに含まれる場合、以下の優先順位で最適な `releaseId` を自動選択し、インデックスに反映する。

1. **形式優先順位**: `single` > `ep` > `album` > `other`
   - プレイリスト側でアルバムが指定されていても、シングルが存在すればシングル盤を優先する。
2. **初出優先順位**: 同一形式（例：複数のシングル）に収録されている場合、`releaseDate` が最も古いものを優先する。

#### 環境変数

| 変数名                  | 必須 | 説明                                              |
| ----------------------- | ---- | ------------------------------------------------- |
| `BLOB_READ_WRITE_TOKEN` | ○    | Vercel Blob 読み書きトークン                      |
| `PLAYLISTS_BLOB_URL`    | ○    | `playlists.json` の URL                           |
| `RELEASES_BLOB_URL`     | ○    | `releases.json` の URL（リリース解決用）          |
| `MEMBERS_BLOB_URL`      | -    | `members.json` の URL（インデックス内の名前解決用）|

---

### W4. Instagram 投稿収集バッチ

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | 毎日 JST 21:30（UTC 12:30）               |
| 手動実行     | GitHub Actions の workflow_dispatch で可能 |

#### 処理フロー

```
1. auth_state.json を読み込み、Playwright ブラウザコンテキストを復元する
2. Instagram トップページにアクセスし、セッション有効性を確認する
   → ログインページへのリダイレクトを検知した場合は sessionStatus: 'expired' を Blob に書き込み終了（非ゼロ終了コード）
3. Vercel Blob からメンバーデータを取得する
   - OGメンバーは `OG_SYNC_WEEKDAYS` で指定した曜日のみ収集対象とする（Activeメンバーは常に対象。メンバー近況同期バッチ・YouTube OGチャンネル収集バッチと同一ルール、#1297）
   - officialSns に active な instagram.com URL を持つメンバーを対象とする
4. 各対象メンバーのプロフィールページにアクセスし、投稿 URL 一覧を収集する
5. 既存の posts.json と比較し、新規投稿のみを取得対象とする（差分検出）
6. 各新規投稿ページにアクセスし、メタデータを取得する
   - og:description からキャプションの1行目を取得する
7. サムネイル URL として `/p/{postId}/media/?size=m` 形式のリダイレクト URL を記録する（Vercel Blob への画像アップロードは行わない）
   - 収集時のサムネイル URL 有効性検証は行わない（Instagram が非ブラウザの HTTP リクエストを 429 で弾くため・#1114）。表示時に取得失敗した投稿はカード非表示で対応する（#1094）
8. instagram/{memberId}/posts.json を更新する（既存投稿を保持しつつ新規を先頭に追加）
9. instagram/daily-new-posts.json を今回の実行で新たに取得した投稿一覧で上書き保存する
10. instagram/sync-status.json を更新する
11. daily-digest/{日付}/instagram.json に実メンバー別の新着件数を書き込む（データ構造はW14参照）
12. `OFFICIAL_INSTAGRAM_ACCOUNTS`（`constants/instagram-official-accounts.ts`）の各アカウント（モーニング娘。公式・ミニモちゃん）について、daily-digest/{日付}/instagram-official.json にアカウント別の新着件数を書き込む（データ構造はW14参照）
```

#### Secret 運用手順

`INSTAGRAM_AUTH_STATE` は Playwright の storageState（Cookie等を含む JSON）。
Session 失効時（ワークフロー失敗時）に以下の手順で再登録する。

```bash
# 0. playwright のブラウザをインストール（初回のみ）
bunx playwright install chromium

# 1. ヘッドフルブラウザで Instagram にログインし、認証状態を保存する
bun run instagram:auth

# 2. 生成された auth_state.json を GitHub Actions Secret に登録する
gh secret set INSTAGRAM_AUTH_STATE < auth_state.json

# 3. auth_state.json を削除する（機密情報のため必ず削除）
rm auth_state.json
```

> **注意**: Session の有効期間は数週間〜数ヶ月。ワークフローが認証エラーで失敗したらリポジトリオーナーにメール通知が届く（GitHub Actions の失敗通知）。

#### 環境変数

| 変数名                  | 必須 | 説明                                                    |
| ----------------------- | ---- | ------------------------------------------------------- |
| `BLOB_READ_WRITE_TOKEN` | ○    | Vercel Blob 読み書きトークン                            |
| `MEMBERS_BLOB_URL`      | ○    | Vercel Blob のメンバーデータ URL（既存）                |
| `INSTAGRAM_AUTH_STATE`  | ○    | Playwright 認証状態（JSON文字列）。Repository secret    |
| `OG_SYNC_WEEKDAYS`      | -    | OG メンバーを収集する曜日番号（カンマ区切り、デフォルト: `1`）。Repository variable（非機密な設定値のため、#1347） |

> **注意**: 秘匿すべき値（APIキー・トークン等）は Repository secrets に統一すること（Environment secrets は `environment:` 指定がないと注入されない）。曜日番号等の非機密な設定値は Repository variables に登録する（#1347）。

#### Blob 書き込みパス

| パス                                | 内容                           | タイミング         |
| ----------------------------------- | ------------------------------ | ------------------ |
| `instagram/{memberId}/posts.json`   | メンバー単位の全投稿メタデータ | 毎回（差分追加）   |
| `instagram/sync-status.json`        | 同期実行状態                   | 毎回上書き         |
| `instagram/daily-new-posts.json`    | 当日新規取得投稿一覧           | 毎回上書き         |
| `daily-digest/{日付}/instagram.json` | 実メンバー別新着件数サマリー（デイリーダイジェスト配信用、データ構造はW14参照） | 毎回（同日は件数を加算マージ） |
| `daily-digest/{日付}/instagram-official.json` | 公式アカウント別新着件数サマリー（デイリーダイジェスト配信用、データ構造はW14参照） | 毎回（同日は件数を加算マージ） |

> **注意**: `instagram/{memberId}/{postId}.jpg` への画像保存は廃止された。

---

### W5. TikTok 投稿収集バッチ

> 本節は、実装（RapidAPI + Neon）と乖離した旧アーキテクチャ（Playwright + Vercel Blob、ストレージがNeon移行前の#888当時の記述）のまま放置されていたため、#1489 対応時に現行実装に合わせて全面的に書き直した。

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | 毎日 JST 21:00（UTC 12:00）               |
| 手動実行     | GitHub Actions の workflow_dispatch で可能 |

#### 処理フロー

```
1. Neon DB の tiktok_posts テーブルから既存 postId 一覧を取得する（postId はアカウント・チャンネル種別を問わずグローバルに一意なため、絞り込まず全件取得する）
2. `OFFICIAL_TIKTOK_ACCOUNTS`（`constants/tiktok-official-accounts.ts`）の各アカウント（モーニング娘。公式・ミニモちゃん）について、RapidAPI（`tiktok-scraper7.p.rapidapi.com`）の `/user/posts` を最新ページから呼び出し、既存 postId に到達するかページ上限（`MAX_PAGES`=10）に達するまで取得する
3. 取得した投稿を TikTokPost（channelType='official'、accountId 付き）に変換する
4. Vercel Blob からメンバーデータを取得し、キャプションのハッシュタグからメンバーへの紐付けを行う（`attributeTikTokPostToMembers`、詳細は次項）
5. 新規投稿のみを tiktok_posts に保存する（差分追加・skipDuplicates）
6. tiktok_sync_status（Neon、id=1固定の1行）を更新する。`lastSyncedAt`・`syncStatus`・`capturedMembersCount`・`totalTargetMembersCount`（`/sync-status` 画面向けの公式収集ステータス表示用）を毎回更新する。加えて、TOP新着投稿セクションの新着ウィンドウ判定用の `officialLastSyncedAt`/`officialLastAutoSyncedAt`/`officialPreviousAutoSyncedAt` も更新する（W15とは行を共有するが、official用カラムのみを更新しW15のog用カラムには触れない）。ウィンドウ判定ロジックの詳細は「[TikTok投稿機能 データ設計書](/design/sns/tiktok-data-design)」§4「新着表示の判定基準」を参照（#1489）
7. 投稿キャプションと楽曲タイトルを照合し、シングル曲へのTrackLink（`type='tiktok'`）を自動生成・修正する（`fixTikTokTrackLinks`・`syncTikTokTrackLinks`）
8. daily-digest/{日付}/tiktok.json に実メンバー別の新着件数を書き込む（データ構造はW14参照）
9. daily-digest/{日付}/tiktok-official.json にアカウント別の新着件数を書き込む（データ構造はW14参照）
```

#### キャプションベースのメンバー属性付与

メンバーへの紐付けは、収集時（本バッチ）にキャプションのハッシュタグから判定し、`mentionedMemberIds` としてDBに保存する（`lib/tiktok-attribution.ts`）。表示側は保存済みの値をそのまま用いる（アプリ実行時の都度計算ではない）。判定対象はキャプション内のハッシュタグ（`#` で始まるトークン）のみで、本文中の言及はハッシュタグに含まれていなければマッチしない（#1412）。

- ハッシュタグ内のフルネーム（正規化後）が部分一致すればマッチ
- フルネームが一致しない場合、苗字または名前（2文字以上のもの）が部分一致すればマッチ
- 異体字（﨑/崎・髙/高・栁/柳等）を正規化してからマッチング

#### 環境変数

| 変数名                  | 必須 | 説明                                   |
| ----------------------- | ---- | -------------------------------------- |
| `RAPIDAPI_KEY`          | ○    | RapidAPI キー（未設定時はエラーで終了） |
| `DATABASE_URL`          | ○    | Neon PostgreSQL 接続文字列（未設定時はエラーで終了） |
| `MEMBERS_BLOB_URL`      | -    | Vercel Blob のメンバーデータ URL（未設定時はハッシュタグ属性付与をスキップ） |
| `BLOB_READ_WRITE_TOKEN` | ○    | Vercel Blob 読み書きトークン（デイリーダイジェスト用サマリーの書き込みに使用、W14） |

---

### W6. イベント同期バッチ

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | 毎日 JST 03:00（UTC 18:00）               |
| 手動実行     | GitHub Actions の workflow_dispatch で可能 |

#### 処理フロー

```
1. Vercel Blob からメンバーデータを取得する（MEMBERS_BLOB_URL）
   → 未設定の場合はメンバーイベントをスキップ（ドライラン）
2. Neon DB からリリースデータを取得する（releases テーブル）
3. Neon DB からライブデータを取得する（lives テーブル、ツアー・参加メンバー含む）
4. Neon DB からラジオ放送回データを取得する（radio_episodes テーブル、番組・出演メンバー含む）
5. 各ソースを events 形式に変換する
   - release  : releaseDate を Date に変換（YYYY-MM-DD / YYYY-MM / YYYY 形式に対応）
   - join     : profile.joinDate を加入イベントに変換
   - graduate : profile.gradDate を卒業イベントに変換（gradDate が null のメンバーはスキップ）
   - live     : ライブ公演を live イベントに変換（title が null の場合はツアータイトルにフォールバック）
   - media    : ラジオ放送回を media イベントに変換
6. 全イベントを events テーブルへ upsert する（source + sourceId で一意判定）
   → 既存レコードあり → UPDATE（date / type / title / members 等を更新）
   → 既存レコードなし → INSERT
7. 同期完了ログを出力する（created / updated / errors 件数）
```

#### upsert キー規則

| source   | type      | sourceId                   |
| -------- | --------- | -------------------------- |
| release  | release   | `releaseId`                |
| member   | join      | `memberId`                 |
| member   | graduate  | `${memberId}:graduate`     |
| live     | live      | `liveId`                   |
| radio    | media     | `episodeId`                |

#### 環境変数

| 変数名              | 必須 | 説明                                                    |
| ------------------- | ---- | ------------------------------------------------------- |
| `DATABASE_URL`      | ○    | Neon PostgreSQL 接続文字列                              |
| `MEMBERS_BLOB_URL`  | ○    | Vercel Blob のメンバーデータ URL（未設定でメンバーイベントをスキップ）|

> **注意**: 全シークレットは Repository secrets に統一すること（Environment secrets は `environment:` 指定がないと注入されない）。

#### ドライラン動作

| スクリプト              | ドライラン条件          | 動作                       |
| ----------------------- | ----------------------- | -------------------------- |
| sync-status.ts          | `SEARCH_API_KEY` 未設定 | 検索せず、Blob 書き込みなし |
| sync-status.ts（Instagram） | `INSTAGRAM_ACCESS_TOKEN` または `INSTAGRAM_PAGE_ID` 未設定 | Instagram 取得をスキップ（近況同期は通常通り実行） |
| sync-discography.ts     | `DATABASE_URL` 未設定   | MusicBrainz 取得のみ、Neon 書き込みなし（units.json・ステータス Blob 書き込みも行わない） |
| sync-genius-links.ts    | `DATABASE_URL` 未設定   | Genius API 取得のみ、Neon 書き込みなし |
| sync-playlist-links.ts  | `DATABASE_URL` 未設定   | YouTube API 取得のみ、Neon 書き込みなし |
| sync-events.ts          | `MEMBERS_BLOB_URL` 未設定 | メンバーイベントをスキップ（Releases / Lives / Radio は通常通り同期） |

#### フェイルセーフ

- Blob 取得失敗: 空配列を使用して処理を継続
- 外部 API エラー: `console.error` でログ出力後、次のメンバーの処理を継続
- MusicBrainz 0件取得: 既存データを維持して終了

#### GitHub Actions ワークフロー共通設定

```yaml
env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true  # Node.js 24 に早期移行（#310）
runs-on: ubuntu-latest
```

---

### W7. YouTube 公式チャンネル投稿収集バッチ

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | 毎日 JST 22:30（UTC 13:30）               |
| 手動実行     | GitHub Actions の workflow_dispatch で可能 |

#### 処理フロー

```
1. Neon DB の youtube_posts テーブルから channelType='official' の既存 videoId 一覧を取得する
2. ハードコードされた公式チャンネルハンドル（@morningmusume）ごとに以下を実行する
   - channels API（forHandle）でアップロードプレイリスト ID を取得する（fetchPlaylistId）
     → 取得できない場合は当該チャンネルをスキップ
   - playlistItems API で最新動画を取得する（fetchVideosFromPlaylist、最大 50 件）
   - 取得した動画を YoutubePost（channelType='official'）に変換する
3. 既存 videoId を除外した新規投稿のみを youtube_posts に保存する（差分追加・skipDuplicates）
4. youtube_sync_status（Neon、id=1固定の1行。W8と行は共有するが、official用カラムのみを更新しW8のog用カラムには触れない）を更新する。定期実行/手動実行での更新ロジックの詳細は「[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design)」§6.2「新着表示の判定基準」を参照（#1450）
5. daily-digest/{日付}/youtube-official.json にアカウント別（`@morningmusume`）の新規取得件数を書き込む（データ構造はW14参照）
```

#### エラーハンドリング

| エラー種別 | 動作 |
| --- | --- |
| クォータ超過（`YouTubeQuotaExceededError`） | 処理全体を中断する（クォータ管理方針: youtube-data-design.md 参照） |
| ネットワーク例外（DNS 解決失敗・接続タイムアウト等） | 当該チャンネルのみスキップしてログ出力後、後続チャンネルの処理を続行する |
| API HTTP エラー（404 / 500 等） | `fetchPlaylistId` / `fetchVideosFromPlaylist` が null / 空配列を返すため、当該チャンネルをスキップして続行する |

#### 環境変数

| 変数名            | 必須 | 説明                                          |
| ----------------- | ---- | --------------------------------------------- |
| `YOUTUBE_API_KEY` | ○    | YouTube Data API v3 キー（未設定時はエラーで終了） |
| `DATABASE_URL`    | ○    | Neon PostgreSQL 接続文字列（未設定時はエラーで終了） |
| `BLOB_READ_WRITE_TOKEN` | ○ | Vercel Blob 読み書きトークン（デイリーダイジェスト用サマリーの書き込みに使用、W14） |

> **注意**: 全シークレットは Repository secrets に統一すること（Environment secrets は `environment:` 指定がないと注入されない）。

---

### W8. YouTube OG チャンネル投稿収集バッチ

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | 毎日 JST 22:45（UTC 13:45）               |
| 手動実行     | GitHub Actions の workflow_dispatch で可能 |

> ワークフローは毎日起動するが、OG の収集は `OG_SYNC_WEEKDAYS` 環境変数で指定した曜日のみ実行する
> （スクリプト内の `shouldSyncOG`（`scripts/lib/og-sync.ts`）で判定。メンバー近況同期バッチと同一ルール）。
> デフォルト（空文字・未設定）は月曜のみ（曜日番号: `1`）。

#### 処理フロー

```
1. OG_SYNC_WEEKDAYS の対象曜日でなければ何もせず正常終了する
2. Vercel Blob からメンバーデータを取得し、youtubeChannel を持つ OG メンバー（status が Active 以外）を対象とする
3. 各対象メンバーについて playlistItems API で最新動画を取得する（fetchVideosFromPlaylist、最大 5 件）
   - playlistId は members.json の youtubeChannel に保存済みのため channels API は呼ばない
4. 取得した動画を YoutubePost（channelType='og'・memberId 付き）に変換する
5. メンバーごとに、今回取得分に含まれない古い投稿を youtube_posts から削除し、新規投稿を保存する（最新 5 件のみ保持）
6. youtube_sync_status（Neon、id=1固定の1行。W7と行は共有するが、og用カラムのみを更新しW7のofficial用カラムには触れない）を更新する。定期実行/手動実行での更新ロジックの詳細は「[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design)」§6.2「新着表示の判定基準」を参照（#1450）
7. daily-digest/{日付}/youtube-og.json にメンバー別の新着件数を書き込む（データ構造はW14参照）
```

#### エラーハンドリング

| エラー種別 | 動作 |
| --- | --- |
| クォータ超過（`YouTubeQuotaExceededError`） | 処理全体を中断する（クォータ管理方針: youtube-data-design.md 参照） |
| ネットワーク例外（DNS 解決失敗・接続タイムアウト等） | 当該メンバーのみスキップしてログ出力後、後続メンバーの処理を続行する |
| API HTTP エラー（404 / 500 等） | `fetchVideosFromPlaylist` が空配列を返すため、当該メンバーの削除・保存をスキップして続行する（既存データ保持） |
| 全チャンネル失敗（取得成功 0 人かつ失敗 1 人以上） | 非ゼロ終了して GitHub Actions の失敗通知に乗せる（PR #1116 レビュー申し送りより採用） |

#### 環境変数

| 変数名             | 必須 | 説明                                          |
| ------------------ | ---- | --------------------------------------------- |
| `YOUTUBE_API_KEY`  | ○    | YouTube Data API v3 キー（未設定時はエラーで終了） |
| `DATABASE_URL`     | ○    | Neon PostgreSQL 接続文字列（未設定時はエラーで終了） |
| `MEMBERS_BLOB_URL` | ○    | Vercel Blob のメンバーデータ URL（未設定時はエラーで終了） |
| `BLOB_READ_WRITE_TOKEN` | ○ | Vercel Blob 読み書きトークン（デイリーダイジェスト用サマリーの書き込みに使用、W14） |
| `OG_SYNC_WEEKDAYS` | -    | OG 収集する曜日番号（カンマ区切り、デフォルト: `1`）。Repository variable（非機密な設定値のため、#1347） |

> **注意**: 秘匿すべき値（APIキー・トークン等）は Repository secrets に統一すること（Environment secrets は `environment:` 指定がないと注入されない）。曜日番号等の非機密な設定値は Repository variables に登録する（#1347）。

---

### W9. メンバー関係性マップ 動的エッジ同期バッチ

#### 概要

`member_relationships` テーブルのうち、ライブ・ラジオ・イベント共演から動的に導出されるエッジ（`live-coappearance` / `radio-coappearance` / `event-coappearance`）を再集計する。イベント同期バッチ（W6）の後続ステップとして同一ワークフロー（`sync-events.yml`）内で実行される。

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | 毎日 JST 03:00（UTC 18:00）。イベント同期バッチの直後に実行 |
| 手動実行     | GitHub Actions の workflow_dispatch で可能（Sync Events ワークフロー） |

#### 処理フロー

```
1. ライブ共演（live-coappearance）を集計する
   → member_lives → lives → tours（LEFT JOIN）を辿り、同一 liveId を共有するメンバーペアごとの共演回数を集計
   → label は Tour.title →（null時）Live.title →（両方null時）固定文言「ライブ共演」の順でフォールバック
2. ラジオ共演（radio-coappearance）を集計する
   → radio_members → radio_episodes → radio_shows を JOIN し、ペア×番組名ごとの共演回数を集計
3. イベント共演（event-coappearance）を集計する
   → events.members（String[]）を unnest してペアを展開し、ペア×イベントタイトルごとの共演回数を集計
   → type='live'（ライブ種別イベント）はライブ共演で別途集計するため除外する
4. ペアごとに、weight（全ラベル共通の合計共演回数）とは別にラベルごとの出現回数を集計し、最多出現ラベルを label に採用する（同数の場合は文字列順で決定的に選ぶ）
5. member_relationships から動的エッジ種別（live-coappearance / radio-coappearance / event-coappearance）を一括削除する
6. 集計結果を member_relationships に一括登録する（createMany, skipDuplicates: true）
```

#### 環境変数

| 変数名         | 必須 | 説明                          |
| -------------- | ---- | ----------------------------- |
| `DATABASE_URL` | ○    | Neon PostgreSQL 接続文字列    |

#### 関連ドキュメント

ラベル算出ロジックの詳細・型定義・エッジ表示ルールは `docs/design/member/member-map-data-design.md` を参照。

---

### W10. Genius リンク同期バッチ

#### 概要

Neon 管理のリリースデータのうち、YouTube リンクを持ち Genius リンク未取得のトラックを対象に、Genius API（検索エンドポイント）から歌詞ページ URL を取得し、`TrackLink`（type: genius）として Neon に保存する。取得状況は同期状況画面（`/sync-status`）向けに `genius-links-status.json`（Vercel Blob）にも記録する。詳細設計は [歌詞サイトリンク機能 データ設計書](/design/discography/genius-data-design) を参照。

#### 実行タイミング

| トリガー | スケジュール |
| --- | --- |
| 定期実行 | なし（#1179 で定期実行を停止） |
| 手動実行 | GitHub Actions の `workflow_dispatch` で可能 |

#### 環境変数

| 変数名 | 必須 | 説明 |
| --- | --- | --- |
| `GENIUS_ACCESS_TOKEN` | ○ | Genius API アクセストークン |
| `DATABASE_URL` | ○ | Neon PostgreSQL 接続文字列 |
| `BLOB_READ_WRITE_TOKEN` | ○ | Vercel Blob 読み書きトークン（同期状況の保存に使用） |
| `RELEASES_BLOB_URL` | - | `releases.json` の Blob URL（`GENIUS_LINKS_STATUS_BLOB_URL` 未設定時のフォールバック導出元として直接参照） |
| `GENIUS_LINKS_STATUS_BLOB_URL` | - | 同期状況保存先の Blob URL（未設定時は `RELEASES_BLOB_URL` から導出） |

---

### W11. モーニング女学院放送データ収集バッチ

#### 概要

X（`@morning1422`）の投稿からラジオ番組「モーニング女学院」の放送回・出演メンバー・オンエア楽曲を抽出し、Neon DB に保存する。詳細設計は [ラジオオンエア収集 設計書](/design/radio/radio-onair-data-design) §4-1 を参照。

#### 実行タイミング

| トリガー | スケジュール |
| --- | --- |
| 定期実行 | なし（#1333 で定期実行を停止。X API のタイムライン取得上限（直近3,200件付近、#1331）に到達し、これ以上の遡及取得ができなくなったため） |
| 手動実行 | GitHub Actions の `workflow_dispatch` で可能 |

> 新規放送分は `register-manual-radio-episodes.ts`（それ以外のスクリプト §定常運用）による手動登録に統一している。

#### 環境変数

| 変数名 | 必須 | 説明 |
| --- | --- | --- |
| `TWITTERAPI_IO_KEY` | ○ | twitterapi.io API キー |
| `DATABASE_URL` | ○ | Neon PostgreSQL 接続文字列 |
| `BLOB_READ_WRITE_TOKEN` | ○ | Vercel Blob 読み書きトークン |
| `MEMBERS_BLOB_URL` | ○ | Vercel Blob のメンバーデータ URL |
| `MORNING_ONAIR_MAX_PAGES` | - | 遡及取得の1回あたり上限ページ数（デフォルト: 20） |

---

### W12. ヤングタウン土曜日収集バッチ

#### 概要

X（`@yando_staff`）の投稿からラジオ番組「ヤングタウン土曜日」の放送回・出演メンバー・オンエア楽曲を抽出し、Neon DB に保存する。詳細設計は [ラジオオンエア収集 設計書](/design/radio/radio-onair-data-design) §4-2 を参照。

#### 実行タイミング

| トリガー | スケジュール |
| --- | --- |
| 定期実行 | なし（#1280, #1333 で定期実行を停止。手動登録の方が容易なため） |
| 手動実行 | GitHub Actions の `workflow_dispatch` で可能 |

> 新規放送分は `register-manual-radio-episodes.ts`（それ以外のスクリプト §定常運用）による手動登録に統一している。

#### 環境変数

| 変数名 | 必須 | 説明 |
| --- | --- | --- |
| `TWITTERAPI_IO_KEY` | ○ | twitterapi.io API キー |
| `DATABASE_URL` | ○ | Neon PostgreSQL 接続文字列 |
| `MEMBERS_BLOB_URL` | ○ | Vercel Blob のメンバーデータ URL |

---

### W13. メイボンソワ選曲データ収集バッチ

#### 概要

STV 公式サイトの HTML を解析し、ラジオ番組「メイボンソワ」の放送回・選曲データを抽出して Neon DB に保存する。詳細設計は [ラジオオンエア収集 設計書](/design/radio/radio-onair-data-design) §4-3 を参照。

#### 実行タイミング

| トリガー | スケジュール |
| --- | --- |
| 定期実行 | なし（#1333 で定期実行を停止。モーニング女学院・ヤングタウン土曜日と同様に手動登録方式へ統一するため） |
| 手動実行 | GitHub Actions の `workflow_dispatch` で可能 |

> 新規放送分は `register-manual-radio-episodes.ts`（それ以外のスクリプト §定常運用）による手動登録に統一している。

#### 環境変数

| 変数名 | 必須 | 説明 |
| --- | --- | --- |
| `DATABASE_URL` | ○ | Neon PostgreSQL 接続文字列 |
| `MEMBERS_BLOB_URL` | ○ | Vercel Blob のメンバーデータ URL |

---

### W14. デイリーダイジェスト配信バッチ

#### 概要

Instagram 投稿収集（W4）・TikTok 投稿収集（W5）・YouTube 公式チャンネル投稿収集（W7）・YouTube OG チャンネル投稿収集（W8）・メンバー近況同期（W1、Ameba ブログ分）・TikTok OGチャンネル投稿収集（W15）の各バッチが `daily-digest/{日付}/{ソース}.json`（Vercel Blob）に書き出した新着件数サマリーを集約し、1通のメール本文にまとめて送信する。

#### 実行タイミング

| トリガー | スケジュール |
| --- | --- |
| 定期実行 | 毎日 JST 23:00（UTC 14:00） |
| 手動実行 | GitHub Actions の `workflow_dispatch` で可能 |

#### 処理フロー

```
1. MEMBERS_BLOB_URL からメンバーデータを取得する（未設定時はエラーで終了）
2. daily-digest/ 配下の Blob を列挙し、ソース種別
   （instagram/instagram-official/tiktok/tiktok-official/tiktok-og/youtube-og/youtube-official/blog）
   ごとに最新日付のファイル URL を取得する（resolveLatestDigestUrls）
   → 日付をまたいで各収集バッチが実行された場合でも最新のファイルを参照できる
3. 各ソースのダイジェストファイルを取得する
   → 取得失敗（fetch 失敗・非 200 応答）したソースはスキップし、警告ログを出力して処理を継続する
4. メンバー・投稿種別ごとに件数を集計する（buildDigestEmailBody）
   → `instagram-official`/`tiktok-official`/`youtube-official`（いずれも `OfficialSummaryEntry`）は、
     SNSごとに「モーニング娘。公式 2件、ミニモちゃん 1件」のようにアカウント名付きの内訳を
     「公式アカウント」セクションに表示する（Instagram公式アカウントにはモーニング娘。公式で
     ないミニモちゃんも含まれるため、セクション見出しは特定アカウント名ではなく「公式アカウント」とする）
   → それ以外（`blog`/`instagram`/`tiktok`/`tiktok-og`/`youtube-og`、いずれも実メンバーのみを持つ `MemberSummaryEntry`）は
     メンバー別セクションに振り分け、現役→OG・期（generation）昇順・加入日昇順でソートする
     （compareMemberSections）
   → 同一メンバーが複数ソースに新着を持つ場合は「Instagram 2件、ブログ 1件」のように読点区切りで1行にまとめる
   → 新着が1件もないソース・セクションは「（新着なし）」と表示する（新着なしでもメールは送信される）
5. 集計結果をメール本文としてローカルファイル（`daily-digest-email.txt`）に書き出す
6. 集約に使用した daily-digest/ 配下の Blob ファイルを削除する（del）
   → 削除は集計元ファイルの重複読み込み防止のためで、次回実行時は新規書き込み分のみを対象とする
7. GitHub Actions ワークフローが `daily-digest-email.txt` の内容を読み込み、
   dawidd6/action-send-mail でメールを送信する
```

#### ダイジェストファイルのデータ構造

`daily-digest/{日付}/{ソース}.json` は以下の各バッチが書き込む。`source` の値によって使用するデータ型（`types/daily-digest.ts`）が異なる。実メンバーの新着件数と公式・グループアカウントの新着件数は同一SNS内でもファイルを分け、`youtube-og`/`youtube-official`の分割にならって`instagram`/`instagram-official`・`tiktok`/`tiktok-official`のように管理する（#1423）。

| ソース (`source`) | 書き込み元バッチ | ファイルパス | 使用する型 |
| --- | --- | --- | --- |
| `blog` | W1 メンバー近況同期バッチ | `daily-digest/{日付}/blog.json` | `MemberSummaryEntry` |
| `instagram` | W4 Instagram投稿収集バッチ | `daily-digest/{日付}/instagram.json` | `MemberSummaryEntry` |
| `instagram-official` | W4 Instagram投稿収集バッチ | `daily-digest/{日付}/instagram-official.json` | `OfficialSummaryEntry` |
| `tiktok` | W5 TikTok投稿収集バッチ | `daily-digest/{日付}/tiktok.json` | `MemberSummaryEntry` |
| `tiktok-official` | W5 TikTok投稿収集バッチ | `daily-digest/{日付}/tiktok-official.json` | `OfficialSummaryEntry` |
| `tiktok-og` | W15 TikTok OGチャンネル投稿収集バッチ | `daily-digest/{日付}/tiktok-og.json` | `MemberSummaryEntry` |
| `youtube-og` | W8 YouTube OGチャンネル投稿収集バッチ | `daily-digest/{日付}/youtube-og.json` | `MemberSummaryEntry` |
| `youtube-official` | W7 YouTube公式チャンネル投稿収集バッチ | `daily-digest/{日付}/youtube-official.json` | `OfficialSummaryEntry` |

##### MemberSummaryEntry（実メンバー別新着件数）

実メンバー単位で新着を持つソース（`blog`/`instagram`/`tiktok`/`tiktok-og`/`youtube-og`）が使用する。公式・グループアカウントはこの型には含まれない（後述の `OfficialSummaryEntry` で別ファイルに分離する）。

| フィールド | 型 | 内容 |
| --- | --- | --- |
| `source` | `'blog' \| 'instagram' \| 'tiktok' \| 'tiktok-og' \| 'youtube-og'` | ソース種別 |
| `date` | `string`（`YYYY-MM-DD`、JST） | 書き込み日 |
| `entries[].memberId` | `string` | 実メンバーID |
| `entries[].memberName` | `string` | メンバー名 |
| `entries[].count` | `number` | 書き込み元バッチがその実行で新規に取得・蓄積した件数 |

##### OfficialSummaryEntry（公式・グループアカウント別新着件数）

公式・グループアカウントを持つ全ソース（`instagram-official`/`tiktok-official`/`youtube-official`）が共通で使用する型。アカウント単位でカウントを保持するため、1ソースにつき複数アカウントを個別に集計できる。

| フィールド | 型 | 内容 |
| --- | --- | --- |
| `source` | `'instagram-official' \| 'tiktok-official' \| 'youtube-official'` | ソース種別 |
| `date` | `string`（`YYYY-MM-DD`、JST） | 書き込み日 |
| `accounts[].accountId` | `string` | 公式アカウントID（下表「対象アカウント一覧」参照） |
| `accounts[].accountName` | `string` | メール本文に表示するアカウント表示名 |
| `accounts[].count` | `number` | 書き込み元バッチがその実行で新規に取得した件数 |

##### 対象アカウント一覧

| ソース | 対象アカウント | 管理方法 |
| --- | --- | --- |
| `youtube-official` | モーニング娘。公式（`@morningmusume`） | `collect-youtube-official.ts` 内の `OFFICIAL_CHANNEL_HANDLES` |
| `instagram-official` | モーニング娘。公式・ミニモちゃんの2アカウント | `OFFICIAL_INSTAGRAM_ACCOUNTS`（`constants/instagram-official-accounts.ts`） |
| `tiktok-official` | モーニング娘。公式・ミニモちゃんの2アカウント（#1458） | `OFFICIAL_TIKTOK_ACCOUNTS`（`constants/tiktok-official-accounts.ts`） |

##### 同日複数回実行時のマージ

同日に複数回実行された場合、書き込み時に `putDigestEntry`（`lib/daily-digest.ts`）が既存ファイルと同一の集計単位（`MemberSummaryEntry`は`memberId`、`OfficialSummaryEntry`は`accountId`）の`count`を加算してマージする。

#### エラーハンドリング

| エラー種別 | 動作 |
| --- | --- |
| 各ソースのダイジェストファイル取得失敗（fetch 失敗・非 200 応答） | 当該ソースをスキップし警告ログを出力、後続処理を継続する（メールは残りのソースのみで生成される） |
| `daily-digest/` 配下の Blob ファイル削除失敗 | スクリプト全体が非ゼロ終了する。`daily-digest-email.txt` はステップ5で生成済みだが、`send-daily-digest.yml` の後続ステップ（メール本文読み込み・送信）は前段ステップの成功が前提（`if: always()` 等の条件なし）のため実行されず、**メール送信自体がスキップされる** |

#### 環境変数

| 変数名 | 必須 | 説明 |
| --- | --- | --- |
| `BLOB_READ_WRITE_TOKEN` | ○ | Vercel Blob 読み書きトークン（`daily-digest/` の一覧取得・削除に使用） |
| `MEMBERS_BLOB_URL` | ○ | Vercel Blob のメンバーデータ URL（未設定時はエラーで終了） |

#### メール送信設定

メール送信ステップ（`dawidd6/action-send-mail@v3`）はスクリプト本体の環境変数とは別に、ワークフロー内で以下を使用する。

| 変数名 | 種別 | 説明 |
| --- | --- | --- |
| `MAIL_SERVER` | Repository variables | SMTP サーバーアドレス |
| `MAIL_PORT` | Repository variables | SMTP ポート番号 |
| `MAIL_USERNAME` | Repository secrets | SMTP 認証ユーザー名 |
| `MAIL_PASSWORD` | Repository secrets | SMTP 認証パスワード |
| `MAIL_TO` | Repository secrets | 送信先メールアドレス |

> **注意**: `daily-digest-email.txt` が生成されなかった場合（`MEMBERS_BLOB_URL` 未設定等でスクリプトが早期にエラー終了した場合や、上記「エラーハンドリング」の Blob 削除失敗の場合）はメール送信ステップ自体がスキップされる。新着が0件の場合はファイルは生成されるため、その場合は「（新着なし）」を含むメールが送信される。

---

### W15. TikTok OGチャンネル投稿収集バッチ（#1478）

#### 概要

OGメンバー個人のTikTokアカウントの新着投稿を RapidAPI 経由で取得し、`tiktok_posts`（`channelType='og'`）に保存する。YouTube OG チャンネル投稿収集バッチ（W8）と同じ運用方式（メンバー編集画面での設定・`OG_SYNC_WEEKDAYS`・最新5件のみ保持）を採用するが、RapidAPIはハンドルのAPI解決が不要なため処理はより単純になる。詳細設計は [TikTok投稿機能 データ設計書](/design/sns/tiktok-data-design) §4を参照。

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | 毎日 JST 22:15（UTC 13:15）               |
| 手動実行     | GitHub Actions の workflow_dispatch で可能 |

> ワークフローは毎日起動するが、OG の収集は `OG_SYNC_WEEKDAYS` 環境変数（YouTube OG収集と共用）で指定した曜日のみ実行する（`shouldSyncOG`（`scripts/lib/og-sync.ts`）で判定）。デフォルト（空文字・未設定）は月曜のみ（曜日番号: `1`）。

#### 処理フロー

```
1. OG_SYNC_WEEKDAYS の対象曜日でなければ何もせず正常終了する
2. Vercel Blob からメンバーデータを取得し、tiktokChannel を持つ OG メンバー（status が Active 以外）を対象とする
3. 既存 tiktok_posts から channelType='og' の既存 postId 一覧を取得する（新着件数の集計に使用）
4. 各対象メンバーについて fetchUserPosts（RapidAPI、unique_id=tiktokChannel.username）を1ページ（先頭5件）のみ呼び出し、
   最新5件を取得する（公式アカウント収集と異なり、既存 postId に到達するまでページを遡る差分取得は行わない）
5. 取得した投稿を TikTokPost（channelType='og'・memberId 付き、mentionedMemberIds は常に空配列）に変換する
6. メンバーごとに、取得した最新5件に含まれない投稿を tiktok_posts から削除したうえで、取得した5件を保存する
   （結果としてメンバーごとに常に最新5件のみが残る）
7. daily-digest/{日付}/tiktok-og.json にメンバー別の新着件数を書き込む（データ構造はW14参照）
8. tiktok_sync_status（Neon、id=1固定の1行。W5と行は共有するが、og用カラムのみを更新しW5のofficial用カラムには触れない）の `ogLastSyncedAt`/`ogLastAutoSyncedAt`/`ogPreviousAutoSyncedAt` を更新する。TOP新着投稿セクションの新着ウィンドウ判定用フィールドであり、`/sync-status` 画面向けの公式収集ステータス表示用フィールド（`syncStatus`等）には触れない。定期実行/手動実行での更新ロジックの詳細は「[TikTok投稿機能 データ設計書](/design/sns/tiktok-data-design)」§4「新着表示の判定基準」を参照（#1489）
```

> **注意（旧: tiktok_sync_status を更新しない設計だった理由、#1478時点）**: 当初（#1478時点）は、YouTube が `youtube_sync_status` を official/og で独立カラム化しTOP新着投稿セクションのウィンドウ判定に使っている（W7・W8、#1450）のに対し、TikTokの `tiktok_sync_status` はTOP新着投稿セクションのウィンドウ判定には使われておらず `/sync-status` 画面の公式収集ステータス表示専用だったため、OG収集はこのテーブルを更新しない設計だった。その後、W5の「同日capturedAt + 件数キャップ」方式では複数アカウント・複数OGメンバーの同時収集時に新着を取りこぼす不具合が判明したため、YouTubeと同じofficial/og独立ウィンドウ方式に変更し、OG収集も`tiktok_sync_status`のog用カラムを更新するようになった（#1489）。

#### エラーハンドリング

| エラー種別 | 動作 |
| --- | --- |
| ネットワーク例外（DNS 解決失敗・接続タイムアウト等） | 当該メンバーのみスキップしてログ出力後、後続メンバーの処理を続行する |
| API HTTP エラー（404 / 500 等） | `fetchUserPosts` が例外を送出するため、当該メンバーの削除・保存をスキップして続行する（既存データ保持） |
| 全メンバー失敗（取得成功 0 人かつ失敗 1 人以上） | 非ゼロ終了して GitHub Actions の失敗通知に乗せる（W8 と同じ方針、PR #1116 レビュー申し送りより採用） |

#### 環境変数

| 変数名             | 必須 | 説明                                          |
| ------------------ | ---- | --------------------------------------------- |
| `RAPIDAPI_KEY`     | ○    | RapidAPI キー（未設定時はエラーで終了） |
| `DATABASE_URL`     | ○    | Neon PostgreSQL 接続文字列（未設定時はエラーで終了） |
| `MEMBERS_BLOB_URL` | ○    | Vercel Blob のメンバーデータ URL（未設定時はエラーで終了） |
| `BLOB_READ_WRITE_TOKEN` | ○ | Vercel Blob 読み書きトークン（デイリーダイジェスト用サマリーの書き込みに使用、W14） |
| `OG_SYNC_WEEKDAYS` | -    | OG 収集する曜日番号（カンマ区切り、デフォルト: `1`）。YouTube OG収集と共用。Repository variable（非機密な設定値のため、#1347） |

> **注意**: 秘匿すべき値（APIキー・トークン等）は Repository secrets に統一すること（Environment secrets は `environment:` 指定がないと注入されない）。曜日番号等の非機密な設定値は Repository variables に登録する（#1347）。

---

## それ以外のスクリプト

`scripts/console/`・`scripts/patch/` 配下の、GitHub Actions を介さず手動実行するスクリプト。用途に応じて以下に大別される。

- **定常運用スクリプト**: データ更新や運用作業のたびに繰り返し手動実行するもの
- **一回限りパッチ**: 特定の不具合修正・データ移行のために作成され、既に実行済みで再実行を想定しないもの
- **開発用・共通ヘルパー**: 単体実行を想定しない補助コードや、開発時のアドホックな動作確認用コード

うち3件（O1〜O3）は稼働中バッチと同水準のフル仕様で記載し、残りは概要のみの一覧表で管理する。

### O1. 曲対比インデックス生成スクリプト

#### 概要

`releases.json` を元に、タイトル前方一致でバリアント曲をグルーピングし、対比ペアインデックスを生成・Vercel Blob に保存する。
ライブアルバム・サウンドトラックは自動的に対比対象から除外される。

#### 実行タイミング

手動実行。以下の場合に再生成する:

- リリースデータ（`releases.json`）に新規リリース・トラックの追加が加わった後
- 除外パターンファイル（`comparison-exclude-patterns.json`）を更新した後
- タイトルエイリアスファイル（`title-aliases.json`）を更新した後

#### 実行コマンド

```bash
# 環境変数をあらかじめ設定した上で実行
BLOB_READ_WRITE_TOKEN=<token> RELEASES_BLOB_URL=<url> bun run scripts/console/build-comparison-index.ts
```

または `.env.local` に環境変数を設定してから:

```bash
bun run scripts/console/build-comparison-index.ts
```

#### 生成ファイル

| ファイル名 | 概要 |
| --- | --- |
| `comparison-index.json` | 対比ペア一覧（`{ pairs: ComparisonPair[] }`） |
| `track-comparison-map.json` | `trackId` → `{ pairId, baseTitle }` マップ |

#### 処理概要

1. `scripts/comparison-exclude-patterns.json` から除外ベースタイトル一覧を読み込む
2. `scripts/title-aliases.json` からタイトルエイリアスマップを読み込む
3. `RELEASES_BLOB_URL` から `releases.json` を取得
4. ライブアルバム・サウンドトラックを除いた全リリースのトラックを `Track.id` で重複排除（`collectUniqueTracks`）
5. エイリアスマップをトラックタイトルに適用し、バリアント表記を正規タイトルに統一
6. タイトル前方一致 + セパレータ判定（`isVariantOf`）でバリアントをグルーピング
7. 2 曲以上かつ除外パターンに一致しないグループを対比ペアとして採用（`buildComparisonPairs`）
8. 各トラックの代表リリースフォーマット（`releaseFormat`）を解決して記録
9. ベースタイトルの表記ゆれを正規化比較で検出し、`title-aliases.json` に自動追記
10. `comparison-index.json` と `track-comparison-map.json` を Vercel Blob に書き込み

#### 補助ファイル

##### `scripts/comparison-exclude-patterns.json` — 除外パターン

対比対象から除外したいベースタイトルの部分文字列を `baseTitles` 配列に列挙する。
ベースタイトルに指定文字列が**部分一致**する場合にそのグループを除外する。

```json
{
  "baseTitles": ["MC", "SE"]
}
```

上記の例では、ベースタイトルに "MC" または "SE" を含むグループ（例: "MC Battle"）が除外される。

##### `scripts/title-aliases.json` — タイトルエイリアス

MusicBrainz 上の表記ゆれ（全角/半角スペース差異など）を吸収するため、
バリアントタイトルから正規タイトルへのマッピングを定義する。

```json
{
  "aliases": {
    "Song　A": "Song A"
  }
}
```

- キー: バリアントタイトル（置換前）
- 値: 正規タイトル（置換後）
- エイリアス適用はインデックス生成内のみ。元の `releases.json` は変更されない
- スクリプト実行時に未登録の表記ゆれが検出された場合、`[WARN]` ログとともに自動追記される

#### 実行後の手順

生成後に表示される Blob URL を環境変数に設定すること。

| 環境変数 | 設定値 |
| --- | --- |
| `COMPARISON_INDEX_BLOB_URL` | `comparison-index.json` の Blob URL |
| `TRACK_COMPARISON_MAP_BLOB_URL` | `track-comparison-map.json` の Blob URL |

#### 環境変数

| 変数名                  | 必須 | 説明                                                      |
| ----------------------- | ---- | --------------------------------------------------------- |
| `BLOB_READ_WRITE_TOKEN` | ○    | Vercel Blob 読み書きトークン（未設定時はエラーで終了）    |
| `RELEASES_BLOB_URL`     | ○    | `releases.json` の Blob URL（未設定時はエラーで終了）     |

---

### O2. 非楽曲トラック削除パッチ（一回限り・#1057）

#### 概要

#1055 の sync スクリプト改修前に Neon DB に蓄積した非楽曲トラック（タイトルからインスト・カラオケ・MC と判定されるトラック）を削除し、
`RadioOnairSong` および `playlists.json` からの参照を事前にクリアする一回限りのパッチスクリプト。

#### 実行手順

```bash
# .env.local に以下を設定してから実行
# DATABASE_URL=...
# BLOB_READ_WRITE_TOKEN=...
# PLAYLISTS_BLOB_URL=...  (省略可: 未設定時はローカルの data/inputs/playlists.json を使用)

# 1. まず --dry-run で削除対象を確認する
bun scripts/patch/patch-delete-non-song-tracks.ts --dry-run

# 2. 内容を確認したら本実行する
bun scripts/patch/patch-delete-non-song-tracks.ts
```

#### 処理フロー

```
1. Neon DB の tracks テーブルからすべてのトラックを取得する
2. タイトルパターン（isNonSongTrack）で非楽曲トラックを特定する
   → recording.video フラグは DB に保存されていないため、タイトルパターンのみで判定
   → 映像トラック（Dance Shot Ver. 等）は sync-discography 再実行時に自然と除外される
3. RadioOnairSong.trackId が対象 trackId を参照している行を NULL に更新する
4. playlists.json（Blob + ローカルファイル）の entries.trackId / releaseId をクリアする
   → PLAYLISTS_BLOB_URL 未設定時はローカルファイルのみ更新
5. 非楽曲トラックを tracks テーブルから削除する
   → track_links は onDelete: Cascade で自動削除
```

#### 環境変数

| 変数名                  | 必須 | 説明                                              |
| ----------------------- | ---- | ------------------------------------------------- |
| `DATABASE_URL`          | ○    | Neon PostgreSQL 接続文字列                        |
| `BLOB_READ_WRITE_TOKEN` | ○    | Vercel Blob 書き込みトークン                      |
| `PLAYLISTS_BLOB_URL`    | △    | playlists.json の Blob URL（未設定時はローカルのみ）|

#### 注意事項

- **冪等性あり**: 削除対象がない場合は何もしない（再実行可能）
- パッチ実行後は sync-discography を再実行し、欠落していた楽曲（`Nature is good!` 等）を補完すること

---

### O3. メンバー関係性マップ 静的エッジシードスクリプト

#### 概要

`member_relationships` テーブルのうち、メンバー・ユニットのプロフィールデータから機械的に導出できる静的エッジ（同期・同郷・同ユニット・同時在籍・リーダー継承・師弟関係の6種別）を生成する定常運用の手動実行スクリプト。メンバー・ユニットデータの更新時（加入・卒業・出身地変更・ユニット編成変更等）に再実行する。W9の動的エッジ同期とは異なり、GitHub Actions ワークフローには組み込まれていない。

#### 実行タイミング

| トリガー     | スケジュール                              |
| ------------ | ----------------------------------------- |
| 定期実行     | なし                                       |
| 手動実行     | `bun scripts/console/seed-member-relationships.ts` |

#### 処理フロー

```
1. Vercel Blob からメンバーデータ（MEMBERS_BLOB_URL）・ユニットデータ（UNITS_BLOB_URL）を取得する
2. 静的エッジを生成する（generateEdges）
   1. 同期（same-gen）: 同一 generation の全ペアに無方向エッジ（weight=1）
   2. 同郷（same-hometown）: 同一 profile.birthplace の全ペアに無方向エッジ（weight=1）
   3. 同ユニット（same-unit）: モーニング娘。本体・年度別ユニット（morning-musume / morning-musume-*）を除くサブユニットのうち、メンバー2名以上のユニット内の全ペアに無方向エッジ（label にユニット名）
   4. 同時在籍（overlapping）: 加入日〜卒業日（未卒業は現在日扱い）の期間が重なる全ペアに無方向エッジ（weight=1）
   5. リーダー継承（leader-succession）: leaderGenerationNumber が連番で隣接するペアに有向エッジ（前任→後任）
   6. 師弟関係（mentor-student）: profile.instructor に列挙された師匠 ID ごとに有向エッジ（fromMemberId=師匠、toMemberId=弟子、weight=1）
3. member_relationships から静的エッジ種別（6種）を一括削除する
4. 生成したエッジを member_relationships に一括登録する（createMany, skipDuplicates: true）
5. 種別ごとの生成件数をログ出力する
```

#### weight / label（現状）

| type | weight | label |
| --- | --- | --- |
| same-gen / same-hometown / overlapping / leader-succession / mentor-student | 1 固定 | null |
| same-unit | 1 固定 | ユニット名 |

#### 環境変数

| 変数名              | 必須 | 説明                                                    |
| ------------------- | ---- | ------------------------------------------------------- |
| `MEMBERS_BLOB_URL`  | ○    | Vercel Blob のメンバーデータ URL（未設定で0件）          |
| `UNITS_BLOB_URL`    | ○    | Vercel Blob のユニットデータ URL（未設定で同ユニットエッジをスキップ） |
| `DATABASE_URL`      | ○    | Neon PostgreSQL 接続文字列                              |

#### スコアリング組み込みによる変更点（実装: #1269）

関係性スコアリングロジックの実装（#1269）により、以下の変更を加える。計算式の詳細は `docs/design/member/member-map-data-design.md` §8 を参照。

- **overlapping の weight 算出方法変更**: 現状の `weight=1` 固定から、重なり月数を 0〜40pt に正規化した値に変更する（上限120ヶ月=10年で満点、以降40pt上限）
- **mentor-student の direct/indirect 判定**:
  - 既存の direct エッジ（`profile.instructor` 由来、depth=1）は `weight=30`・`label='direct'` に更新する
  - `profile.instructor` の連鎖を depth 2 以上たどって到達できるメンバーペア（indirect）を新たに算出し、`weight=15`・`label='indirect'` で追加する
  - **循環参照防止**: 連鎖探索は訪問済みメンバー ID を `Set` で管理し、既訪問メンバーには再度たどらない
- **`data/inputs/manual-mentor-relationships.json` のマージ**: 連鎖では拾えない師弟関係（本人の発言による影響表明等）を手動追記するファイル（初期状態は空）を新設し、`data/inputs/manual-releases.json` と同様のパターンで sync 時にマージする

---

### 定常運用スクリプト一覧

O1・O3 を除く、繰り返し手動実行することを想定したスクリプト。

| スクリプト | 概要 |
| --- | --- |
| `scripts/console/check-playlist-integrity.ts` | プレイリストと Neon リリースの trackId 整合性を検証する |
| `scripts/console/find-missing-tour-album-links.ts` | ツアーとライブアルバムの紐づき漏れを抽出・表示する（`docs/operations/tour-album-link-operations.md` 参照） |
| `scripts/console/playlist-link.ts` | プレイリスト楽曲の YouTube リンクを検索・修正する CLI |
| `scripts/console/register-manual-radio-episodes.ts` | 手動用意した放送回 JSON を Neon のラジオ番組 DB に登録する（[ラジオオンエア収集 設計書](/design/radio/radio-onair-data-design) §4-4） |
| `scripts/console/register-tour-lives.ts` | CSV からツアー公演・会場情報を Neon に登録する（`docs/operations/tour-live-registration.md` 参照） |
| `scripts/console/save-instagram-auth.ts` | Instagram に手動ログインし認証状態ファイルを保存する（W4 の Secret 運用手順） |
| `scripts/console/seed-member-map-layout.ts` | 全メンバーの初期円形座標を Neon にシード登録する |
| `scripts/console/sync-tiktok-track-links.ts` | 既存 TikTok リンクを修正し TrackLink と再照合登録する |
| `scripts/patch/fetch-release-images.ts` | Cover Art Archive からリリースイベントの jacket 画像を補完する |
| `scripts/patch/migrate-to-blob.ts` | ローカルの members.json を Blob へマージしてアップロードする（新規メンバー登録の定型フロー） |
| `scripts/patch/patch-birthplace.ts` | ローカルデータの出身地情報を Blob にマージ反映する |
| `scripts/patch/patch-instagram-reel-flag.ts` | 指定アカウント（メンバー or 公式アカウント）のプロフィールグリッドを再確認し、既存投稿の `isReel` フラグをバックフィルする CLI（`docs/operations/patch-instagram-reel-flag.md` 参照、#1373） |
| `scripts/patch/patch-instructor.ts` | ローカルデータの指導者情報を Blob にマージしキャッシュを破棄する |
| `scripts/patch/patch-leader-generations.ts` | ローカルデータのリーダー世代番号を Blob にマージ反映する（リーダー交代時） |
| `scripts/patch/patch-member-colors.ts` | ローカルデータのメンバーカラーを Blob にマージ反映する（新メンバー加入時） |
| `scripts/patch/patch-member-graduate.ts` | 指定メンバーを OG 卒業扱いに更新する CLI |
| `scripts/patch/patch-member-joining-routes.ts` | ローカルデータの加入ルートを Blob にマージ反映する |
| `scripts/patch/patch-member-nicknames.ts` | ローカルデータの愛称情報を Blob にマージ反映する |
| `scripts/patch/patch-official-sns.ts` | ローカルデータの公式 SNS リンクを Blob にマージ反映する |

### 一回限りパッチ一覧（実行済み）

O2 を除く、特定の不具合修正・データ移行のために作成され、既に実行済みで再実行を想定しないスクリプト。

| スクリプト | 概要 | 関連Issue |
| --- | --- | --- |
| `scripts/console/delete-youtube-shorts.ts` | Neon の TrackLink から youtube-short 型を一括削除する | - |
| `scripts/console/restore-youtube-links.ts` | Blob の releases.json から youtube リンクを Neon に復元する | - |
| `scripts/patch/migrate-instagram-thumbnails.ts` | Instagram サムネイル URL を新形式に一括移行する | - |
| `scripts/patch/migrate-releases-to-neon.ts` | releases.json を Neon の Release/Track 等に一括移行する | #854 |
| `scripts/patch/migrate-tiktok-to-neon.ts` | tiktok/posts.json を Neon の tiktok_posts に移行する | #888 |
| `scripts/patch/backfill-ameba-captured-at.ts` | statusHistory の既存エントリへ capturedAt（収集日時）を手動投入する（新着判定を投稿日時基準から収集日時基準に変更した対応の表示確認用） | #1408 |
| `scripts/patch/patch-ameba-duplicate-status.ts` | statusHistory の重複 Ameba エントリを削除する | #1329 |
| `scripts/patch/patch-ameba-misattributed-status.ts` | 他メンバーに誤帰属した Ameba 投稿を削除する | #1328 |
| `scripts/patch/patch-ameba-title-only-status.ts` | Ameba 由来 content を本文除去しタイトルのみに補正する | #1328 |
| `scripts/patch/patch-blog-history-pubdates.ts` | ブログ履歴の lastUpdated を RSS pubDate で補正・整列する | - |
| `scripts/patch/patch-morning-musume-subunit-ids.ts` | 特定楽曲の unitId を正しいサブユニットへ修正する | #826 |
| `scripts/patch/patch-radio-onair-song-titles.ts` | 誤抽出された放送曲名を是正し再紐付けする（PR #1202 以前登録分のみ対象） | #1292 |
| `scripts/patch/patch-tampopo-unit-id.ts` | unitId「tampopo」の誤字を「tanpopo」に修正する | - |
| `scripts/patch/seed-ameba-sync-status.ts` | Ameba同期状態（`ameba/sync-status.json`）の初期データを投入する（sync-members 初回実行前の表示確認用） | #1387 |

### 開発用・共通ヘルパー

単体実行を想定しない補助コード、または開発時のアドホックな動作確認用コード。

| スクリプト | 概要 |
| --- | --- |
| `scripts/console/test-api.ts` | ローカル API への PUT リクエストを試す開発用テストスクリプト |
| `scripts/patch/revalidate-cache.ts` | 他パッチスクリプトから呼ばれるキャッシュ破棄処理の共通ヘルパー（単体実行不可） |

---

## 改訂履歴

| 版  | 更新日     | 変更内容                                                                 |
| --- | ---------- | ------------------------------------------------------------------------ |
| 4.1 | 2026-08-09 | W5をPlaywright + Vercel Blob前提の旧アーキテクチャ記述（#888のNeon移行前のまま放置されていた）からRapidAPI + Neon前提の現行実装に全面的に書き直し、`tiktok_sync_status`のofficial用ウィンドウ（`officialLastSyncedAt`等）を更新するステップを追記。W15にも`tiktok_sync_status`のog用ウィンドウを更新するステップを追記し、「更新しない」としていた§注意を経緯の記録に更新。TOP新着投稿セクションの新着判定を「同日capturedAt + slice(0, 10)」の件数キャップ方式からYouTube（W7・W8、#1450）と同じofficial/og独立ウィンドウ方式に置き換えたことに伴う変更（詳細は「[TikTok投稿機能 データ設計書](/design/sns/tiktok-data-design)」§4、#1489） |
| 4.0 | 2026-08-09 | OGメンバーのTikTok個人アカウント収集バッチ（W15）を新設（YouTube OG収集・W8と同方式）。ワークフロー一覧・W14のデイリーダイジェストデータ構造（`tiktok-og`ソース追加）・対象アカウント一覧の`tiktok-official`の記述（#1458完了を反映）を更新（#1478） |
| 3.9 | 2026-07-31 | W14のメール本文集計（buildDigestEmailBody）の記述を実装（#1457）に合わせて修正。公式アカウントのセクション見出しを「モーニング娘。公式」から「公式アカウント」に訂正し（Instagram公式アカウントにはモーニング娘。公式でないミニモちゃんも含まれるため）、集計内容もSNS単位でのアカウント名付き内訳表示であることを明記 |
| 3.8 | 2026-07-31 | W14のデイリーダイジェストファイルのデータ構造を、公式・グループアカウント集計方法の統一方針に合わせて改訂。`OfficialSummaryEntry`を`instagram-official`/`tiktok-official`/`youtube-official`の3ソース共通型（アカウント単位カウント）に拡張し、Instagram・TikTokが`MemberSummaryEntry.entries`に疑似メンバーとして間借りする現行実装を廃止する設計に変更。W4・W5の処理フロー・Blob書き込みパス表に新ファイル（`instagram-official.json`・`tiktok-official.json`）を追記（#1423） |
| 3.7 | 2026-07-30 | W7・W8の`youtube_sync_status`更新記述を、official/og独立カラム構成に合わせて修正（PRレビューで単一ウィンドウ共有の回帰を検出したため。詳細は「[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design)」§6.2、#1450） |
| 3.6 | 2026-07-30 | W7・W8の処理フローに `youtube_sync_status`（Neon）を更新するステップを追記。新着投稿のTOP表示に使う新着ウィンドウ判定の同期ステータス書き込み処理が抜けていたため是正（詳細は「[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design)」§6.2、#1450） |
| 3.5 | 2026-07-25 | W14にデイリーダイジェストファイルのデータ構造節を新設し、`MemberSummaryEntry`/`OfficialSummaryEntry`の型・公式グループアカウントの扱い（ソースごとに管理方法が異なる点、Instagram2アカウント分がメール集計で合算される点）を明記。W1・W4・W5・W7・W8の処理フロー・Blob書き込みパス表にダイジェストファイル書き込みの記載が欠けていたため追記（#1416） |
| 3.4 | 2026-07-24 | 一回限りパッチ一覧に`backfill-ameba-captured-at.ts`を追加。新着判定基準を投稿日時から収集日時（`StatusEntry.capturedAt`）に変更した対応の表示確認用（#1408） |
| 3.3 | 2026-07-20 | W1にAmeba新着表示用の同期ステータス書き込み（`ameba/sync-status.json`）・`GITHUB_EVENT_NAME`環境変数を追記。一回限りパッチ一覧に`seed-ameba-sync-status.ts`を追加（#1387） |
| 3.2 | 2026-07-20 | 環境変数の使用状況棚卸し（#1363）で判明した各バッチの環境変数テーブルの欠落を是正。W2に`UNITS_BLOB_URL`・`REFETCH_ALL_SINGLES`、W5に`RAPIDAPI_KEY`、W7・W8に`BLOB_READ_WRITE_TOKEN`、W10に`RELEASES_BLOB_URL`を追記。横断的な環境変数一覧は新規 [環境変数一覧](/design/common/environment-variables) を参照 |
| 3.1 | 2026-07-20 | デイリーダイジェスト配信バッチ（`scripts/workflow/send-daily-digest.ts`）の本文セクションを新設（W14）。概要テーブルには記載があるが本文セクションが存在しなかった状態を解消。エラーハンドリング節を追加し、Blob 削除失敗時にメール送信自体がスキップされる挙動を明記（#1377、PR #1388 レビュー対応） |
| 3.0 | 2026-07-20 | ドキュメント全体を「ワークフロー」「それ以外のスクリプト」の2カテゴリに再構成し、カテゴリごとの連番（W1〜W13・O1〜O3）に変更（カテゴリを跨いだ追加のたびに発生していた全体繰り下げを解消）。§4・§8（旧番号）のスクリプトパス誤記（`scripts/console/`・`scripts/patch/` 配下の実際の配置が未反映）を是正。Genius リンク同期・モーニング女学院放送データ収集・ヤングタウン土曜日収集・メイボンソワ選曲データ収集の本文セクションを新設（W10〜W13、#1378）。`scripts/console/`・`scripts/patch/` 配下の未記載スクリプト32件を「それ以外のスクリプト」に一覧化し、定常運用（18件）／一回限りパッチ（12件）／開発用・共通ヘルパー（2件）に分類（#1376） |
| 2.16 | 2026-07-19 | 概要テーブル: スクリプトパス9件を実際の配置（`scripts/workflow/` 配下）に是正。モーニング女学院・メイボンソワ・Geniusリンク同期の状態を実態（定期実行停止・手動実行のみ）に修正し、同様に定期実行停止済みだったディスコグラフィー同期・プレイリストリンク同期の状態も是正。欠落していたヤングタウン土曜日収集・デイリーダイジェスト配信の2行を追加（#1376） |
| 2.15 | 2026-07-19 | Ameba RSS 解析セクションの詳細記述を「Amebaブログ機能 データ設計書」に集約し、二重管理を解消（#1367） |
| 2.14 | 2026-07-18 | `OG_SYNC_WEEKDAYS` / `WEB_SEARCH_SYNC_WEEKDAYS` の保存先を Repository secrets から Repository variables に修正（非機密な短い数字列がCIログの無関係な箇所を誤マスクする不具合の対策、#1347） |
| 2.13 | 2026-07-13 | メンバー近況同期バッチ: 複数新着ブログ投稿の蓄積処理で、ループ後の最新投稿蓄積にも重複チェック（`shouldSkipAccumulation`）を追加し、同一投稿が2件連続で蓄積されるバグを修正（#1329） |
| 2.12 | 2026-07-12 | メンバー近況同期バッチ: Ameba RSS 解析セクションを新設。CDATA抽出の改行対応、content にタイトルのみを格納する仕様（著作権保護のため本文を含めない）、グループブログでは本文一致によるフォールバックを行わない投稿者判定の変更を明記（#1328） |
| 2.11 | 2026-07-12 | Instagram 投稿収集バッチ: OGメンバーの収集を `OG_SYNC_WEEKDAYS` で指定した曜日のみに制限するよう修正（毎日収集されていたバグの修正、#1297） |
| 2.10 | 2026-07-11 | メンバー近況同期バッチ: Exaクレジット消費削減のため公式SNS登録状況に応じた検索範囲の排他化・公式SNS未登録メンバーの一般Web検索週1回制限（`WEB_SEARCH_SYNC_WEEKDAYS`）を追加。Exa Search API の料金体系を追記（#1307） |
| 2.9 | 2026-07-04 | メンバー関係性マップ 静的エッジシードスクリプトをセクション12と概要テーブルに追加。スコアリング組み込みによる変更点（#1269）を追記 |
| 2.8 | 2026-07-04 | メンバー関係性マップ 動的エッジ同期バッチ（#1236, #1263）をセクション11と概要テーブルに追加 |
| 2.7 | 2026-06-13 | ディスコグラフィー同期バッチ: リリース単位 MV 取得廃止・定期実行停止・実行タイミングを手動のみに変更（#1123） |
| 2.6 | 2026-06-11 | YouTube OG チャンネル投稿収集バッチ（#1104）をセクション10と概要テーブルに追加。OG 曜日判定 `shouldSyncOG` を `scripts/lib/og-sync.ts` に共通化 |
| 2.5 | 2026-06-11 | YouTube 公式チャンネル投稿収集バッチ（#1103）をセクション9と概要テーブルに追加 |
| 2.4 | 2026-06-10 | Instagram 投稿収集: 収集時のサムネイル URL 有効性検証を廃止（非ブラウザリクエストへの 429 対策・#1114）。処理フローから未使用の og:image 取得の記述を削除（PR #1115 レビュー対応） |
| 2.3 | 2026-06-02 | 非楽曲トラック削除パッチスクリプト（#1057）のセクションを追加 |
| 2.2 | 2026-06-02 | ディスコグラフィー同期バッチ: 非楽曲トラック自動除外（recording.video フラグ・タイトルパターン）とシングル全エディションマージを追加（#1055） |
| 2.1 | 2026-05-24 | イベント同期バッチ（#927）を概要テーブルとセクション7に追加 |
| 2.0 | 2026-05-22 | モーニング女学院放送データ収集・メイボンソワ選曲データ収集バッチ（#921/#923）を概要テーブルに追加 |
| 1.9 | 2026-05-14 | TikTok 投稿収集バッチ（#805）を追加 |
| 1.8 | 2026-05-14 | sync-discography・sync-genius-links・sync-playlist-links の書き込み先を Vercel Blob から Neon DB に移行。ドライラン条件を `DATABASE_URL` 未設定に変更（#856, #851） |
| 1.7 | 2026-05-07 | Shorts 取得クエリに `Shorts` キーワードを追加し、同期ステータスに Shorts 取得件数を追加（#830） |
| 1.6 | 2026-05-06 | ディスコグラフィー同期バッチに `syncTrackShortLinks`（YouTube Shorts URL 付与）を追加し、処理フローと環境変数表を更新（#774） |
| 1.5 | 2026-04-29 | Instagram サムネイル取得方式への移行（Blob 保存の廃止）（#751） |
| 1.4 | 2026-04-26 | Instagram 投稿収集バッチ（#718）の設計を追加 |
| 1.3 | 2026-03-29 | エイリアスファイル・除外パターンの説明追加、処理概要を最新化（#508） |
| 1.2 | 2026-03-28 | 曲対比インデックス生成スクリプト（#488）の設計を追加 |
| 1.1 | 2026-03-27 | プレイリスト集計・インデックス生成スクリプトの設計、および選曲リリースの自動解決ロジックを追加 |
| 1.0 | 2026-03-21 | 初版作成                                                                 |
