メンバー関係性マップ データ設計書
最終更新: 2026-07-06 関連 Issue:
- #1197(メンバ関係性マップ)/ #1208(データ管理)/ #1210(レイアウト操作・永続化)/ #1236(共演エッジのラベル表示)/ #1263(集計とラベルの関連付け見直し)/ #1266(加入時リーダーによるチルドレン分類)
- #1198(関係性スコアリングマップ)/ #1268(関係性スコアリング設計書作成)/ #1269(関係性スコアリングロジックの実装)
1. 概要
メンバー関係性マップ機能は、モーニング娘。現役メンバー間の関係性(同期・同郷・師弟など)を React Flow でビジュアライズする画面。データは Neon(PostgreSQL)上の2テーブルで管理する。
2. テーブル設計
2-1. member_relationships(MemberRelationship)
メンバー間の関係性を1件1レコードとして保持する。
| カラム | 型 | 制約 | 説明 |
|---|---|---|---|
id |
INT | PK, AUTO INCREMENT | 関係性 ID |
fromMemberId |
VARCHAR(50) | NOT NULL | 関係の起点メンバー ID |
toMemberId |
VARCHAR(50) | NOT NULL | 関係の終点メンバー ID |
type |
VARCHAR(50) | NOT NULL | 関係種別(下記参照) |
weight |
INT | NOT NULL, DEFAULT 1 | 関係の強さ(エッジの太さに反映) |
label |
VARCHAR(255) | NULL | 補足ラベル(same-unit ではユニット名、live-coappearance/radio-coappearance/event-coappearance では最多共演ライブ名・番組名・イベント名) |
- ユニーク制約:
(fromMemberId, toMemberId, type) - インデックス:
fromMemberId・toMemberId(各単列インデックス) - Prisma モデル名:
MemberRelationship(@@map("member_relationships"))
関係種別(type)一覧
| type 値 | 表示ラベル | 有向エッジ | フェーズ1現役マップ表示 |
|---|---|---|---|
same-gen |
同期 | なし | ✅ 表示 |
same-hometown |
同郷 | なし | ✅ 表示 |
same-unit |
label(ユニット名) |
なし | ❌ 非表示 |
leader-succession |
リーダー継承 | あり(→後任) | ✅ 表示 |
mentor-student |
師弟 | あり(→弟子) | ✅ 表示 |
live-coappearance |
ライブ共演 | なし | ❌ 非表示 |
radio-coappearance |
ラジオ共演 | なし | ❌ 非表示 |
event-coappearance |
イベント共演 | なし | ❌ 非表示 |
overlapping |
同在籍 | なし | ❌ 非表示(ラベルも非表示) |
エッジ生成元(静的エッジ / 動的エッジ)
member_relationships は生成元スクリプトが異なる2種類のエッジで構成される。
| 区分 | 対象 type | 生成スクリプト | 実行契機 |
|---|---|---|---|
静的エッジ(STATIC_EDGE_TYPES) |
same-gen・same-hometown・same-unit・overlapping・leader-succession・mentor-student |
scripts/console/seed-member-relationships.ts |
メンバー名簿・ユニット構成の変更時に手動実行(コンソールスクリプト) |
動的エッジ(DYNAMIC_EDGE_TYPES) |
live-coappearance・radio-coappearance・event-coappearance |
scripts/workflow/sync-relationships.ts |
Sync Events ワークフローから定期実行 |
「静的」「動的」は生成元スクリプトの実行契機による分類であり、値そのものの不変性を意味しない(例: overlapping は静的エッジだが、現役メンバーの在籍月数は時間経過で変わる。ただし weight/label への反映は手動実行時点のスナップショットであり、自動で追従はしない)。
現役マップ表示対象 (ACTIVE_MEMBER_ALLOWED_RELATIONSHIP_TYPES):
// lib/member-map-utils.ts
export const ACTIVE_MEMBER_ALLOWED_RELATIONSHIP_TYPES = new Set([
'same-gen',
'same-hometown',
'leader-succession',
'mentor-student',
]);
GET /api/member-relationships は全種別を返す。クライアント側の filterDisplayRelationships() で絞り込む(「OGを含む」OFF 時はこの4種別かつ両端が表示対象メンバーの関係のみ、ON 時は全種別)。
ラベル表示ルール (getEdgeLabel):
overlapping: 常にnull(ラベルを表示しない)same-unit:r.label(ユニット名)を使用。nullの場合は'ユニット'live-coappearance/radio-coappearance/event-coappearance:r.label(最多共演ライブ名・番組名・イベント名)を使用。null(同期前の旧データ等)の場合は固定ラベル(「ライブ共演」「ラジオ共演」「イベント共演」)にフォールバック- その他:
RELATIONSHIP_LABEL_MAP[type]の固定ラベルを使用
live-coappearance/radio-coappearance/event-coappearance の label 算出(scripts/workflow/sync-relationships.ts):
sync-relationships.ts の同期処理で、メンバーペアごとに以下を集計する。
- ライブ共演:
member_lives→lives→tours(LEFT JOIN)を辿り、Tour.title→ (null の場合)Live.title→ (両方 null の場合)固定文言「ライブ共演」の順でフォールバックしたラベルをペアごとに取得 - ラジオ共演:
radio_members→radio_episodes→radio_showsを JOIN し、ペア×番組名ごとの共演回数を取得 - イベント共演:
events.titleを JOIN 不要でそのまま参照し、ペア×イベント名ごとの共演回数を取得。type='live'(ライブ種別イベント)はライブ共演で別途集計するため除外する - ペアごとに、
weight(全ラベル共通の合計共演回数)とは別に、ラベルごとの出現回数を集計 - 出現回数が最多のラベルを
labelに設定する。同数の場合は文字列順(昇順)で決定的に選ぶ
2-2. member_map_layouts(MemberMapLayout)
メンバーのノード座標を永続化する。
| カラム | 型 | 制約 | 説明 |
|---|---|---|---|
memberId |
VARCHAR(50) | PK | メンバー ID(member.id と対応) |
x |
FLOAT | NOT NULL | React Flow キャンバス上の X 座標 |
y |
FLOAT | NOT NULL | React Flow キャンバス上の Y 座標 |
- Prisma モデル名:
MemberMapLayout(@@map("member_map_layouts")) - 座標未登録のメンバーは円形配置アルゴリズムで仮配置される(DB には保存されない)
- 「配置を保存」ボタン押下時に
PUT /api/member-map-layoutで UPSERT される
3. TypeScript 型定義
lib/member-map-utils.ts に定義。
export interface MemberRelationship {
id: number;
fromMemberId: string;
toMemberId: string;
type: string;
weight: number;
label: string | null;
}
export interface MemberMapLayoutPoint {
memberId: string;
x: number;
y: number;
}
export type ControlMode = 'filter' | 'group';
export type ControlAxis = 'generation' | 'hometown' | 'relationship' | 'leader' | 'joiningRoute';
4. API エンドポイント
4-1. GET /api/member-map-layout
保存済みの全ノード座標を返す。
- 実装:
app/api/member-map-layout/route.ts - レスポンス:
MemberMapLayoutPoint[]
[
{ "memberId": "oda-sakura", "x": 120.5, "y": -80.0 },
{ "memberId": "ishida-ayumi", "x": -200.0, "y": 150.0 }
]
- エラー: 500 →
{ "error": "メンバーマップレイアウトデータの取得に失敗しました。" }
4-2. PUT /api/member-map-layout
ノード座標を保存(UPSERT)する。
- 実装:
app/api/member-map-layout/route.ts - リクエストボディ:
{
"layouts": [
{ "memberId": "oda-sakura", "x": 120.5, "y": -80.0 }
]
}
- 処理: Prisma
$transaction内で各レコードをupsert(where: { memberId }で一致時 update、不一致時 create) - レスポンス:
{ "updated": <更新件数> } - エラー: 400 →
{ "error": "layouts は空でない配列である必要があります。" }(layoutsが空配列または非配列の場合) - エラー: 500 →
{ "error": "レイアウトデータの保存に失敗しました。" }
4-3. GET /api/member-relationships
全関係性データを返す(全 type・全ステータスのメンバー間を含む)。
- 実装:
app/api/member-relationships/route.ts - ソート:
type昇順、id昇順 - レスポンス:
MemberRelationship[]
[
{ "id": 1, "fromMemberId": "oda-sakura", "toMemberId": "nonaka-miki", "type": "same-gen", "weight": 1, "label": null }
]
- エラー: 500 →
{ "error": "メンバー関係性データの取得に失敗しました。" }
注意: API はフィルタリングを行わない。表示対象メンバー間・許可 type への絞り込みはクライアント側(
filterDisplayRelationships)で実施する。
5. クライアント側ユーティリティ(lib/member-map-utils.ts)
5-1. データ変換
| 関数 | 用途 |
|---|---|
buildActiveMemberSet(members) |
Active メンバーの ID セットを生成 |
buildDisplayMemberSet(members, includeOg) |
表示対象メンバーの ID セットを生成(includeOg=false は Active のみ、true は全メンバー) |
filterDisplayRelationships(relationships, displayMemberIds, includeOg) |
表示対象メンバー間の関係を抽出(includeOg=false 時は許可 type のみ、true 時は全種別) |
buildMemberColorMap(members) |
memberId → color のマップを生成 |
buildLayoutMap(layouts) |
memberId → {x, y} のマップを生成 |
5-2. フィルタリング(mode = ‘filter’)
getFilteredMemberIds(axis, selectedValue, members, activeRelationships, activeMemberIds)
| axis | 返却内容 |
|---|---|
generation |
選択期(parseInt(selectedValue))の表示対象メンバー ID セット |
hometown |
member.profile.birthplace === selectedValue の表示対象メンバー ID セット |
relationship |
r.type === selectedValue の関係に含まれる両端メンバー ID セット |
leader |
加入時のリーダーが選択したリーダーだった表示対象メンバー ID セット(deriveLeaderPeriods、#1266) |
joiningRoute |
member.joiningRoute === selectedValue の表示対象メンバー ID セット(#1284) |
getFilteredEdgeIds(axis, selectedValue, activeRelationships, highlightedMemberIds)
| axis | 返却内容 |
|---|---|
relationship |
r.type === selectedValue のエッジ ID セット(edge-${r.id}) |
| その他 | 両端が highlightedMemberIds に含まれるエッジ ID セット |
5-3. グルーピング(mode = ‘group’)
buildGroupInfos(axis, members, activeRelationships, activeMemberIds, selectedRelType)
| axis | グループキー | ラベル |
|---|---|---|
generation |
String(member.generation) |
${key}期 |
hometown |
member.profile.birthplace |
出身地名(null メンバーはグループ外) |
relationship |
r.label ?? r.type |
関係種別ラベル(selectedRelType 一致の関係のみ) |
leader |
加入時のリーダーの memberId(getMemberLeaderId、#1266) |
getLeaderChildrenLabel による「(愛称|氏名)チルドレン」形式(member.nickname 配列の先頭(1件目)が登録されていれば愛称、未登録なら氏名) |
joiningRoute |
member.joiningRoute |
加入ルート名(undefined メンバーはグループ外、#1284) |
buildGroupedLayout(activeMemberIds, groups, layoutMap)
- グループ内メンバーの座標(
layoutMap)から min/max を求める GROUP_PADDING = 36を四辺に加えて包含矩形を算出- グループ同士の重なりを
resolveGroupOverlapsで解消(重なりがなくなった時点で終了、最大 100 回) - 子ノードに
parentId・相対座標(relX = absX - groupX・relY = absY - groupY)を付与 - いずれのグループにも属さないメンバーは
ungroupedIdsとして管理(絶対座標)
5-4. 並列エッジ計算
computeParallelEdgeGroups(edges)
同一ノードペア(source・target をソートしたキー)に複数エッジが存在する場合、各エッジに parallelIndex(0-based)・parallelTotal を付与する。
GradientEdge 側でこれらを使って垂直方向 EDGE_SEPARATION = 18 px ずつオフセットする。
6. 円形配置アルゴリズム
getCirclePosition(index, total) — DB 未登録メンバーの仮配置に使用。
radius = max(200, total × 60)
angle = 2π × index / total
x = round(radius × cos(angle))
y = round(radius × sin(angle))
total: 座標未登録メンバー(unsaved、現役+OG 全メンバーが対象)の総数index:unsaved配列中での順序(unsaved.forEachのインデックス)- 仮配置の座標は DB に保存しない。「配置を保存」ボタン押下時に初めて DB へ書き込まれる
7. エッジ線幅計算
getEdgeStrokeWidth(weight) — weight を 1〜5px の線幅に変換。
strokeWidth = max(1, min(1 + weight / 20, 5))
| weight | strokeWidth |
|---|---|
| 1 | 1.05 |
| 20 | 2.0 |
| 80 | 5.0 |
| 100 | 5.0(上限) |
8. 関係性スコアリング(実装: #1269)
メンバー間の関係性を4軸で数値化し、総合スコア(0〜100pt)として算出する機能。既存の相関図(React Flow)とは独立した機能であり、member_relationships テーブルを置き換えるものではなく拡張する(§8-6)。
8-1. スコア軸と配点
| 軸 | 配点 | 算出元 |
|---|---|---|
| 在籍期間重なり | 0〜40pt | member.profile.joinDate / gradDate |
| 師弟関係 | 0〜30pt | member_relationships(静的エッジ、type='mentor-student') |
| メンバーカラー距離 | 0〜20pt | member.color |
| 出身地距離 | 0〜10pt | member.profile.birthplace |
総合スコア = 4軸の合計(0〜100pt)。
実装: lib/member-map-utils.ts の getOverlapScore・getMentorStudentScore・getColorDistanceScore・getBirthplaceDistanceScore・getRelationshipScore(4軸合算)。既存の相関図と同じくクライアントから import 可能な純粋関数として実装し、DB・API 呼び出しは行わない(§8-6)。
8-2. 在籍期間重なり(0〜40pt)
overlapMonths = 2メンバーの在籍期間(joinDate〜gradDate:現役は現在まで)が重なる月数
score = min(40, round(40 × min(overlapMonths, 120) / 120))
10年(120ヶ月)で満点(40pt)とし、10年以上は40ptで頭打ちとする。
overlapMonths はカレンダー年月の差分(年×12+月)で算出し、日にちは見ない(例: 1/25〜2/10の重なりも実日数16日ではなく1ヶ月として扱う)。スコアリング用途の粗い指標であり、日単位の精度は求めない。
8-3. 師弟関係(0〜30pt)
既存の mentor-student エッジ(静的エッジ、profile.instructor 由来、方向: 師匠→弟子)を direct/indirect の2段階に分けてスコア化する。
| 区分 | 判定 | score |
|---|---|---|
| direct | profile.instructor 由来の直接エッジ(depth=1) |
30pt |
| indirect | profile.instructor の連鎖を depth 2 以上たどって到達できるメンバー(師匠の師匠…) |
15pt |
| 上記以外 | 無関係 | 0pt |
- 区別方法: 新規カラムは追加せず、
member_relationships.labelに'direct'/'indirect'を格納して表現する。same-unit(ユニット名)・radio-coappearance(番組名)等と同様、typeごとにlabelの意味を再定義する既存パターンを踏襲する - indirect 算出:
profile.instructorの連鎖を depth 2 以上たどって機械的に算出する。新規データ収集は不要(既存の direct エッジをグラフ探索するのみ) - 循環参照防止: 連鎖探索時は訪問済みメンバー ID を
Setで管理し、既訪問メンバーには再度たどらない - 将来拡張: 連鎖では拾えない関係(本人の発言による影響表明等)を追加できるよう、
data/inputs/manual-releases.jsonと同様のパターンでdata/inputs/manual-mentor-relationships.json(手動追記ファイル、初期状態は空)を用意し、静的エッジ生成スクリプト実行時にマージする方針とする - 実装: direct/indirect の生成・手動登録マージは静的エッジ生成スクリプト
scripts/console/seed-member-relationships.tsのgenerateEdges(+loadManualMentorRelationships)で行う。動的エッジ生成スクリプトscripts/workflow/sync-relationships.ts(live/radio/event共演のみを扱う)は対象外
8-4. メンバーカラー距離(0〜20pt)
hueDiff = |hueA - hueB| を 0〜180 度に正規化(円環距離: min(|a-b|, 360-|a-b|))
score = round(20 × (1 − hueDiff / 180))
member.color(#RRGGBB)を RGB → HSV 変換して色相(hue、0〜360°)を算出する。hue は既存データに保存されておらず、スコア計算時に都度変換する(新規カラム追加なし)。
8-5. 出身地距離(0〜10pt)
| 条件 | score |
|---|---|
同一都道府県(profile.birthplace 完全一致) |
10pt |
| 同一地方(都道府県は異なる) | 5pt |
上記以外・birthplace 未設定 |
0pt |
地方判定は既存インフラを再利用する(新規マッピング不要):
prefecture_codesテーブル(aliasesにbirthplaceの表記ゆれを含む)でbirthplace→prefectureCodeを解決するtypes/live.tsのREGION_PREFECTURE_MAP(prefectureCode→Region)で地方ブロックを解決する- 両メンバーの
Regionが一致すれば同地方と判定する
birthplace が未設定、または prefecture_codes に一致するエイリアスがない場合は出身地距離を 0pt とする。
8-6. member_relationships テーブルとの関係
スコアリングは既存の member_relationships テーブルを置き換えるのではなく拡張する。
- 在籍期間重なり・メンバーカラー距離・出身地距離は、スコア計算時に
membersデータから都度算出する(DB に新規テーブル・カラムを追加しない) - 師弟関係のみ、既存
mentor-studentエッジ(静的エッジ)のlabelを書き換えて direct/indirect を表現する(§8-3)。動的エッジ(live/radio/event共演)はスコアリングの対象外であり変更しない - 4軸の計算結果(総合スコア・軸別内訳)はリクエスト時にサーバー側で算出し、永続化しない(メンバー数規模ではキャッシュテーブルを追加するほどの計算コストではないため)
9. メンバー愛称(Member.nickname、実装: #1266・#1286)
グルーピング軸 leader(加入時リーダーによるチルドレン分類、§5-3)で、グループラベルに使うリーダーの愛称。当初はリーダー限定の leaderNickname フィールドだったが、#1286 で汎用化し、メンバー詳細画面(MemberDetailView)での表示にも用いる。
9-1. データ形状
- フィールド:
member.nickname: string[](types/member.tsのMemberSchemaに定義。任意フィールド)。愛称は複数バリエーションを持つメンバーがいるため配列とする - 値: メンバーの愛称(例:
iida-kaori→['かおりん'])。登録対象は愛称が判明しているメンバーのみで、data/inputs/member-nicknames.jsonに列挙された分だけ存在する - 利用箇所:
lib/member-map-utils.tsのgetLeaderChildrenLabel。member.nicknameが登録されていれば配列の先頭(1件目)を、未登録ならmember.name(氏名)を使って「(愛称|氏名)チルドレン」ラベルを組み立てる(§5-3)components/MemberDetailView.tsx。設定されていれば「愛称」欄に配列を連結して表示する
9-2. データ投入
デプロイなしにBlobデータへ反映できるよう、以下の3点セットで構成する。
| ファイル | 役割 |
|---|---|
types/member.ts の MemberSchema.nickname |
Member のスキーマフィールド(任意の string[]) |
data/inputs/member-nicknames.json |
入力データ。{ "nicknames": [{ "memberId": string, "nicknames": string[] }] } |
scripts/patch/patch-member-nicknames.ts |
入力 JSON を読み込み、Blob の members.json の該当メンバーへ nickname をセットして書き戻す。入力に含まれないメンバーは既存の nickname を削除する。書き戻し先は Blob 本体(put())と data/work/members.json(作業用の一時コピー。確認後に削除する)の両方 |
9-3. 実装の経緯
当初は lib/member-map-utils.ts から data/leader-nicknames.json を静的import(resolveJsonModule)する実装だった。これは Next.js のビルド時にバンドルされるため、JSONを編集しただけでは本番に反映されずコミット+デプロイが必要になり、「コンテンツはBlob、コードはデプロイ」という設計原則に反していた。上記の3点セット方式に変更し、lib/member-map-utils.ts 側は member.leaderNickname を直接参照するだけにした(#1266)。
その後、愛称はリーダーに限らず多くのメンバーが持つものであり、メンバー詳細画面にも表示価値があることから、leaderNickname(単一・リーダー限定)を汎用的な nickname(配列・全メンバー対象)に置き換えた(#1286)。
10. 加入ルート(Member.joiningRoute、実装: #1283)
メンバーの加入経緯(オーディション合格・キッズ/研修生からの昇格 等)を表す属性。
10-1. データ形状
- フィールド:
member.joiningRoute: string(types/member.tsのMemberSchemaに追加。任意フィールド) - 値: 生データをそのまま保持する(正規化・3値への集約は行わない)。収集済みの値は以下の6種類:
初期メンバー(オーディションを経ない最初期の指名メンバー)オーディションキッズ昇格(ハロプロキッズからの昇格)研修生昇格(ハロプロ研修生からの昇格)留学生(海外からの留学生としての加入)その他
- 設計判断: #1267 のコメントでは表示用に「オーディション合格/キッズ・研修生からの昇格/その他」の3値への集約が提案されていたが、生データの粒度(キッズ昇格と研修生昇格の区別、初期メンバー・留学生の区別)を失わないよう、Member 側は生データを保持する方針とした
- 対象: 全メンバー(現役+OG)。未収集のメンバーは
joiningRouteを持たない(undefined)
10-2. データ投入
§9-2 の nickname と同型の3点セットを踏襲する。
| ファイル | 役割 |
|---|---|
types/member.ts の MemberSchema.joiningRoute |
Member のスキーマフィールド(任意の string) |
data/inputs/member-joining-routes.json |
入力データ。{ "joiningRoutes": [{ "memberId": string, "route": string }] } |
scripts/patch/patch-member-joining-routes.ts |
入力 JSON を読み込み、Blob の members.json の該当メンバーへ joiningRoute をセットして書き戻す。入力に含まれないメンバーは既存の joiningRoute を削除する。書き戻し先は Blob 本体と data/work/members.json(作業用の一時コピー。確認後に削除する)の両方 |
改訂履歴(バージョン降順に記載)
| バージョン | 日付 | 変更内容 |
|---|---|---|
| 1.10 | 2026-07-07 | §9 リーダー限定の Member.leaderNickname(単一)を汎用的な Member.nickname(配列)に置き換え。data/leader-nicknames.json → data/member-nicknames.json、scripts/patch/patch-leader-nicknames.ts → scripts/patch/patch-member-nicknames.ts にリネーム。MemberDetailView での愛称表示への利用を追記(#1286) |
| 1.9 | 2026-07-06 | §3 ControlAxis に加入ルート軸(joiningRoute)を追加。§5-2/§5-3 に加入ルート軸のフィルタ・グルーピング仕様(member.joiningRoute をそのままキーに使う)を追記(#1284) |
| 1.8 | 2026-07-06 | §9 リーダー愛称(Member.leaderNickname)のデータ設計を新規セクションとして追加(実装済みの#1266の内容を明文化)。§10 加入ルート(Member.joiningRoute)のデータ設計を新規追加。生データ(6種類の値)をそのまま保持する方針、data/member-joining-routes.json → scripts/patch/patch-member-joining-routes.ts によるBlobマージ方式を記載(#1283) |
| 1.7 | 2026-07-06 | 愛称データの管理方式を修正。data/leader-nicknames.json の静的importをやめ、leaderGenerationNumber と同様に Member の leaderNickname フィールド(Blob の members.json)で管理する方式に変更。scripts/patch/patch-leader-nicknames.ts を新設し、デプロイ不要で愛称を反映できるようにした(#1266) |
| 1.6 | 2026-07-06 | §3 ControlAxis にリーダー軸(leader)を追加。§5-2/§5-3 にリーダー軸のフィルタ・グルーピング仕様(getMemberLeaderId・getLeaderChildrenLabel)を追記。旧関数名 filterActiveRelationships を現行の filterDisplayRelationships に修正し、「Active メンバー」表記を実装(includeOg 対応)に合わせて「表示対象メンバー」に修正。§6 円形配置アルゴリズムの対象範囲・変数名の記述を実装(unsaved、現役+OG 全メンバー)に合わせて修正(#1266) |
| 1.5 | 2026-07-05 | §2-1 静的エッジ/動的エッジの生成元スクリプトの区分を新規追加し、§8 の実装参照先(scripts/console/seed-member-relationships.ts・lib/member-map-utils.ts)を明記(#1269) |
| 1.4 | 2026-07-04 | §8 関係性スコアリングの計算式・direct/indirect ラベル運用・出身地距離の既存インフラ再利用方針を新規追加(#1268) |
| 1.3 | 2026-07-04 | §2-1 live-coappearance の label 算出(Tour.title→Live.title→固定文言フォールバック)を追記し、event-coappearance からライブ種別イベントを除外する仕様を反映(#1263) |
| 1.2 | 2026-07-02 | §2-1 label カラムの説明・ラベル表示ルールを更新し、radio-coappearance/event-coappearance の最多共演番組名・イベント名算出方法を追記(#1236) |
| 1.1 | 2026-06-29 | §4-3 エラーレスポンス追記・§5-3 resolveGroupOverlaps の早期収束に関する補足追加 |
| 1.0 | 2026-06-29 | 初版作成(#1245) |