Spike #1759 - How to organize the Seal of Reliability endpoint(s) #1789
davidgamez
started this conversation in
Decisions
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
1. Summary of the proposal
GET /v1/gtfs_feeds/{id}/reliability→ full breakdown (summary + 6 criteria).reliability_sealsummary object in the existing feed-detail (GtfsFeed) and search (SearchFeedItemResult) responses, so the "verified feed" badge needs no extra call./reliabilitydetail) is public. Filtering/searching feeds by seal status is gated (requires special permissions).The seal data model (#1760) already dictates a single-aggregate shape: one
feed_reliability_sealrow + sixseal_criterionrows per feed. The endpoint simply surfaces those two tables. Splitting into multiple endpoints would only add round-trips for a UI that renders all six criteria together.2. Findings
2.1 This spike is the API layer of an already-designed epic
The backend data model and evaluation logic are defined in sibling tickets, and #1761 is explicit that the API is read-only and stored-as-is ("
has_sealandgrace_failingare stored directly and read as-is - no on-the-fly recomputation needed").Tables (#1760):
feed_reliability_seal- one row per feed:has_seal,seal_earned_at,seal_lost_at.seal_criterion- six rows per feed:raw_failing,grace_failing,evaluated_at,first_raw_failure_at,last_raw_failure_at,last_grace_failure_at.Criteria (#1783 / #1784 / #1782):
official,stable,available,compliant,fresh_coverage,fresh_continuous.has_sealis true iff all six criteria havegrace_failing = false. The grace period is absorbed inside the nightly job: a criterion failing but still within its grace window keepsgrace_failing = false, so it still counts toward the seal.2.2 Backing data already exists for every criterion
feed.official(+officialstatushistoryaudit)gtfsdataset.created_at(latest) +feed.is_producer_url_unstablegtfs_feed_availability_check.successvalidationreport.total_erroron latest datasettotal_error == 0gtfsdataset.service_date_range_end(latest)gtfsdataset.service_date_range_*+ newfeed_info_date_range_*(#1775)No existing endpoint or schema aggregates these into a verdict - that is the net-new surface.
2.3 Existing public API surface (
docs/DatabaseCatalogAPI.yaml)GET /v1/gtfs_feeds/{id}(GtfsFeed) already exposesofficial,official_updated_at,status, andlatest_dataset(validation counts + service dates). Nothing aggregates a seal.GET /v1/gtfs_feeds/{id}/availabilityandGET /v1/gtfs_feeds/{id}/datasets. A/reliabilitysub-resource fits this pattern exactly.Authentication). The spec has no tiered/scoped/gated concept. "Restricted" access is enforced in the impl layer viais_user_email_restricted()(api/src/middleware/request_context.py), not in the spec. Privileged/admin operations live in a separatedocs/OperationsAPI.yaml(ApiKeyAuth).3. Proposed endpoint & schema
3.1 New endpoint
Response codes:
200(evaluated, or not-yet-evaluated with empty /not_evaluatedcriteria),404unknown feed. Public - no special permission required.3.2 Embedded summary (drives the public badge)
Added to
GtfsFeedandSearchFeedItemResult:3.3 Detail schema
Why
statusandin_grace_periodare orthogonal: grace is not a failing state. While a feed is within its grace windowgrace_failing = false, so the criterion still passes and the feed still holds the seal. The web renders three states from these two fields: healthy (pass, not in grace), at-risk (pass+in_grace_period→ warning), and lost (fail); plusnot_applicable/not_evaluated.in_grace_periodandgrace_period_ends_atare derived server-side fromfirst_raw_failure_at+ the per-criterion grace window (14/30/7 days), so the web gets #141's grace-period warning without knowing the rules.3.4 Example payloads
Feed-detail addition:
GET /v1/gtfs_feeds/mdb-1210/reliability:{ "feed_id": "mdb-1210", "has_seal": false, "earned_at": "2026-01-15T00:00:00Z", "lost_at": "2026-07-20T04:00:00Z", "evaluated_at": "2026-07-30T04:00:00Z", "criteria": [ {"criterion": "official", "status": "pass", "in_grace_period": false, "grace_period_ends_at": null, "first_failure_at": null, "last_failure_at": null}, {"criterion": "stable", "status": "pass", "in_grace_period": false, "grace_period_ends_at": null, "first_failure_at": null, "last_failure_at": null}, {"criterion": "available", "status": "fail", "in_grace_period": false, "grace_period_ends_at": null, "first_failure_at": "2026-06-01T04:00:00Z", "last_failure_at": "2026-07-30T04:00:00Z"}, {"criterion": "compliant", "status": "pass", "in_grace_period": true, "grace_period_ends_at": "2026-08-24T04:00:00Z", "first_failure_at": "2026-07-25T04:00:00Z", "last_failure_at": "2026-07-30T04:00:00Z"}, {"criterion": "fresh_coverage", "status": "not_applicable", "in_grace_period": false, "grace_period_ends_at": null, "first_failure_at": null, "last_failure_at": null}, {"criterion": "fresh_continuous", "status": "pass", "in_grace_period": false, "grace_period_ends_at": null, "first_failure_at": null, "last_failure_at": null} ] }Here
has_sealis false only becauseavailable=fail;compliantis inside its 30-day grace window so it still passes.3.5 Gated search filter
This is where #1759's "requires special permissions" applies. A new query parameter lets a caller filter/search the catalog by seal status:
4. UI mockup → API data-point mapping
Feed-detail "Data Quality" tab (MobilityData/mobilitydatabase-web#141) mapped to the API. "new" = the proposed
GET /v1/gtfs_feeds/{id}/reliabilityendpoint / embeddedreliability_sealobject (spike #1759); everything else already exists.GET /v1/gtfs_feeds/{id}(existing); Full-Report URL viaGET /v1/datasets/gtfs/{id}GET /v1/gtfs_feeds/{id}/reliability(new)/reliability(new); dates ("extends to Jul 28 2027") fromGET /v1/gtfs_feeds/{id}(existing)/reliability(new)GET /v1/gtfs_feeds/{id}/availability(existing)/reliability(new)GET /v1/gtfs_feeds/{id}/datasets(existing)/reliability(new)url_json(existing, public)reliability_sealembedded inGET /v1/gtfs_feeds/{id}(new field)Summary - the whole tab needs 3 primary calls
GET /v1/gtfs_feeds/{id}- identity, Fresh dates, validation counts, and the embeddedreliability_sealbadge.GET /v1/gtfs_feeds/{id}/reliability(new) - every pass/fail verdict and grace-period countdown./availability(uptime bars) and/datasets(coverage Gantt); notice list read straight from the validatorurl_json.All reactions