A dot on a map is an observation reported by a source. It does not tell me whether a passenger caught a bus, whether the published trip ran on time, or whether a route was disrupted. Those are different questions, and a single position snapshot cannot answer them.
AEGIS is my .NET 10 command-line project for checking Wrocław’s public transport schedule feed alongside its vehicle-position feed. I designed it as a data-quality pipeline: download inputs, validate their structure and relationships, compare the vehicle sample with scheduled brigades, then keep a report that explains each finding. It is not a punctuality dashboard or a service-level monitor.
AEGIS / WROCŁAW DATA QUALITY
Two feeds. One explainable QA report.01 / INPUTS
trips · stop_times · calendarline · brigade · time · GPS02 / RELATE + CHECK
A position needs context.Brygada→LAST TWO DIGITStrips.brigade_id- Only assigned lines enter live checks
- Freshness, WGS84 and movement rules
- GTFS references and service-time order
03 / KEEP EVIDENCE
JSON reportStable rule IDs, severity, source, capture time and feed hash.
PASS / WARNING / FAIL- 984vehicle records
- 444with an assigned line45.1% of all records
- 347brigade matches78.2% of assigned-line sample
- 44over the 20-minute threshold9.9% of assigned-line sample
- 01coordinate range error0.2% of assigned-line sample
Start with two sources that answer different questions
The schedule archive describes planned service: routes, trips, stop times and service calendars. The vehicle endpoint describes records returned at a capture time, including a vehicle identifier, line, brigade, coordinates and the source’s update timestamp. The live command downloads the latest schedule archive from the Wrocław OpenData catalogue and follows pages from the vehicle-position feed.
These inputs need separate checks before they can be related. The GTFS reader handles quoted CSV fields and UTF-8 text; the schedule validator checks required tables and columns, duplicate keys, references between routes, trips, stops and stop times, coordinates, calendars, and time ordering. It accepts service times beyond midnight, such as 25:15:00, as elapsed time from the service day’s start instead of wrapping them to 01:15.
Vehicle data has its own time problem. Wrocław’s source can emit a local timestamp without a UTC offset, so the parser interprets it in Europe/Warsaw. If a wall-clock time is ambiguous or falls into a daylight-saving gap, the parser leaves it unparsed rather than guessing which instant the source intended.
Join only what the source contract supports
The live validator checks the last two digits of a vehicle’s Brygada against trips.brigade_id, following the source’s documented relationship. It does not infer a vehicle’s entire journey from its current coordinate. The join produces a count and, when a suffix is available but no schedule value matches, a warning that points to the related identifier.
The feed can retain old coordinates for parked vehicles. For that reason, the live rules apply only to rows with an assigned line. For those rows, the current defaults flag timestamps older than 20 minutes, timestamps more than two minutes in the future, invalid WGS84 coordinates, coordinates outside a broad Wrocław monitoring envelope, and large position jumps when a prior sample is available within 30 minutes. The envelope, freshness and jump limits are project thresholds; they are review signals, not official boundaries or service guarantees.
That distinction changes what a warning means. A stale row says that its timestamp is older than the configured cutoff at this capture. A large jump says that two samples are far apart under the chosen distance calculation. Neither finding, on its own, proves a missed trip, a vehicle fault or a breach of an operator’s service commitment.
Make a run reviewable
On 14 October 2026, one live QA capture returned 984 vehicle records. Of those, 444 had an assigned line and entered the live checks (45.1% of the full sample). Within those 444 records, 347 matched a scheduled brigade using the documented suffix rule (78.2%), 44 were beyond the 20-minute freshness threshold (9.9%), and one had coordinates outside WGS84 (0.2%). These rule findings can overlap. The rates describe one capture, not current conditions or a trend.
The report keeps the source, capture time, schedule-feed hash, table summaries, stable rule IDs, severities and entity references. The GTFS ZIP is stored under a path derived from its SHA-256, so repeated runs can identify the exact archive bytes they checked. A local normalized vehicle sample is retained as the previous sample for jump comparisons. Optional PostgreSQL history stores run reports; raw vehicle positions are not committed to the repository.
There is also a separate compare command for two schedule archives. It uses table-specific keys such as stop_id, route_id, trip_id and (trip_id, stop_sequence) to count added, removed and changed rows. If the archive hash changes but the keyed table content does not, the report warns that packaging metadata may explain the difference. That keeps an archive-level change from being mistaken for a timetable edit.
Keep the claim narrower than the data
I reviewed the private repository at commit c2dc815. On Windows with .NET 10, dotnet test Aegis.slnx --configuration Release passed all 16 tests, including a PostgreSQL Testcontainers history round-trip. The tests use synthetic schedule and vehicle fixtures; they do not prove long-term live-feed reliability. The two latest GitHub Actions runs for this commit failed without exposing any job-step output, so I do not count them as green remote CI evidence.
The project can check data consistency and retain evidence for a capture. It does not estimate punctuality, reconstruct complete vehicle journeys, identify route diversions as failures, or establish an operational SLA. The MobilityData schedule validator is an optional separate command; its findings are not silently merged into AEGIS’s local rule report. The repository also contains no published vehicle identifiers or raw live-position sample.
The next useful proof would be a longer, approved history with stable schedule versions and enough observations to calibrate the warning thresholds. Then the project could measure how often a rule fires, how often a human confirms it, and which source changes explain the difference. Until that evidence exists, the report is a careful account of what one feed capture contained—not a verdict on how the city ran its buses.
For another example of treating incomplete public-source data cautiously, read how ProcureScope preserves TED pages before normalizing notices.