A lineage-grounded transit-feed compliance and change-impact agent, built on DataHub.
TransitGuard turns a transit agency's GTFS feeds into a governed DataHub context graph, then runs an agent that answers the two questions behind an agency's funding eligibility: is this feed compliant with the federal funding mandate right now, and if it changes or breaks, what downstream products and which federal report go down with it?
Built for the Build with DataHub: The Agent Hackathon.
The problem: the FTA now requires fixed-route agencies that take federal money to publish a valid, current GTFS feed, tied to National Transit Database (NTD) reporting and grant eligibility. Free validators answer "is this feed well-formed against the spec." None map that to the funding mandate, and none can tell you what breaks downstream when a feed changes. A context-blind LLM asked those questions guesses, because it cannot see what is connected to what. TransitGuard gives it the graph. See Sources for the primary-source citations behind every compliance claim.
| Component | What it does |
|---|---|
GTFS to DataHub connector (src/features/gtfs_connector/) |
Registers each GTFS table as a DataHub dataset on a custom gtfs platform with a real schema and column-level foreign-key lineage (stops.stop_id to stop_times.stop_id, etc.). Only spec-defined FKs are emitted, so nothing is manufactured. |
Validation + NTD compliance (src/features/validation/) |
Runs MobilityData's canonical gtfs-validator, maps posture to NTD compliance, and writes two DataHub assertions per dataset (validation + freshness) that render as green/red badges, plus ntd_certification / feed_status tags and properties. |
Derived warehouse chain (warehouse/ + src/features/derived_chain/) |
A real dbt-duckdb project (staging, a departures mart, an NTD stop export) whose column-level lineage is emitted back onto the gtfs platform, so a stop_id rename provably propagates from the raw feed all the way to the federal NTD export and the departures dashboard. |
The impact agent (src/features/impact_agent/, .mcp.json) |
Claude Code, grounded by the official DataHub MCP server, given a proposed feed change. It traverses the real lineage and reads compliance from the graph, then returns a plain-language impact statement plus a structured verdict (safe_to_ship, ntd_certification, affected_datasets, breaks_ntd_export). |
The context-moat benchmark (src/features/benchmark/, data/benchmark_questions.json) |
Runs the same agent with the DataHub graph vs. an isolated raw-feed-only baseline, over a fixed question set, and reports the per-bucket accuracy delta. The headline is the downstream-impact bucket, where the graph is load-bearing. |
Governed write-back (src/features/writeback/, src/entrypoints/govern.py) |
Closes the loop: on a confirmed "not safe to ship" finding, and only after a human [y/N] confirm, it tags the changed table and its affected downstream datasets pending-breaking-change, records the finding as properties, and fails a "safe to ship" assertion. The tag write is a PATCH merge (compliance tags are preserved), and --resolve reverses the whole record. |
Public GTFS feeds become a governed DataHub context graph, an agent answers off that graph, a context-blind baseline shows the graph is load-bearing, and a human-confirmed write-back closes the loop.
"We want to rename the column
stop_idin the failingclallamfeed. Are we safe to ship, and are we NTD-compliant?"
- Context-blind (raw feed only): can't see the downstream or the compliance
status, so it returns
affected_datasets: [],ntd_certification: "unknown". - Grounded by DataHub: traces
stop_timestostg_stop_timestomart_departurestontd_stop_export(and the departures dashboard), reports the feed is AT RISK with a failing GTFS-validation assertion, and returns the exact affected URNs (not safe to ship).
Prerequisites: uv, Docker, a JDK (for the validator jar), and an authenticated
claude CLI (the agent runs on Claude Code, so no API key is needed).
# 1. Install deps (Python 3.11 is pinned)
uv sync
# 2. Bring up DataHub locally. NOTE: this repo maps GMS to host port 8090
# (8080 was taken); the UI is on 9002 (login datahub/datahub).
DATAHUB_MAPPED_GMS_PORT=8090 uv run datahub docker quickstart
# 3. Ingest the demo feeds (clean / failing / stale) as DataHub datasets + lineage
uv run python -m src.entrypoints.ingest_feeds --gms http://localhost:8090
# 4. Build the derived warehouse chain for a feed (real dbt transforms + lineage)
uv run python -m src.entrypoints.build_derived_chain \
--feed-id clallam --zip scratch/feeds/clallam.zip --gms http://localhost:8090
# 5. Validate + emit NTD compliance (assertions + tags)
uv run python -m src.entrypoints.validate_feeds --gms http://localhost:8090
# 6. Ask the impact agent (grounded by the DataHub MCP server via .mcp.json)
uv run python -m src.entrypoints.impact_agent --feed-id clallam \
--change "rename column stop_id in stop_times"
# 7. Run the context-moat benchmark (grounded vs blind, per-bucket)
uv run python -m src.entrypoints.run_benchmark
# 8. On a confirmed finding, write a governance record back (human-confirmed).
# --run-agent gets a fresh verdict; --dry-run writes a file instead of GMS.
uv run python -m src.entrypoints.govern --feed-id clallam --table stop_times \
--change "rename column stop_id in stop_times" --run-agent --gms http://localhost:8090The benchmark asks the agent the same questions twice, once grounded by the DataHub graph and once with only a raw-feed summary, and scores each against ground truth. The "blind" baseline is deliberately isolated (a throwaway working directory, no MCP, no repo access), so the only difference is the DataHub context. Results are reported per bucket rather than as one inflated number.
| bucket | n | grounded | blind | delta |
|---|---|---|---|---|
| spec_validity | 2 | 1.00 | 0.00 | +1.00 |
| freshness | 2 | 1.00 | 1.00 | +0.00 |
| downstream_impact | 3 | 1.00 | 0.00 | +1.00 |
| compliance_consequence | 2 | 1.00 | 0.00 | +1.00 |
| overall | 9 | 1.00 | 0.22 | +0.78 |
Scores are the fraction of ground-truth checks each mode got right (grounded =
DataHub graph available, blind = same agent with the graph removed). In this
run, the blind agent returned unknown on every question in three buckets
(spec validity, downstream impact, compliance consequence), because it has no
validator, no lineage, and no compliance metadata to work from, and unknown
scores 0 against a definite ground truth. Those are exactly the buckets the
graph makes answerable. Freshness is a deliberate +0.00: staleness is
computable from the feed's own service dates,
which the blind agent gets in its raw-feed summary, so the graph earns nothing
there. Reporting it that way (instead of hiding it) is what keeps the benchmark
honest rather than a strawman.
Per the hackathon's pre-existing-work policy, src/features/validation/validate.py
adapts the canonical-validator wrapper from a pre-existing script
(subprocess invocation, report parsing, service-date extraction). The deterministic
validator itself is MobilityData's gtfs-validator
(Apache-2.0), provided as a jar (not committed). Everything else is new work for
this submission: the DataHub connector, the derived-chain lineage emission, the
compliance/assertion mapping, the impact agent, and the benchmark.
Every compliance claim maps to a public primary source:
- The mandate: 88 FR 13503, "National Transit Database: Reporting Changes and Clarifications" (Federal Register 2023-04379, March 3, 2023), which adopted the GTFS weblink requirement for federally funded fixed-route reporters, under statutory authority 49 U.S.C. § 5334(k). https://www.federalregister.gov/documents/2023/03/03/2023-04379/national-transit-database-reporting-changes-and-clarifications
- RY2025/26 expansion: Federal Register 2024-25341 (docket FTA-2024-0013), which
adds
shapes.txtand an NTD P-50agency_idcrosswalk. https://www.federalregister.gov/documents/2024/10/31/2024-25341/national-transit-database-proposed-reporting-changes-and-clarifications-for-report-years-2025-and - Live per-agency data: the NTD GTFS Weblinks table (
2u7n-ub22ondata.transportation.gov, keyed onntd_id), carrying each reporter's declared feed URL, certification flag, and validation date. - Feed registry + validator: the Mobility Database and MobilityData's gtfs-validator (Apache-2.0), the source of the well-formedness posture used in the demo.
FTA monitors compliance by periodically checking that GTFS weblinks are viable and current, tied to NTD reporting and grant eligibility. Findings are framed as "at risk of NTD non-compliance / funding eligibility," never a dollar penalty.
Apache-2.0. See LICENSE.


