Status: Implemented as an opt-in weekly QA path. National publishing remains intentionally disabled because this project does not operate a hosted OTP service.
The scorecard otp CLI command runs trip-plannability QA against an external OpenTripPlanner instance: sample origin-destination pairs, query the OTP endpoint, and fail if any trip returns no itinerary. This document outlines how to surface those results on agency pages once OTP is available.
pipeline/src/scorecard_pipeline/otp.py: Trip-plannability assessment (already built, serverless-compatible)scorecard otpCLI: Query OTP for sampled routes (registered, requires OTP endpoint URL)otp-qa.ymlworkflow: Weekly and manual sampling- Agency pages:
_otp_sectioninrender_site.pyrenders a "Can a rider plan a trip?" section, gated on the artifact carrying a measuredrouting_qablock — pages are unchanged until the OTP job publishes results
{
"routing_qa": {
"summary": "123 of 125 sampled trips returned an itinerary",
"name": "Trip-plannability QA",
"status": "measured",
"score": 98.4,
"details": {
"total_sampled": 125,
"routable_trips": 123,
"unroutable_pct": 1.6,
"notes": "Failures on 2 late-night trips outside service area."
}
}
}The renderer is implemented in _otp_section() and called by _render_agency().
The example below documents the artifact contract rather than pending code.
def _otp_section(artifact: dict[str, Any]) -> str:
"""Trip-plannability QA, when available (requires external OTP service)."""
rq = artifact.get("routing_qa") or {}
if not rq.get("status"):
return "" # OTP not run for this feed
score = rq.get("score")
pct = rq.get("details", {}).get("unroutable_pct", 0)
return f"""
<section aria-labelledby="otp-h" class="feed-details">
<h2 class="section-title" id="otp-h">Can a rider actually plan a trip?</h2>
<p class="page-lede">
We sampled {rq['details']['total_sampled']} origin-destination pairs via
<a href="https://www.openplans.org/opentripplanner/">OpenTripPlanner</a>.
{pct:.1f}% returned no itinerary (typically outside service hours or area).
</p>
</section>
"""The scorecard otp command already exists:
# Requires a running OTP instance (e.g., locally or via Docker)
scorecard otp ./feed.zip --base http://localhost:8080The otp-qa.yml workflow can be adapted to run on demand:
name: Route QA (OTP)
# Requires: OTP instance running, GTFS feed URL
# Usage: gh workflow run otp-qa.yml -f feed_url=...- Build the OTP artifact structure in
otp.py→artifact["routing_qa"]. - Add
_otp_section()torender_site.pyand call it in_render_agency(). - Provide weekly and manual execution in
.github/workflows/otp-qa.yml. - Document local setup and command usage.
- National-map publishing is intentionally not applicable until a representative hosted run exists; publishing a partial opt-in sample would mislead readers.
- Daily scoring integration is intentionally not applicable without a hosted OTP service. The weekly/manual workflow preserves the serverless cost boundary.
- No national OTP by default. OTP is a heavy Java service; the project remains serverless. OTP is always opt-in: users can run it locally or with their own instance.
- Gated like equity overlays. If OTP data doesn't exist, the section is absent (no "not measured" note on pages where it wasn't run).
- Reuse the sampling pattern.
sample_od_pairs()is already inotp.py; just needs wiring to the render layer.
If the project adds a backend (a routing service, OTP instance, or integration with a third-party routing API), this becomes a fully automated, national feature. Until then, it's an available CLI tool for operators who want to check trip-plannability locally.