From c59fd2a46aa827636c7bd9f65d9d73b25bae3c48 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Fri, 16 Oct 2026 09:25:18 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BA=AC=E7=8E=8B=E7=B7=9A=E7=B3=BB?= =?UTF-8?q?=E7=B5=B1=E3=81=AE=E5=81=9C=E8=BB=8A=E9=A7=85=E3=82=92=E7=85=A7?= =?UTF-8?q?=E5=90=88=E3=81=97=E6=80=A5=E8=A1=8C=E3=81=A8=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=83=8A=E3=83=BC=E3=81=AE=E5=81=9C=E8=BB=8A=E6=BC=8F=E3=82=8C?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + data/5!station_station_types.csv | 12 +- data_validator/tests/fixtures/keio_stops.csv | 54 +++++++++ .../tests/keio_stopping_patterns.rs | 109 ++++++++++++++++++ docs/keio-stopping-pattern-audit.md | 30 +++++ 5 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 data_validator/tests/fixtures/keio_stops.csv create mode 100644 data_validator/tests/keio_stopping_patterns.rs create mode 100644 docs/keio-stopping-pattern-audit.md diff --git a/README.md b/README.md index d68c86ae..1b4ab33c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ A GraphQL API that provides nearby Japanese train stations and bus stops, runnin - For system architecture and design decisions, see [docs/architecture.md](docs/architecture.md). - For the design of the transfer route search (`connectedRoutes`, RAPTOR), see [docs/route-search.md](docs/route-search.md). - For the source-backed operating constraints used by timetable-free search, see [docs/route-service-audit.md](docs/route-service-audit.md). +- For the Keio stopping-pattern audit and regression checks, see [docs/keio-stopping-pattern-audit.md](docs/keio-stopping-pattern-audit.md). - For technical debt analysis and architectural concerns, see [docs/technical_debt.md](docs/technical_debt.md). - For the record of the Cloudflare Workers migration, see [docs/cloudflare-workers-migration.md](docs/cloudflare-workers-migration.md). - For the published GraphQL schema, see [schema/public.graphql](schema/public.graphql). @@ -109,3 +110,4 @@ make deploy-production # production -> stationapi The data lives inside the WASM binary, so **a data change needs a rebuild and a redeploy**; it is not picked up at runtime. + diff --git a/data/5!station_station_types.csv b/data/5!station_station_types.csv index c2827588..81344aa9 100644 --- a/data/5!station_station_types.csv +++ b/data/5!station_station_types.csv @@ -2022,7 +2022,7 @@ DEFAULT,9950213,203,64,0,修善寺 DEFAULT,2400101,400,65,0,新宿 DEFAULT,2400104,400,65,1,笹塚 DEFAULT,2400105,400,65,1,代田橋 -DEFAULT,2400106,400,65,1,明大前 +DEFAULT,2400106,400,65,0,明大前 DEFAULT,2400107,400,65,1,下高井戸 DEFAULT,2400108,400,65,1,桜上水 DEFAULT,2400109,400,65,1,上北沢 @@ -2049,12 +2049,12 @@ DEFAULT,2400129,400,65,0,高幡不動 DEFAULT,2400130,400,65,1,南平 DEFAULT,2400131,400,65,1,平山城址公園 DEFAULT,2400132,400,65,1,長沼 -DEFAULT,2400133,400,65,1,北野 +DEFAULT,2400133,400,65,0,北野 DEFAULT,2400134,400,65,0,京王八王子 DEFAULT,2400101,401,66,0,新宿 DEFAULT,2400104,401,66,1,笹塚 DEFAULT,2400105,401,66,1,代田橋 -DEFAULT,2400106,401,66,1,明大前 +DEFAULT,2400106,401,66,0,明大前 DEFAULT,2400107,401,66,1,下高井戸 DEFAULT,2400108,401,66,1,桜上水 DEFAULT,2400109,401,66,1,上北沢 @@ -11375,7 +11375,7 @@ DEFAULT,2400110,382,285,1,八幡山 DEFAULT,2400111,382,285,1,芦花公園 DEFAULT,2400112,382,285,0,千歳烏山 DEFAULT,2400113,382,285,1,仙川 -DEFAULT,2400114,382,285,1,つつじヶ丘 +DEFAULT,2400114,382,285,0,つつじヶ丘 DEFAULT,2400115,382,285,1,柴崎 DEFAULT,2400116,382,285,1,国領 DEFAULT,2400117,382,285,1,布田 @@ -28232,7 +28232,7 @@ DEFAULT,2400110,382,757,1,八幡山 DEFAULT,2400111,382,757,1,芦花公園 DEFAULT,2400112,382,757,0,千歳烏山 DEFAULT,2400113,382,757,1,仙川 -DEFAULT,2400114,382,757,1,つつじヶ丘 +DEFAULT,2400114,382,757,0,つつじヶ丘 DEFAULT,2400115,382,757,1,柴崎 DEFAULT,2400116,382,757,1,国領 DEFAULT,2400117,382,757,1,布田 @@ -28336,7 +28336,7 @@ DEFAULT,2400110,382,760,1,八幡山 DEFAULT,2400111,382,760,1,芦花公園 DEFAULT,2400112,382,760,0,千歳烏山 DEFAULT,2400113,382,760,1,仙川 -DEFAULT,2400114,382,760,1,つつじヶ丘 +DEFAULT,2400114,382,760,0,つつじヶ丘 DEFAULT,2400115,382,760,1,柴崎 DEFAULT,2400116,382,760,1,国領 DEFAULT,2400117,382,760,1,布田 diff --git a/data_validator/tests/fixtures/keio_stops.csv b/data_validator/tests/fixtures/keio_stops.csv new file mode 100644 index 00000000..34d4f6bb --- /dev/null +++ b/data_validator/tests/fixtures/keio_stops.csv @@ -0,0 +1,54 @@ +station_number,stopping_types,source,checked_on +1,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko01_shinjuku/,2026-10-16 +2,各駅停車|快速|区間急行|急行,https://www.keio.co.jp/train/station/ko02_hatsudai/,2026-10-16 +3,各駅停車|快速|区間急行|急行,https://www.keio.co.jp/train/station/ko03_hatagaya/,2026-10-16 +4,各駅停車|快速|区間急行|急行|特急,https://www.keio.co.jp/train/station/ko04_sasazuka/,2026-10-16 +5,各駅停車,https://www.keio.co.jp/train/station/ko05_daitabashi/,2026-10-16 +6,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko06in08_meidaimae/,2026-10-16 +7,各駅停車|快速,https://www.keio.co.jp/train/station/ko07_shimo-takaido/,2026-10-16 +8,各駅停車|快速|区間急行|急行,https://www.keio.co.jp/train/station/ko08_sakurajosui/,2026-10-16 +9,各駅停車,https://www.keio.co.jp/train/station/ko09_kami-kitazawa/,2026-10-16 +10,各駅停車|快速,https://www.keio.co.jp/train/station/ko10_hachiman-yama/,2026-10-16 +11,各駅停車,https://www.keio.co.jp/train/station/ko11_roka-koen/,2026-10-16 +12,各駅停車|快速|区間急行|急行|特急,https://www.keio.co.jp/train/station/ko12_chitose-karasuyama/,2026-10-16 +13,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko13_sengawa/,2026-10-16 +14,各駅停車|快速|区間急行|急行,https://www.keio.co.jp/train/station/ko14_tsutsujigaoka/,2026-10-16 +15,各駅停車,https://www.keio.co.jp/train/station/ko15_shibasaki/,2026-10-16 +16,各駅停車,https://www.keio.co.jp/train/station/ko16_kokuryo/,2026-10-16 +17,各駅停車,https://www.keio.co.jp/train/station/ko17_fuda/,2026-10-16 +18,各駅停車|快速|区間急行|急行|特急,https://www.keio.co.jp/train/station/ko18_chofu/,2026-10-16 +19,各駅停車|快速,https://www.keio.co.jp/train/station/ko19_nishi-chofu/,2026-10-16 +20,各駅停車|快速,https://www.keio.co.jp/train/station/ko20_tobitakyu/,2026-10-16 +21,各駅停車|快速,https://www.keio.co.jp/train/station/ko21_musashinodai/,2026-10-16 +22,各駅停車|快速,https://www.keio.co.jp/train/station/ko22_tama-reien/,2026-10-16 +23,各駅停車|快速|区間急行|急行,https://www.keio.co.jp/train/station/ko23_higashi-fuchu/,2026-10-16 +24,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko24_fuchu/,2026-10-16 +25,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko25_bubaigawara/,2026-10-16 +26,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko26_nakagawara/,2026-10-16 +27,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko27_seiseki-sakuragaoka/,2026-10-16 +28,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko28_mogusaen/,2026-10-16 +29,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko29_takahatafudo/,2026-10-16 +30,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko30_minamidaira/,2026-10-16 +31,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko31_hirayamajoshi-koen/,2026-10-16 +32,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko32_naganuma/,2026-10-16 +33,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko33_kitano/,2026-10-16 +34,各駅停車|快速|区間急行|急行|特急|京王ライナー,https://www.keio.co.jp/train/station/ko34_keio-hachioji/,2026-10-16 +35,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko35_keio-tamagawa/,2026-10-16 +36,各駅停車|快速|区間急行|急行|特急,https://www.keio.co.jp/train/station/ko36_keio-inadazutsumi/,2026-10-16 +37,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko37_keio-yomiuri-land/,2026-10-16 +38,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko38_inagi/,2026-10-16 +39,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko39_wakabadai/,2026-10-16 +40,各駅停車|快速|区間急行|急行|特急|京王ライナー,https://www.keio.co.jp/train/station/ko40_keio-nagayama/,2026-10-16 +41,各駅停車|快速|区間急行|急行|特急|京王ライナー,https://www.keio.co.jp/train/station/ko41_keio-tama-center/,2026-10-16 +42,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko42_keio-horinouchi/,2026-10-16 +43,各駅停車|快速|区間急行|急行|特急|京王ライナー,https://www.keio.co.jp/train/station/ko43_minami-osawa/,2026-10-16 +44,各駅停車|快速|区間急行,https://www.keio.co.jp/train/station/ko44_tamasakai/,2026-10-16 +45,各駅停車|快速|区間急行|急行|特急|京王ライナー,https://www.keio.co.jp/train/station/ko45_hashimoto/,2026-10-16 +46,各駅停車|急行|特急,https://www.keio.co.jp/train/station/ko46_fuchukeiba-seimommae/,2026-10-16 +47,各駅停車|急行,https://www.keio.co.jp/train/station/ko47_tama-dobutsukoen/,2026-10-16 +48,各駅停車|快速|区間急行|特急,https://www.keio.co.jp/train/station/ko48_keio-katakura/,2026-10-16 +49,各駅停車|快速|区間急行|特急,https://www.keio.co.jp/train/station/ko49_yamada/,2026-10-16 +50,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko50_mejirodai/,2026-10-16 +51,各駅停車|快速|区間急行|特急,https://www.keio.co.jp/train/station/ko51_hazama/,2026-10-16 +52,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko52_takao/,2026-10-16 +53,各駅停車|快速|区間急行|急行|特急|京王ライナー|Mt.TAKAO号,https://www.keio.co.jp/train/station/ko53_takaosanguchi/,2026-10-16 diff --git a/data_validator/tests/keio_stopping_patterns.rs b/data_validator/tests/keio_stopping_patterns.rs new file mode 100644 index 00000000..00c94fa7 --- /dev/null +++ b/data_validator/tests/keio_stopping_patterns.rs @@ -0,0 +1,109 @@ +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::path::Path; + +use csv::StringRecord; + +fn records(path: &Path) -> Vec { + csv::Reader::from_path(path) + .unwrap() + .records() + .collect::>() + .unwrap() +} + +// Line-specific station IDs at junctions must appear on both sides, in order. +fn sections(group: u32) -> Vec<(u32, u32, u32)> { + match group { + 65 | 67..=71 => vec![(24001, 1, 34)], + 66 | 252 | 416 | 760..=762 => vec![(24001, 1, 18), (24002, 18, 45)], + 253 | 412..=415 | 758 | 759 => vec![(24001, 1, 33), (24003, 33, 53)], + 254 | 285..=287 | 757 => { + vec![(24007, 1, 4), (24001, 4, 18), (24002, 18, 45)] + } + 255 => vec![(24007, 1, 4), (24001, 4, 33), (24003, 33, 53)], + 756 => vec![(24007, 1, 4), (24001, 4, 34)], + 763 | 764 => vec![(24001, 1, 23), (24004, 23, 46)], + 765 => vec![(24004, 23, 46)], + 766 => vec![(24001, 1, 29), (24005, 29, 47)], + 767 => vec![(24005, 29, 47)], + _ => panic!("unaudited Keio group: {group}"), + } +} + +#[test] +fn existing_keio_patterns_match_official_stops_and_complete_station_sequences() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + let data = root.join("data"); + let stations = records(&data.join("3!stations.csv")); + let station_by_id: HashMap<&str, &StringRecord> = + stations.iter().map(|r| (&r[0], r)).collect(); + let types = records(&data.join("4!types.csv")); + let type_names: HashMap<&str, &str> = types.iter().map(|r| (&r[1], &r[2])).collect(); + let fixture = records(&root.join("data_validator/tests/fixtures/keio_stops.csv")); + let official: HashMap = fixture + .iter() + .map(|r| (r[0].parse().unwrap(), &r[1])) + .collect(); + assert_eq!(official.len(), 53); + + let mut actual: BTreeMap> = BTreeMap::new(); + for row in records(&data.join("5!station_station_types.csv")) { + let station = station_by_id[&row[1]]; + let line: u32 = station[13].parse().unwrap(); + if !matches!(line, 24001..=24005 | 24007) { + continue; + } + let number: u32 = station[8].parse().unwrap(); + let group: u32 = row[3].parse().unwrap(); + let name = type_names[&row[2]]; + let category = if name.starts_with("京王ライナー") { + "京王ライナー" + } else if name == "Mt.TAKAO" { + "Mt.TAKAO号" + } else { + name + }; + // Per-row type is essential: Toei locals become Keio express services. + let mut expected = u32::from(!official[&number].split('|').any(|t| t == category)); + if group == 758 { + // Mt.TAKAO down: Shinjuku, Meidaimae, Takaosanguchi only. + expected = u32::from(!matches!(number, 1 | 6 | 53)); + } + if number == 23 && category == "特急" { + // Racecourse through service stops; main-line services partly stop. + expected = if group == 764 { 0 } else { 5 }; + } + assert_eq!( + row[4].parse::().unwrap(), + expected, + "group {group}, station {} ({}), type {name}", + &station[2], + &station[0] + ); + actual.entry(group).or_default().push(row[1].to_owned()); + } + + let audited: BTreeSet = [ + 65, 66, 67, 68, 69, 70, 71, 252, 253, 254, 255, 285, 286, 287, 412, 413, 414, + 415, 416, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, + ] + .into_iter() + .collect(); + assert_eq!(actual.keys().copied().collect::>(), audited); + for (group, station_ids) in actual { + let mut expected = Vec::new(); + for (line, first, last) in sections(group) { + let mut section: Vec<&StringRecord> = stations + .iter() + .filter(|r| { + r[13].parse::().unwrap() == line + && &r[21] == "0" + && (first..=last).contains(&r[8].parse::().unwrap()) + }) + .collect(); + section.sort_by_key(|r| (r[22].parse::().unwrap(), r[0].to_owned())); + expected.extend(section.into_iter().map(|r| r[0].to_owned())); + } + assert_eq!(station_ids, expected, "incomplete/out-of-order group {group}"); + } +} diff --git a/docs/keio-stopping-pattern-audit.md b/docs/keio-stopping-pattern-audit.md new file mode 100644 index 00000000..c72aedc5 --- /dev/null +++ b/docs/keio-stopping-pattern-audit.md @@ -0,0 +1,30 @@ +# 京王線系統の停車駅照合(2026-10-16) + +京王線・京王新線・相模原線・高尾線・競馬場線・動物園線の公式各駅案内53駅と、既存31路線グループの936行を照合した。都営新宿線直通の京王区間も含む。駅ごとの参照URLと確認日は `data_validator/tests/fixtures/keio_stops.csv` に収録した。 + +## 修正 + +| 路線グループ | 種別 | 駅 | 修正 | +| --- | --- | --- | --- | +| 285 | 急行(都営線内も急行) | つつじヶ丘 | 通過 → 停車 | +| 757 | 急行(都営線内は各駅停車) | つつじヶ丘 | 通過 → 停車 | +| 760 | 急行(京王線新宿~橋本) | つつじヶ丘 | 通過 → 停車 | +| 65 | 京王ライナー(京王八王子系統) | 明大前・北野 | 通過 → 停車 | +| 66 | 京王ライナー(橋本系統) | 明大前 | 通過 → 停車 | + +急行のつつじヶ丘は[公式駅案内](https://www.keio.co.jp/train/station/ko14_tsutsujigaoka/)、京王ライナーは[上り](https://www.keio.co.jp/zasekishitei/)・[下り](https://www.keio.co.jp/zasekishitei/index_n.html)の公式停車駅・時刻表で確認した。京王ライナーの系統名に「方面」とあるが、既存の方向設定は0(両方向)であり、上りも同じグループを使用する。 + +## 回帰検査 + +`cargo test -p data_validator --test keio_stopping_patterns` で、各行の種別と公式駅案内の停車種別を比較する。都営線内の各駅停車をグループ全体の種別として扱わず、京王区間の各行の `type_cd` を使用する。 + +既存31グループがすべて存在すること、および各グループの路線区間から求めた駅ID列がCSVの列と完全一致することも検証する。これにより行の削除、重複、並び違い、調布・北野・東府中・高幡不動・笹塚の接続部での片側ID欠落を検出する。井の頭線は別線区のためこの検査の対象外。 + +次の差異は公式の運行形態に基づくもので、均一化しない。 + +- [Mt.TAKAO号](https://www.keio.co.jp/zasekishitei/index_mt.html)の758は下り(新宿・明大前・高尾山口)、759は上り(府中以西の所定駅・明大前・新宿)。駅案内の停車種別一覧だけでは方向別の停車駅を表現できないため、グループ別の期待値を使用する。 +- 京王八王子・高尾山口系統の特急は東府中を一部停車(`pass=5`)とし、競馬場線直通特急764は東府中の両路線IDで停車する。 +- 相模原線の急行・特急、高尾線の急行・特急は各線の公式駅案内に従う。特急が高尾線内で各駅に停車しても急行の通過駅まで変更しない。 + +本検査は既存の停車パターンと駅列を対象とする。新系統の収録網羅性、時刻指定検索、乗降制限の実装を保証するものではない。公式案内には高尾線の京王ライナーも掲載されているが、現行CSVには専用の路線グループがなく、系統追加は別途対応が必要。 + From 7c197f6d8bbfe1053a18fbb849ea1dd449a5b0a4 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Fri, 16 Oct 2026 09:26:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BA=AC=E7=8E=8B=E7=B7=9A=E3=81=AE?= =?UTF-8?q?=E5=9B=9E=E5=B8=B0=E3=83=86=E3=82=B9=E3=83=88=E3=82=92Rustfmt?= =?UTF-8?q?=E3=81=AE=E6=9B=B8=E5=BC=8F=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_validator/tests/keio_stopping_patterns.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/data_validator/tests/keio_stopping_patterns.rs b/data_validator/tests/keio_stopping_patterns.rs index 00c94fa7..a4e052cb 100644 --- a/data_validator/tests/keio_stopping_patterns.rs +++ b/data_validator/tests/keio_stopping_patterns.rs @@ -35,8 +35,7 @@ fn existing_keio_patterns_match_official_stops_and_complete_station_sequences() let root = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); let data = root.join("data"); let stations = records(&data.join("3!stations.csv")); - let station_by_id: HashMap<&str, &StringRecord> = - stations.iter().map(|r| (&r[0], r)).collect(); + let station_by_id: HashMap<&str, &StringRecord> = stations.iter().map(|r| (&r[0], r)).collect(); let types = records(&data.join("4!types.csv")); let type_names: HashMap<&str, &str> = types.iter().map(|r| (&r[1], &r[2])).collect(); let fixture = records(&root.join("data_validator/tests/fixtures/keio_stops.csv")); @@ -84,8 +83,8 @@ fn existing_keio_patterns_match_official_stops_and_complete_station_sequences() } let audited: BTreeSet = [ - 65, 66, 67, 68, 69, 70, 71, 252, 253, 254, 255, 285, 286, 287, 412, 413, 414, - 415, 416, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, + 65, 66, 67, 68, 69, 70, 71, 252, 253, 254, 255, 285, 286, 287, 412, 413, 414, 415, 416, + 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, ] .into_iter() .collect(); @@ -104,6 +103,9 @@ fn existing_keio_patterns_match_official_stops_and_complete_station_sequences() section.sort_by_key(|r| (r[22].parse::().unwrap(), r[0].to_owned())); expected.extend(section.into_iter().map(|r| r[0].to_owned())); } - assert_eq!(station_ids, expected, "incomplete/out-of-order group {group}"); + assert_eq!( + station_ids, expected, + "incomplete/out-of-order group {group}" + ); } }