データソース連携インベントリ
spec:
data-integration-inventorycommit:28ee7670/ 2026-05-05
0. Summary
WingStats Manager は試合データを 2 系統 から取り込む:
- ローカルゲームログ(
StreamLogParserV2/LogParser経由、ローカルファースト) - 戦績サイト(ブラウザ自動化)(Playwright 経由、オプトイン)
両者は findMatchByOpponentUids または時刻 ±5 分窓での照合により players テーブル上で 1 行にマージされる。本ドキュメントはそれぞれが何を持っているか・何を持っていないか・現状の DB 列との対応関係を網羅的に整理し、次の改修 spec の素材を提供する。
結論サマリー:
- ローカルログのみが持つ: BAN / Pick の細部、所持キャラ 4 枠 (
available_character_ids)、TYPE マーカーによる Rank/Normal 確定判定、log_fileへの trace - ブラウザのみが持つ:
BattleId、Score (rank score) before/after、avatar URL、damage/kill/death の正確な数値、awakening kill 等の戦績詳細、シーズン横断 summary - 両方あるが粒度が違う: 試合 type(log は文字列、browser は
BattleType数値)、結果(log はWin/Lose、browser はBattleResult数値 + WinnerDetails/LoserDetails の team 構造) - 照合の弱点:
findMatchByOpponentUidsは 完全一致(対戦相手 UID 集合の正確一致)+ 直近時刻でのみ動く。saveBattleDetails(matchId 未指定経路)は 時刻 ±5 分窓 + UID 一致。複数プロファイル / 同一相手連戦 / battle_list 取得遅延に弱い
1. データソース定義
1.1 ローカルログ系
ゲームクライアントが出力する 日付.txt ログを chokidar(リアルタイム)または一括解析で読む。
| 経路 | 入力 | 出力(型) | 出典 |
|---|---|---|---|
| 一括解析 | ログファイル全文 | AnalysisResult { matches: MatchData[] } | src/utils/logAnalyzer/LogParser.ts、types.ts:138 |
| ストリーム | 行追加イベント | MatchData(試合確定時に emit) | src/utils/logAnalyzer/core/fsm/StreamFsm.ts、core/aggregator/Aggregator.ts |
| パース行 | 1 行 | LineEvent | src/utils/logAnalyzer/core/line/LineParser.ts |
得られる主な情報(types.ts:33-103):
- 試合メタ:
index,timestamp,type(Rank/Normal/null)、startTime、timestampLine - BP データ:
players[] {name, id, team, availableCharacterIds[]}、bans[] {characterId, bannerName, bannerId, bannerTeam}、picks[] {characterId, playerName, playerId, team} - 試合結果:
players: PlayerResult[] {name, result: 'Win'|'Lose', id?, availableCharacterIds?} - フェーズ:
IDLE/BP/LOADING/INGAME/RESULT(MatchPhase/types.ts:108)
得られない情報(コード上明示なし):
- BattleId(戦績サイト固有)
- Score (rank score) before/after
- damage / kill / death の 正確な数値(log は kill 通知のテキストを抽出する経路があるが、数値の信頼性は browser 経由が高い前提)
- avatar 画像 URL
- シーズン情報
1.2 ブラウザ自動化系
Playwright で xzysteam.shengtiangames.com の戦績ページを叩いて raw JSON を取得し、api_responses テーブルに保存しつつ、構造化したものを matches / players / season_summaries / season_shows に流し込む。エンドポイントごとに 4 種類の入力。
battle_list (試合一覧)
reseponses/battle_list.json:data[] のサンプル:
{
"BattleId": "1771323294000000112",
"BattleResult": 0, // 0=負け、1=勝ち想定
"StartTime": 1771323357, // Unix 秒
"RoundTime": 147, // 試合時間(秒)
"BattleType": 22, // 試合タイプ(22 = 2v2 ランク?)
"BeatHeroNum": 0, // 自分のキル数
"DeathHeroNum": 2, // 自分のデス数
"TotalDamage": 2830,
"IsMvp": false
}型: BattleListItem(electron/database/types.ts:158)
→ matches(id = BattleId)と players(自分行のみ)を INSERT OR IGNORE で作る。saveBattleListItems() (electron/database.ts:325)。
battle_detail (1 試合の詳細)
reseponses/battle_detail.json:data 構造(要旨):
data
├── BattleType
├── WinnerDetails: [PlayerEntry...]
└── LoserDetails: [PlayerEntry...]
PlayerEntry
├── BattleInfo
│ ├── BeatAwakeCnt // 覚醒中キル
│ ├── BeatCnt // 通常キル
│ ├── BeatedCnt // 被キル
│ ├── BeatFreidnCnt // 味方への誤射数(typo は API 側)
│ ├── Debarrass // デバフ解除回数
│ ├── ExportDamage // 与ダメ
│ └── Role
│ ├── code, id, RoleCode
│ ├── name, name_jp, name_ko, name_tw, english_name
│ ├── img / img_jp / img_en / img_tw / img_preview
│ ├── battle_img_left / battle_img_right / battle_img_2v2_bg
│ ├── avatar_link / avatar_link_balance / avatar_link_xcx
│ ├── images.role_lh_img
│ ├── properties (JSON 文字列で title / cv / power / health / role_intro 等)
│ └── cost, sort, status
└── UserInfo
├── Avatar // ユーザーアバター URL
├── DaDuanId // 段位 ID
├── Id // ユーザー UID(数値)
├── IsMvp
├── Name
├── ScoreAfter
└── ScoreBefore→ saveBattleDetails(BattleDetail) (electron/database.ts:316、本体は electron/database/StatsService.ts:265-345)。UPDATE players SET kill_count, death_count, damage, score_before, score_after, is_mvp, rank_id, character_id, awake_kill_count, friendly_kill_count, debuff_clear_count, avatar_url WHERE match_id = ? AND uid = ?。character_id も同時に確定させる(saveBattleListItems では NULL で挿入されるため)。
型: BattleDetail(electron/database/types.ts:268)。
season_list / battle_season_summary
reseponses/seasonlist.json:data[] { Code, Name, StartTime, EndTime } — シーズン一覧。
reseponses/battle-season-summary.json:data { T1, T2 } — チームタイプ別(T1=2v2, T2=1v1 想定)の現シーズン累計。各チーム:
ArenaScore | GradeDanId | MvpCnt | TotalCnt | WinRate→ saveSeasonSummary / saveSeasonShow(electron/database.ts:769-771)。season_summaries / season_shows テーブル(SeasonSummaryRow / SeasonShowRow、types.ts:185-237)。
winrate.json / memo.json(キャラマスタ)
戦績サイト由来のキャラ別 winrate / pick rate / ban rate。role_code ベース。これは試合データではなく キャラクターマスタ更新(yarn update-characters)に使うので本 inventory のスコープ外。
1.3 DB スキーマ
electron/database/DatabaseConnection.ts の CREATE TABLE 群(要旨。詳細は docs/database-design.md):
| テーブル | 主要列 | 書き込み元 |
|---|---|---|
matches | id (TEXT PK), timestamp, date, time, type, duration, log_file, created_at | log + browser(saveBattleListItems は INSERT OR IGNORE) |
players | id AI, match_id, uid, name, team (A/B), result, character_id, available_character_ids (JSON), kill_count, death_count, damage, score_before, score_after, is_mvp, rank_id, awake_kill_count, friendly_kill_count, debuff_clear_count, avatar_url | log(先に挿入)+ browser(saveBattleDetails で UPDATE) |
bans | id AI, match_id, character_id, banner_uid, banner_name, team | log のみ |
picks | id AI, match_id, character_id, player_uid, player_name, team | log のみ |
season_summaries | id AI, captured_at, captured_date, t{1,2}_arena_score / grade_dan_id / mvp_cnt / total_cnt / win_rate, raw_json | browser のみ |
season_shows | id AI, captured_at, captured_date, t{1,2}_max_score / arena_score / total_cnt / win_cnt / win_rate / score_map, top_role_list, raw_json | browser のみ |
api_responses | id AI, url, status, content_type, timestamp, endpoint, body, is_json, json_code, json_msg | browser のみ(ResponseStorageService) |
profiles | id AI, name, uids (JSON), player_names (JSON), log_dir, is_default, created_at | UI(profile manager) |
log_files | (未確認、CHANGELOG では言及あり) | log(startup-import) |
2. フィールド対応表
凡例: ✓ 取得・保存される / ✗ 取れない / △ 部分的・条件付き / — 該当なし
2.1 試合メタ
| 項目 | log | battle_list | battle_detail | DB 列 (matches) | UI 利用 | 備考 |
|---|---|---|---|---|---|---|
| 試合 ID | △ ローカル生成(timestamp ベース) | ✓ BattleId | — | matches.id (TEXT) | MatchListTab、MatchDetail | log 経路は内部 ID、browser 経路は BattleId をそのまま使う。両者の一致は照合ロジック頼み |
| 試合開始時刻 | ✓ timestamp (ISO) | ✓ StartTime (Unix 秒) | ✓ data.StartTime (Unix 秒) | matches.timestamp (TEXT ISO) | 全画面 | 単位差: log は ISO 文字列、browser は Unix 秒 → ISO 変換(saveBattleListItems 内)。誤差確認は §4 |
| 日付 | ✓ date (YYYY-MM-DD) | △ StartTime から導出 | — | matches.date | MatchListTab フィルタ | |
| 時刻 (time) | △ 別列に保存している経路あり | — | — | matches.time | (未調査) | log のみ |
| 試合 type (Rank/Normal) | ✓ TYPE マーカー優先 → BP数据日志 → プレイヤー数 | △ BattleType(数値、対応マップ要) | △ BattleType | matches.type (TEXT) | MatchListTab、PlayerDashboard | log は 'Rank'/'Normal'/null、browser は 22 等の数値 → 文字列マッピングが必要(コード上どこで変換しているか要再確認) |
| duration(試合時間) | △ log では明確に取れない | ✓ RoundTime (秒) | ✗(直接フィールドなし) | matches.duration (INTEGER) | (未調査) | browser の RoundTime が信頼できる |
| log_file | ✓ 生ログのファイルパス | — | — | matches.log_file | DB ビューア、デバッグ | trace 用途 |
| BattleId(戦績サイト) | ✗ | ✓ | △(URL から逆引き、BrowserAutomationService.ts:451) | matches.id(browser 経路) | — | log 経由で作った match に BattleId は 付かない(次の改修候補) |
| BattleResult(自視点 win/lose 数値) | △ 自分の result で判定可 | ✓ BattleResult (0/1) | △ Winner/Loser に分かれているので集計可 | players.result('Win'/'Lose')に変換 | MatchListTab、Dashboard | log は文字列、browser は数値 |
2.2 チーム / プレイヤー
| 項目 | log | battle_list | battle_detail | DB 列 (players) | UI 利用 | 備考 |
|---|---|---|---|---|---|---|
| チーム所属 (A/B) | ✓ TeamA/TeamB | △ 自分のみ(敵味方の分離なし) | ✓ Winner/Loser を team A/B に変換できる | players.team ('A'/'B') | MatchDetail | log は明示的、browser は勝敗ベースで暗黙 |
| プレイヤー UID | ✓ 文字列 | △ 自分のみ(呼び出し側引数で渡す) | ✓ UserInfo.Id (数値) | players.uid (TEXT) | 全画面 | 書式違い: log=string, browser=number → .toString() 変換(StatsService:336) |
| プレイヤー名 | ✓ | △ 自分のみ | ✓ UserInfo.Name | players.name (TEXT) | 全画面 | 同 UID で名前変更があるとき優先順が曖昧 |
| アバター URL | ✗ | ✗ | ✓ UserInfo.Avatar | players.avatar_url | MatchDetailView?(要確認) | UI で表示しているか未確認 |
| 段位 ID (DaDuanId) | ✗ | ✗ | ✓ UserInfo.DaDuanId | players.rank_id | 戦績バッジ UI 想定 | |
| ScoreBefore / ScoreAfter | ✗ | ✗ | ✓ | players.score_before/score_after | MatchDetailView (line 180-184) | 勝率推移の精度に直結 |
| キャラクター | ✓ BP の Pick から | ✗(NULL で挿入) | ✓ BattleInfo.Role.RoleCode | players.character_id | 全画面 | log → INSERT 時に決定、browser → UPDATE で上書き |
| 所持キャラ枠 (4 キャラ) | ✓ availableCharacterIds[] | ✗ | ✗ | players.available_character_ids (JSON) | PlayerDashboard、MatchDetail | only-in-log の代表例 |
| IsMvp | ✗ | ✓ 自分のみ | ✓ 全員 | players.is_mvp (INT 0/1) | MatchDetailView (line 188) | |
| キル数 (BeatCnt) | ✗ | ✓ BeatHeroNum(自分のみ) | ✓ BattleInfo.BeatCnt | players.kill_count | MatchDetailView, MatchTableRow | browser 経由で全員分が揃う |
| デス数 (BeatedCnt) | ✗ | ✓ DeathHeroNum(自分のみ) | ✓ BattleInfo.BeatedCnt | players.death_count | 同上 | |
| 与ダメ (ExportDamage) | ✗ | ✓ TotalDamage(自分のみ) | ✓ BattleInfo.ExportDamage | players.damage | 同上 | log は取れず、browser 経由のみ |
| 覚醒キル (BeatAwakeCnt) | ✗ | ✗ | ✓ | players.awake_kill_count | (未確認) | only-in-detail |
| 味方誤射 (BeatFreidnCnt) | ✗ | ✗ | ✓ | players.friendly_kill_count | (未確認) | typo 注意 |
| デバフ解除 (Debarrass) | ✗ | ✗ | ✓ | players.debuff_clear_count | (未確認) |
2.3 Pick / Ban
| 項目 | log | battle_list | battle_detail | DB 列 | UI 利用 | 備考 |
|---|---|---|---|---|---|---|
| BAN リスト | ✓ bans[] | ✗ | ✗ | bans.character_id, banner_uid/name, team | MatchDetail、PlayerDashboard 「BAN 統計」 | only-in-log |
| Pick リスト | ✓ picks[] | ✗ | △ Role.RoleCode から復元可(実装してない) | picks.character_id, player_uid/name, team | MatchDetail、PlayerDashboard 「Pick 統計」 | log のみ。browser detail から再構築する余地はあるが、現状は log 依存 |
| Pick 候補(fallback) | ✓ isCandidateFallback, candidateCharacterIds[] | ✗ | ✗ | (DB に candidateCharacterIds 列なし) | (UI 未使用?) | log 解析中の暫定情報、DB に永続化されていない |
2.4 戦績詳細(battle stats)
§2.2 の下半分と重複するが、ここでは「すべて browser 経由」が要点:
| 項目 | log | battle_list | battle_detail | DB 列 | UI 利用 | 備考 |
|---|---|---|---|---|---|---|
| kill_count / death_count | ✗ | △(自分のみ) | ✓ 全員 | players.kill_count/death_count | MatchDetailView, MatchTableRow | browser 必須 |
| damage | ✗ | △(自分のみ)TotalDamage | ✓ 全員 ExportDamage | players.damage | 同上 | |
| score_before / score_after | ✗ | ✗ | ✓ 全員 | players.score_before/score_after | MatchDetailView | rank score 推移の真実 |
| is_mvp | ✗ | △(自分のみ) | ✓ 全員 | players.is_mvp | MatchDetailView | |
| rank_id (DaDuanId) | ✗ | ✗ | ✓ 全員 | players.rank_id | (未確認) | |
| awake_kill / friendly_kill / debuff_clear | ✗ | ✗ | ✓ 全員 | 専用列あり | (未確認) | |
| avatar_url | ✗ | ✗ | ✓ 全員 | players.avatar_url | (未確認) |
2.5 シーズン / その他
| 項目 | log | season_list | battle-season-summary | season-show(同 endpoint 別構造?) | DB 列 | 備考 |
|---|---|---|---|---|---|---|
| シーズン一覧 (Code, Name, StartTime, EndTime) | ✗ | ✓ | ✗ | (api_responses に raw、専用列はなし) | キャラマスタ生成の補助 | |
| 当シーズン累計 (T1/T2 別 ArenaScore, GradeDanId, MvpCnt, TotalCnt, WinRate) | ✗ | ✗ | ✓ | ✗ | season_summaries.t{1,2}_* | プレイヤーダッシュボードで「現在のシーズン累計」を表示するソース |
| シーズン show(試合数 / WinCnt / ScoreMap / TopRoleList) | ✗ | ✗ | ✗ | ✓(別 endpoint) | season_shows.t{1,2}_* + top_role_list (JSON) | 別途 endpoint 経由 |
| キャラ画像系 URL(5〜8 種) | ✗ | ✗ | ✗ | ✓ BattleInfo.Role.img / img_jp / img_en / img_tw / img_preview / battle_img_* | (DB に列なし) | 現状 public/images/starward/ の ローカル画像 を使うため未保存 |
| キャラ properties (CV / 紹介文 / power / health) | ✗ | ✗ | ✗ | ✓(JSON 文字列でネスト) | キャラマスタ補助 | reseponses/winrate.json 経由の yarn update-characters で別途取り込み |
3. 差分レポート
3.1 only in log(ローカルログのみが持つ)
| 項目 | 現状の挙動 | 想定される問題 | 修正規模 | 依存 |
|---|---|---|---|---|
| BAN 詳細 (banner UID / 名前 / team) | bans テーブルに保存。UI で表示済み | log を取り込んでいないユーザー(browser のみ運用)は BAN 統計が空になる | M | browser から取得する API がない / 機能要望次第 |
| Pick の細部(candidateCharacterIds, isCandidateFallback) | 解析時のメタだが DB に永続化されていない | デバッグ時にどの判定経路で character が決まったかが分からない | S | DB スキーマ追加(picks.is_candidate_fallback)or 削除して諦める |
| 所持キャラ 4 枠 | log でのみ取れる。available_character_ids JSON 列に保存。Dashboard で活用 | browser のみ運用では 4 枠統計が空 | L | 戦績サイトに該当機能ない場合は仕様として諦め |
| TYPE マーカーによる Rank/Normal 確定判定 | log の TYPE: 行が最優先 | browser の BattleType 数値→文字列のマッピングが log と一致しているか未検証 | S | マッピング表の文書化 |
log_file パス(trace 用) | log のみ。browser 経由 INSERT は NULL | browser 経由で作られた match のデバッグ手がかりが減る | S | 列の意味として OK(browser 経由は raw を api_responses に持つ) |
3.2 only in browser(ブラウザのみが持つ)
| 項目 | 現状の挙動 | 想定される問題 | 修正規模 | 依存 |
|---|---|---|---|---|
| BattleId | saveBattleListItems 経由で作る match の id に直接使われる | log 経由で作った match には付かない。後から battle_list を取り込んでも、log 由来の match と紐付かない可能性 | M | 照合ロジック改修(§4 も参照) |
| ScoreBefore / ScoreAfter | players.score_before/score_after に UPDATE。MatchDetailView で表示 | log のみ運用ではすべて NULL | S | 仕様として OK(browser opt-in) |
| アバター URL / 段位 ID | UI 利用は未確認 | UI 改修で活かせる余地あり(player chip にアバター表示など) | S | UI 仕様検討 |
| 全プレイヤーの kill / death / damage 等の正確値 | saveBattleDetails でまとめて UPDATE | battle_detail 取得が遅延 / 失敗するとずっと NULL | M | リトライ戦略・履歴遡及 |
| シーズン累計 / 段位(season_summaries / season_shows) | 別テーブルに raw 保存 | UI で活用しているか確認要(ダッシュボードで使う設計だが現状の UI 接続を未確認) | M | UI 設計 |
3.3 両方あり / 優先順あり
| 項目 | 現状の優先順 | 妥当性 | 修正規模 | 依存 |
|---|---|---|---|---|
| 試合 type (Rank/Normal) | log が先(INSERT 時に決まる)→ browser の BattleType で上書きしていない | log の判定が正確(TYPE マーカー優先)なので OK。browser の数値 type は補助に留める | — | (現状 OK) |
| character_id | log の Pick で INSERT → battle_detail で UPDATE 上書き | browser detail の方が「実際に使ったキャラ」として信頼できるので妥当 | — | (現状 OK) |
| プレイヤー名 | log の名前が INSERT、browser detail の名前で 上書きしない(UPDATE 文に name 列なし) | 同 UID で名前変更があるとき log 名が残る。browser から取った最新名で上書きしたいケースもある | S | 仕様確認次第 |
| 試合 result(Win/Lose) | log が先に決定 | 一貫しているはず(同じ試合なら結論は同じ)。差分が出たら異常 | — | 整合性チェックがあれば良い |
3.4 両方あり / 書式違い
| 項目 | log | browser | 現状の変換 | 注意点 |
|---|---|---|---|---|
| UID | string | number (UserInfo.Id) | player.uid.toString()(StatsService:336) | leading zero がある UID で破綻するケースは無さそう |
| 試合開始時刻 | ISO 文字列 | Unix 秒 | saveBattleListItems 内で ISO に変換 | タイムゾーン: new Date(unix*1000).toISOString() は UTC 出力。log 側は localtime のままならズレる可能性 → §4.2 のケース |
| BattleType | 'Rank' / 'Normal' / null | 数値(22 等) | log → そのまま挿入。browser → 数値のまま(?要確認) | 一貫したマッピング表が無いと UI でフィルタが破綻する |
| BattleResult | 'Win' / 'Lose' | 0 / 1 | saveBattleListItems 内で変換(要確認) |
3.5 両ソース不在 / UI 要望あり(推測)
ヒアリング次第のセクション。以下は実装計画書 / 改善計画から拾った想定:
| 項目 | 用途 | 取得経路の候補 | 修正規模 |
|---|---|---|---|
| 対戦相手の所持キャラ 4 枠(自分以外) | overlay の B-3 提案 | log の BP データから他プレイヤー分も取れる場合あり(実装次第) | M |
| 試合内のキル時刻軸 | 配信向けハイライト | log のキル通知行を時系列でまとめる | L |
| シーズン横断の対面別勝率 | Dashboard 拡張 | DB 集計のみで OK(既存データで足りる) | S |
3.6 DB 列にあるが UI 未使用(要監査)
| 列 | 由来 | UI で読まれている箇所 | 削除候補? |
|---|---|---|---|
players.avatar_url | browser detail | (要 grep)MatchDetailView で読まれているか未確認 | UI で使いたい候補 |
players.rank_id (DaDuanId) | browser detail | (要 grep) | 同上 |
players.awake_kill_count / friendly_kill_count / debuff_clear_count | browser detail | (要 grep) | UI で詳細スタッツを出す要望次第 |
season_shows.top_role_list (JSON) | browser shows | (要 grep) | データ取得はしているが活用未確認 |
season_*.raw_json | デバッグ用バックアップ | UI 利用なし | 残しておく(trace 用途) |
3.7 雑多な気づき
BeatFreidnCntの typo は API 側。こちらが直すと API 変更時に壊れるので命名はそのまま受け入れて、DB 列名 (friendly_kill_count) で修正してある(OK)。Role.propertiesは JSON 文字列のさらに文字列化 という二重エンコード。パースするときはJSON.parse(JSON.parse(props))相当が要る場合がある。Role.images.role_lh_imgのような未使用画像 URL も含まれる。全部取り込まずキャラマスタ更新時にだけ参照する方針でよさそう。
4. 照合ロジックの仕様
4.1 findMatchByOpponentUids(opponentUids: string[], startTime: number): string | null
実体: electron/database.ts:386-416、export: :767。
入力:
opponentUids: string[]— 自分以外(敵チーム + 味方の自分以外)のプレイヤー UID 文字列startTime: number— 試合の Unix 秒(戦績サイト由来のStartTime)
マッチング条件:
playersテーブルからuid IN (?, ?, …)の行を引き、match_idで GROUP BY、HAVING COUNT(DISTINCT uid) = ${opponentUids.length}→ 対戦相手 UID の集合が完全一致 するmatch_idを候補として全件取得- 候補が 1 件: そのまま返す
- 候補が複数件: 各候補の
matches.timestampを ms に直し、startTime * 1000との 絶対値差が最小 のものを返す - 候補ゼロ:
null
戻り値: match_id (TEXT) or null
副作用: なし(read-only の SELECT のみ)
4.2 取りこぼしケース
| ケース | 再現条件 | 現状の挙動 | 回避策 |
|---|---|---|---|
| opponent UIDs が部分一致 | log 取り込みが失敗して 1 名分の player レコードが欠けている | HAVING COUNT(DISTINCT) = N で落ちる → null | log 取り込み失敗を別途検知。あるいは >= N-1 の許容を入れる(リスクあり) |
| 同一相手と短時間連戦 | 同 4 人で続けて 2 試合 | 候補 2 件 → startTime 最近接で選ぶ。間違った試合に detail が UPDATE される可能性 | startTime の許容幅を狭める(現状は最近接、許容なし)。あるいは BattleId を別経路で持たせる |
| StartTime のずれ | log の試合終了時刻と browser の StartTime が ±N 秒ずれる | saveBattleDetails 経路(matchId 未指定)は ±300 秒の窓で検索。findMatchByOpponentUids は最近接で 1 件選ぶので比較的ロバスト | ±300s は経験則。短時間連戦と組み合わさるとミスマッチ |
| 複数プロファイル | 1 台で 2 アカウントを切り替えて使う場合、両方の試合を 1 つの DB に持つ | findMatchByOpponentUids はプロファイル境界を見ない | profile 列を players に持つ / プロファイルごとに DB 分離 |
| battle_list の取得遅延 | log で試合は取り込まれたが、戦績サイト側がまだ反映されていない | executeFullWorkflow (BrowserAutomationService.ts) のリトライで補完される。ただしユーザーが workflow を手動で叩かないと進まない | UI で「未 detail 試合」を可視化(既に部分的に実装あり? 要確認) |
| log なしで battle_list を先に取り込み | log 自動監視 OFF + browser 自動化のみ運用 | saveBattleListItems で INSERT OR IGNORE で BattleId をそのまま match.id にして作る。Pick / Ban は永遠に空のまま | 仕様として OK(log opt-in)。UI で「Pick/Ban 不明」を区別表示すべき |
| opponentUids が 0 件 | サンプルデータが壊れている | 即 null を返す | 例外的だが、上位呼び出し側がログを出すべき |
| uid が string と number で混在 | log 由来 players.uid は string、browser 検索キーも .toString() で文字列化 | 一致するはず | 万が一 leading zero を含む UID があるとき型変換でズレる可能性(実例なし) |
4.3 副作用と保証
findMatchByOpponentUids自体は read-only。- 呼び出し元
BrowserAutomationService.ts:556/:928は、見つかったmatchIdをsaveBattleDetails({ matchId, ... })に渡す。matchId 未指定経路(StatsService.saveBattleDetails:265-345)が動くのは log 由来 match と既に紐付いているケースで、そこでも ±300 秒窓 + UID 一致で UPDATE 対象を選ぶ。 - どちらの経路も
INSERTではなくUPDATEなので、誤った match に書いても 元の log データは破壊しない(character_id を上書きするケースだけ注意)。
5. 推奨アクションリスト
優先度順。次 spec の requirements.md にそのまま貼れる粒度。
[x] A1: BattleId を log 由来 match にも紐付ける(M / DB 変更) — spec
data-integration-improvement(PR #34) で実装済み- 動機: §3.2 BattleId / §4.2 「短時間連戦」「StartTime ずれ」の両方を一気に緩和
- 案:
matches.battle_id列を追加 →findMatchByOpponentUids成功時にBattleIdをmatchesに書き戻す。以降の照合はBattleId直引きで OK - 依存: マイグレーション必要、
saveBattleDetailsの経路調整 - 実装:
findMatchByBattleIdを新設し、BrowserAutomationServiceの matchId 解決を 3 段(BattleId → UID 集合 → 時刻 fuzzy)に変更。saveBattleDetailsで UPDATE 1 件以上のときmatches.battle_idが NULL のみ書き戻す
[x] A2: BattleType の数値 → 文字列マッピングを単一ソース化(S) — spec
data-integration-improvement(PR #34) で実装済み- 動機: §3.4 書式違い。log の
'Rank'/'Normal'と browser の22の対応がコード散在 - 案:
src/utils/logAnalyzer/battleTypeMap.ts(仮)に対応表を集約 → log 解析側 / browser 取り込み側の双方が import - 実装:
src/utils/logAnalyzer/battleType/battleTypeMap.tsに集約。既知 ID(21, 22)→'2v2'、未知 →'Unknown'。saveBattleListItemsをmapBattleTypeId()経由に置換
- 動機: §3.4 書式違い。log の
[ ] A3: avatar_url / rank_id / awake_kill_count 等の UI 反映を点検(S〜M)
- 動機: §3.6 DB 列にあるが UI 未使用。データを取っているのに見えていないなら無駄
- 案:
MatchDetailView/MatchTableRowの表示項目を再設計 → 配信オーバーレイの拡張候補(B 系)と接続
[ ] A4:
saveBattleListItems経路でplayers.nameを UPDATE 上書きする選択肢(S)- 動機: §3.3 名前の優先順不確定
- 案: 設定で「browser 取得時に最新名で上書き」をオプトインに
[ ] A5: opponent UIDs 部分一致への耐性を判断(M)
- 動機: §4.2 1 名分欠けで丸ごと miss する設計上の弱点
- 案:
>= N - 1の許容を入れる or 入れない、を実データで判断(false-positive リスク評価)
[ ] A6: 複数プロファイル分離(L / 仕様判断あり)
- 動機: §4.2 複数プロファイル
- 案:
players.profile_id列を追加 + 照合時に絞り込み
[ ] A7: 過去 log の遡及 enrich(M)
- 動機: §3.2 battle_list 取得遅延
- 案: 「過去 N 件の matches で
score_beforeが NULL のものを再取得」する運用ボタン or 自動
[x] A8:
picks.is_candidate_fallback等の log 解析メタを永続化するか削除するか決める(S) — specdata-integration-improvement(PR #34) で 永続化 に決定- 動機: §3.1。デバッグ用途で有用かを判断
- 判断: 既存 UI(
MatchDetailView.tsx)でisCandidateFallback/candidateCharacterIdsが参照されていたが DB 列が無く log 由来の in-memory データのみで動いていた → DB 列を追加して永続化 - 実装:
picks.is_candidate_fallback(INTEGER 0/1) とpicks.candidate_character_ids(TEXT JSON) を追加、PickData型に対応フィールド、mapPickRowヘルパーで JSON parse / boolean 変換を一元化
[x] A9:
Role.propertiesの二重 JSON エンコードに対する正規化(S) —feature/data-integration-A9-role-propertiesで実装済み- 動機: §3.7 雑多な気づき。キャラマスタ更新時に hidden bug の温床
- 案:
update-characters内でパース → DB 列としては正規化された値だけ保存 - 実装:
scripts/data/parseRoleProperties.tsを新設(1 重/2 重エンコード/object/null をすべて吸収、最大 3 階層まで展開)。CharacterDBItemにproperties構造化フィールドを追加し、title_jp/cv_jp/power/health等を正規化済みでsrc/data/characters.jsonに永続化(68 キャラ更新済み)。parseRoleProperties.test.tsで 15 ケース(null / 単一 / 二重 / 不正 JSON / 数値強制 / 暴走防止)を網羅
[x] A10: UID の string / number 境界をテスト化(S) — spec
data-integration-improvement(PR #34) で実装済み- 動機: §3.4 書式違い。今は事故が起きていないが、将来 leading zero を含む UID が出たら破綻
- 案:
findMatchByOpponentUids/saveBattleDetailsのユニットテストに「数値 UID」「文字列 UID」のミックスケースを足す - 実装:
electron/database/__tests__/StatsService.battleId.test.ts(7 ケース)+MatchRepository.battleId.test.ts(6 ケース)。number / leading-zero string / 安全整数超過 string / 混在を網羅
6. メタデータ
- 調査 commit:
28ee7670b6264afc6980fa691d866b5b91e46fab - 調査日: 2026-05-05
- 調査範囲のソース:
src/utils/logAnalyzer/{types,LogParser}.ts、core/{line,fsm,aggregator}/*electron/database/{DatabaseConnection,types,MatchRepository,StatsService}.tselectron/database.tselectron/services/{BrowserAutomationService,ResponseStorageService}.tselectron/ipc/BrowserAutomationHandlers.tsreseponses/{battle_list,battle_detail,seasonlist,battle-season-summary,winrate}.jsonsrc/components/tabs/db-viewer/components/MatchDetailView.tsxほか
- 関連ドキュメント:
- 次 spec のたたき台: §5 推奨アクションリストをそのまま要件カタログに使う。優先度の上位(A1 / A2 / A3)から spec 化する想定。
- 進捗(2026-05-06):
data-integration-improvementspec (PR #34) で A1 / A2 / A8 / A10 を完了。feature/data-integration-A9-role-propertiesで A9 を完了。残タスクは A3 / A4 / A5 / A6 / A7(UI 反映点検 / 名前ポリシー / 部分一致 / 複数プロファイル / 過去 log enrich)。