Releases: ChelseaKR/tods-validate
Release list
v0.11.0
If you validate against a companion GTFS feed, read this one
A malformed row in the companion feed could make a run wrong in either direction, and neither outcome mentioned the real problem.
The loader treated a file that parsed but did not read in full as a clean read. So a single short row in the companion trips.txt did one of two things. If it dropped a block_id, the run reported "No problems found", claimed "Every applicable check ran", and exited 0. If it dropped a trip_id, the run reported TODS-E307 against your own run_events.txt, an ERROR saying a trip does not exist when it does exist and the fault was in the companion feed.
Both are fixed. A file that parsed but lost values now counts as not fully read, the checks that depend on it are recorded as skipped rather than run, and the run coverage manifest says so.
What changes in your reports: TODS-W302 now also fires when a referenced file parsed but did not read in full, because a row was ragged or a column was declared twice. Feeds that previously validated clean for this reason will start reporting that warning. That is the point: the checks were never run, and the report now says so instead of implying they passed.
For anyone using the Python API
The package ships a PEP 561 py.typed marker. mypy now resolves types from tods_validate instead of skipping it with "missing library stubs or py.typed marker".
Everything else
Most of this release is work on the project's own checks rather than on validation. Several of them could report a pass they had not earned. The secret scan never looked at the working tree, only at commits. The throughput budget divided an assumed row count by CPU time, so a validator that stopped reading feeds entirely would have passed further inside the budget than a correct one. The public-contract check compared the export list against a declaration of the export list. The deployed-playground check could not distinguish a working validator from a page that answers every feed with findings. Each now has a test that fails when the repair is removed.
Compatibility
No rule IDs were added, removed, or renumbered. Exit codes and the JSON report schema are unchanged from 0.10.0. TODS-W302's description was reworded to match the broadened condition above.
Full detail is in CHANGELOG.md.
v0.10.0
Superseding v0.9.1
v0.9.1 was tagged and signed (commit edd2ea1) but its GitHub Release
object was never created, so pypi-publish.yml never ran: PyPI's latest
published version stayed 0.9.0 while pyproject.toml and the tag said 0.9.1
(#136). Sixteen PRs landed on main after that tag, several changing
validator behavior, so re-publishing the number 0.9.1 would misdescribe
what actually ships. This release supersedes it. The v0.9.1 tag is left in
place, signed and unmoved, and is not the version anyone should install;
v0.10.0 is.
The version is a MINOR bump, not a PATCH, because two of the changes below
are not backward-compatible: the Python floor rises to 3.12 (drops installs
on 3.11), and TODS-E301/TODS-E303/companion-GTFS reference checks now
fail closed on an unreadable file instead of silently skipping or inventing
findings, which can change a previously-clean run's exit code. Per this
repo's pre-1.0 SemVer policy (docs/standards/RELEASE-AND-VERSIONING-STANDARD.md
REL-05), a 0.y.z MINOR release may carry a breaking change; this is not
yet the v1.0.0 release described in docs/v1-contract-audit.md, which is
reserved for a conformance-only release after the contract snapshot has
gone unchanged for one full release cycle. This one does not qualify --
it adds a rule (TODS-E207) and changes coverage-manifest behavior in three
commands.
Fixed:
- A companion GTFS file that could not be decoded (bad encoding, empty,
unparseable CSV) counted as present. The reference rules that read it ran
against an empty table instead of being skipped, invented ERRORs against
every real ID in the TODS file that referenced it, and the coverage
manifest recorded themran. The same shape reached two TODS-internal
checks: an unreadablerun_events.txtorvehicles.txtproduced invented
TODS-E301/TODS-E303findings the same way. All three now treat an
unreadable file the same as a missing one — the rule is skipped
(skipped:needs_gtfs_tablefor the companion-GTFS case), andTODS-W302
discloses that the file could not be read (pointing toTODS-E103for the
reason on the TODS side), instead of silently reportinghas no <file>or,
worse, inventing errors against it. diff OLD NEWreported a rule's old finding "fixed" whenever it was absent
from NEW, without checking whether the rule ran in NEW at all. A rule that
stopped running — a companion GTFS feed dropped, or newly unreadable
(#125), between OLD and NEW — makes its old findings disappear the same
way a genuine fix does, anddiffcould not tell them apart: comparing
tests/fixtures/invalid/TODS-E307(a badtrip_idreference) against the
same package with the companiontrips.txtremoved reportedfixed: 1
and exit 0, when the bad reference was never re-checked, let alone fixed.
diffnow usesrun_with_coverageand only counts an OLD-only finding
fixedwhen its rule ran in NEW; otherwise it lands in a newunknown
bucket, named in the counts line. Every rule that ran in OLD and not in
NEW is also named below the findings, whether or not it had a finding to
lose — a dropped companion can zero out 16 checks with 0 findings on
either side, which used to read as a silently clean diff.batchused the two-tuplerun()wrapper, so none of its three formats
(text,--format json,--format markdown) had a coverage manifest to
disclose: a TODS-only feed in a fleet run skipped 16 of 42 checks, 9 of
them ERROR-severity, and its row read0 0 0 pass— exactly the numbers a
fleet compliance artifact is read for, with nothing saying the run was
partial.batchnow usesrun_with_coverage. Every format carries the
manifest: text and Markdown gain a "checks not run" column beside each
feed's status plus a fleet-wideRule-set coverageline in the roll-up
(pooling every feed's outcomes, the same disclosure a single-feed report
already carries);--format jsonadds a per-feedchecksNotRuncount and
acoverageblock matchingvalidate --format json's.
--require-complete-run(#124) is now available onbatchtoo: a feed
with an unrequested skip (missing/unreadable companion GTFS) fails that
feed, the same as it does forvalidate.uv.lockpinspipat 26.2.1, pastPYSEC-2026-3721(disclosed after
26.1.2 was pinned). Vendored only as a transitive build/audit tool, never
imported bytods_validateitself, but it was failingmake audit(and
would fail it for any PR, unrelated to that PR's own change) until bumped.fix -o OUTandanonymize -o OUTno longer destroy a file the loader could
not read. Both commands rebuild every file from the loader's headers and rows;
a file whose decode or CSV parse failed has neither, so it was written out as a
single newline — the user's data replaced by an empty file.fixcompounded it
by printingNothing to fix., because no trim/blank/duplicate counter had
moved, so the run reported that it had changed nothing while it was the run
that lost the data. Both commands now refuse to write such a package and name
the offending file;fix's dry run reports it instead of claiming there was
nothing to fix. Use--encodingif the file is deliberately not UTF-8.
Packages that load cleanly are unaffected.--format githubnow discloses the checks that did not run. It is the only
format the composite action emits, and it was the one format that never
carried the coverage manifest:render_githubtook nocoverageargument
at all, so a feed validated without a companion GTFS feed printed
0 error(s), 0 warning(s), 0 infoand stopped there, while 16 of 42 checks
had not run, 9 of them ERROR-severity. An agency or vendor who left the
gtfs:input out of the workflow got a green check and had no way to learn
that no reference was ever resolved. The summary line now carries the run's
scope, and each reason a check did not run becomes its own::notice
annotation naming the rules, so the disclosure reaches the pull request's
Checks tab and not only the log.- Every report format now names the rules that did not run, not just how many,
and a run that skipped nothing says so (Every applicable check ran (42 of 42).) rather than staying silent. Silence could not be told apart from a
format that does not disclose, which is how this defect survived. - The Markdown report states its rule-set coverage with or without
--stamp.
The block used to be printed only under--stamp, which tied a statement of
what the run checked to a statement of when it ran; the unstamped report is
the default and the one people paste into issues.
Changed:
- Minimum supported Python raised from 3.11 to 3.12 (#72), closing CQ-01
directly against the standard's stated floor (Python 3.10 reaches EOL
October 2026) instead of via the declared deviationdocs/adr/0001had
recorded since 2026-07-09.docs/adr/0006-python-312-floor.mdsupersedesREADME.mdandCONTRIBUTING.mdnow say "Requires Python 3.12 or
newer"; CI's test matrix is3.12/3.13(3.11 dropped). Installed
releases are unaffected; this binds new installs, upgrades, and local dev.
Added:
TODS-E207checks thatroutes_supplement.txt'sroute_colorand
route_text_colorare valid GTFS Color values: six hexadecimal digits, no
leading#(GTFS reference, "Field Types > Color"). Every other field a
supplement file inherits from its GTFS base is typedTextby
schema._supplement()regardless of the base file's real GTFS type, so
these two carried no format check at all before this;_supplement()
gained afield_typesoverride used only for these two fields, rather
than transcribing the full GTFS field-type inventory for a single rule.
(#101)--require-complete-runfails the run when a check could not run because an
input was missing, such as a companion GTFS feed that was not given. Skips
the caller asked for (--ignore, opt-in rules left off,--spec-version
scoping) are disclosed but do not fail it. The GitHub Action exposes it as
therequire-complete-runinput.- A skipped check still does not change the exit code by default. That is
deliberate: this tool has shipped as a merge gate since 0.1.0 and every feed
validated without a companion GTFS feed skips 16 checks, so failing on a
skip would turn existing pipelines red on upgrade for something they never
asked the tool to promise. The README now states it instead of leaving0
to be read as "fully checked".
Docs:
- The Standards Conformance section's intro paragraph enumerated eleven
standards ("code quality, security & supply chain, ... AI-evaluation") while
the table below it declares fifteen -- Performance, Incident Response, Data
Governance, and AI Development Measurement were in the table and missing
from the prose. v0.9.1 (#118) fixed the table itself (a comma in the
Accessibility row's state broke the vendored portfolio-standards v2.0.0
DOC-11 checker, which the prose drift did not: the checker grades the
table, not the paragraph above it); this is the second, smaller half of
#113. The paragraph now points at the table instead of maintaining a second,
driftable count.
Full changelog: https://github.com/ChelseaKR/tods-validate/blob/v0.10.0/CHANGELOG.md
v0.9.0
Upgrading: this release can change your job's result
action.yml is byte-identical to v0.8.0 — same inputs (path, gtfs,
fail-on, enable), same outputs (error-count, warning-count,
info-count), same exit-code contract (0 clean, 1 findings at or above the
threshold, 2 usage error). No rule ID was renumbered, removed, or given a new
severity.
What can change is the finding set on an unchanged feed, and it moves in
both directions. Three checks are responsible.
Your job may now fail where it passed
TODS-E204(ERROR) now covers exact duplicate rows in
employee_run_dates.txt. That file now has an explicit four-field primary
key (date+service_id+run_id+employee_id). Previously an exact
duplicate produced onlyTODS-W408(WARNING), so on the default
fail-on: errorit did not fail the job. It does now.TODS-W408is retained
on the same row as a grouped compatibility signal so existing machine
consumers do not lose the rule ID.TODS-E201(ERROR) is stricter on supplement rows that add a new GTFS
entry. Such a row must now provide every field GTFS marks Required for that
file, not just the primary-key fields. Updates and deletes are unchanged, and
the check stays permissive when no companion GTFS is available, because an
addition cannot be told from an update without one.
Your job may now pass where it failed
- A stray GTFS file no longer promotes a package to its own companion GTFS
feed. A package counts as a companion only when it carries a file TODS IDs
actually resolve against (trips.txt,stops.txt,stop_times.txt,
routes.txt,calendar.txt,calendar_dates.txt). A singleagency.txt
used to be enough, which ran all 16 GTFS cross-reference rules against a feed
with no trips, stops, or calendars — 28 invented errors on a valid feed,
and a coverage manifest that claimed 39 of 42 rules had run.
If you parse the JSON report
- New rule status
skipped:needs_gtfs_table. Every rule that reads the
companion GTFS now declares which files it reads and is skipped with this
status when the companion lacks them, instead of running against data that
cannot answer it. A TODS supplement file no longer counts as its own GTFS base
table (trips_supplement.txtmodifiestrips.txt; withouttrips.txtthere
is nothing to resolve atrip_idagainst). This also stopsTODS-I501
reporting trip coverage computed from supplement rows alone. docs/report.schema.jsonnow listsskipped:spec_version. The validator
has emitted it since v0.8.0 without documenting it, so a report produced with
--spec-version 1.0.0failed the schema this project publishes. If you
validate reports against that schema, this is a fix you want.
Also in this release
- The current GTFS supplement field inventory recognizes
trips.safe_duration_factor,trips.safe_duration_offset,
stops.stop_access, androutes.cemv_support. - A reviewed v1-candidate public-contract snapshot and blocking drift check
covering rule IDs, severities, categories, exit codes, supported spec
versions, Python exports, and required JSON report fields. It now runs on
every pull request; previously it reached CI only through the release
workflows, so the public contract was first verified after a tag was cut.
The CLI's exit codes have names intods_validate.policy
(EXIT_CLEAN/EXIT_FINDINGS/EXIT_USAGE) that the check reads.
Maintenance (no consumer impact)
- The CQ-09 lockfile-drift gate could not fail, and now can. CI installed
withuv sync --frozen, which installs exactly whatuv.lockrecords and
exits 0 whether or not the lock still agrees withpyproject.toml. Measured:
withpyproject.tomlat 0.9.0 anduv.lockat 0.8.0,uv sync --frozen --extra devexits 0 whileuv lock --checkexits 1.uv lock --checknow
runs before everyuv syncin CI and as the first gate inmake verify.
ADR 0005, which claimed--frozen"fails on any lockfile drift", records the
correction. - The accessibility gate now performs an accessibility check.
make a11ybegan
withnpm audit --audit-level=high, so an unpatched HIGH advisory in the
pa11y-ci toolchain aborted the recipe before pa11y ever started — the job was
red for a dependency reason and audited nothing, for weeks. The npm audit is
its own gate now, andmake verifyruns every gate independently instead of
stopping at the first failure. - The perf budget is enforced (QM-02), measured in rows per CPU-second so a busy
shared runner is not reported as a regression. - The browser playground is deployed when a release is published, and refuses to
publish a page pinned to a wheel PyPI does not have yet. The deployed page is
compared against the page this repository publishes and audited for
accessibility after each deploy and weekly.
Full changelog: https://github.com/ChelseaKR/tods-validate/blob/v0.9.0/CHANGELOG.md
v0.8.0
v0.8.0 broadens compatibility and makes operational changes easier to inspect. TODS v1 feeds can now be validated directly, GTFS changes can be checked for broken TODS references, and HTML reports can include accessible run timelines.
Highlights:
- Validate historical feeds with
--spec-version 1.0.0, using the documented v1 file and field inventory. - Diagnose broken TODS references before a GTFS update with
tods-validate drift OLD_GTFS NEW_GTFS --tods FEED. - Add an accessible run timeline to HTML reports with
--timeline; the same information is available in a screen-reader-friendly table. - Use the browser playground at https://chelseakr.github.io/tods-validate/ or the reproducibly packaged VS Code client.
- Gate CAD/AVL imports with the new
ingest-readyprofile.
Validation and conformance:
- TODS-E203 now checks latitude, longitude, and non-negative float fields.
- Malformed feed values and baseline files produce findings or clear input errors instead of uncaught exceptions.
- The advisory spec watcher preserves conditionally required fields described as optional in the source table.
- The downloadable conformance corpus now uses a committed, reviewed oracle for all 43 fixtures.
The release publishes the Python package to PyPI, a signed container image to GHCR, an SBOM, and the conformance corpus. See the changelog for the complete list.
v0.7.0
tods-validate 0.7.0 validates TODS v2.1 feeds from the editor as well as the command line.
New
- Language server.
tods-validate lspre-validates the feed when you open or save a TODS file and marks each finding at its row and field, with rule detail on hover and quick fixes for trimming whitespace (TODS-W206) and deleting duplicate rows (TODS-W408). Install withpip install 'tods-validate[lsp]'. A thin VS Code client lives undereditor/vscode/. - Reports state their own scope. Every run records which rules ran and which were skipped and why, so "no problems found" is qualified by what was actually checked. The JSON report carries a
coverageblock (report schema 1.2.0), SARIF records it underinvocations, and the text, Markdown, and HTML reports add a one-line disclosure. - Local severity policy, disclosed. An optional
[severity]table intods-validate.tomlremaps individual rule severities. Every remapped finding is disclosed in every report format, and downgrading a rule the spec declares ERROR requires an explicitacknowledged = true. validate --suggestlists concrete, meaning-preserving fix suggestions after the report, each markedautoorreview.explain RULE_IDprints a rule's full detail offline, with a worked before/after example;--format markdownfor pasting into an issue.- Two run-continuity warnings. TODS-W316: a run event that works a trip end to end should match the trip's scheduled start and end times. TODS-W409: consecutive events in one run should connect in space.
- Test helpers for exporters.
tods_validate.testingprovidesassert_feed_validandassert_feed_producesso an exporter's own pytest suite can gate on the same checks the CLI and Action run. - Permanent per-rule pages at https://chelseakr.github.io/tods-validate/rules/, linked from SARIF
helpUriand hover text.
Changed and fixed
fixnow also drops entirely-blank rows and byte-identical duplicate rows; rows that share a key but differ in a value are left for a human. Still a dry run by default.- The HTML report passes an explicit accessibility pass (landmarks, table caption and scoped headers, WCAG AA contrast for all severities).
- The reported tool version comes from package metadata instead of a hand-edited constant that had drifted.
- TODS-W302 discloses when
vehicle_assignments.txtreferences could not be checked instead of silently skipping.
Release integrity
This is the first release whose tag is annotated, SSH-signed, and verified by CI against the committed .github/allowed_signers before anything publishes to PyPI or the container registry.
Install: pip install tods-validate==0.7.0 · Action: uses: ChelseaKR/tods-validate@v0.7.0 · Full details in CHANGELOG.md.
v0.6.0
v0.6.0 - 2026-06-29
New surfaces for working with a feed live (--watch, browser playground),
acting on findings (fix), and sharing results (stats --format markdown,
conformance corpus), plus a new cross-feed operational check (TODS-W315).
Added:
tods-validate validate --watchre-validates whenever the feed changes
(polls the files), the cheap interim ahead of editor/LSP integration.- A browser playground (
web/) that validates a feed entirely in the browser
via Pyodide, with no upload, deployable to GitHub Pages. The Python it calls
is guarded by tests; the page itself needs a browser to verify. - TODS-W315: a run event that works a trip end to end should start at the
trip's first stop and end at its last stop (in the supplemented
stop_times.txt); a mismatch is a warning, skipped for mid-trip events. The
companion GTFS now ingestsstop_times, so this checks an operational
consistency constraint no GTFS-only validator can see. tods-validate fixapplies safe, deterministic fixes — currently trimming the
TODS-W206 whitespace padding that stops IDs from matching. It is a dry run by
default and writes a cleaned, UTF-8/no-BOM package with-o.tods-validate stats --format markdownprints a feed profile (now including a
date range and a file-presence list) suitable for pasting into an issue or a
working-group thread.- A downloadable conformance corpus, attached to each release: every fixture
plus anexpectations.jsonmapping each to the rule IDs it should produce, so
another validator can run the suite without cloning the repo
(scripts/build_conformance_corpus.py).
v0.5.0
Correctness fixes (no rule IDs changed), a runnable bundled sample feed with a
fixed quickstart, and a conformance check that runs the spec's own examples.
Fixed (no rule IDs changed):
- TODS-E204 now detects duplicate
vehicle_assignmentsprimary keys when the
optionalservice_idis blank (the common case). Previously a blank optional
key component silently suppressed the whole uniqueness check, so real
duplicate keys passed clean and coalesced duringmerge. - Time values with hours
>= 100:00:00are now accepted (GTFS time has no upper
hour bound). They previously raised a false TODS-E203 and were dropped from the
time-based semantic checks (E401/E402/W403). - TODS-E314 no longer fires on a
stop_times_supplementrow whose trip was
deleted viatrips_supplement(TODS_delete=1); the spec says such
stop_times are ignored, not an error. - Duplicate header columns now keep the first occurrence's value (matching the
TODS-E105 message that the duplicate column is ignored) instead of letting a
later duplicate column silently win. - All-blank data rows (a stray
,,,line past the header) are no longer
silently dropped; their missing required values are now reported (TODS-E201). - TODS-E205 (vehicle_assignments block ambiguity) is now marked as requiring a
companion GTFS feed, so a TODS-only run reports it as unchecked instead of
silently passing the check.
Other:
- Bundled a runnable sample feed at
examples/sample-feed/and pointed the
README quickstart at it, so a new install has something that passes on the
first run. The GitHub Action now sets up Python explicitly.
v0.4.0
Distribution, reporting, and analysis surfaces. No rule IDs changed; the JSON
report gained fields (it is now reportVersion 1.1.0) without removing any.
Added:
tods-validate ruleslists the rule catalog from the tool itself
(--format jsonfor tooling, now including category, default-enabled, and
spec-interpretation metadata).- Published JSON Schema for the
--format jsonreport
(docs/report.schema.json), enforced by tests. - Dockerfile and a workflow publishing images to GHCR on each release.
- pre-commit hook definition (.pre-commit-hooks.yaml).
- New report formats:
--format sarif(GitHub code-scanning / security
dashboards) and--format html(a standalone, shareable report). - JSON report now carries
toolVersion,reportVersion, a per-rule
summary.byRulebreakdown, and a stablelocationpointer per finding. - Text and Markdown reports group findings by rule, show the shortest path to a
clean run, and add root-cause hints when one rule clusters. - New flags on
validate:--enable(opt-in rules/categories),--profile
(default/strict/lenient presets),--spec-version,--baseline(fail only
on findings new since a previous JSON report),--max-findings,--quiet,
--stamp(citable Markdown footer), and--encoding. - New subcommands:
diff(compare two feeds),batch(validate many feeds
with a roll-up table),stats(descriptive feed metrics), andanonymize
(pseudonymize person-identifying fields). mergenow writes amerge-report.jsonmanifest alongside the merged feed.- The GitHub Action exposes
error-count,warning-count, andinfo-count
outputs and accepts anenableinput. - New opt-in rules: TODS-I501 / TODS-I502 (coverage) and TODS-I601 (advisory).
- Public Python API:
from tods_validate import validate_feed. - Input-safety hardening of zip ingestion (zip-bomb and path-traversal
defenses, size limits) and aSECURITY.md. scripts/benchmark.pyfor throughput measurement on large synthetic feeds.
v0.3.0
The merge pipeline:
- New
mergesubcommand writes the "TODS-Supplemented GTFS" dataset (the GTFS feed after supplement rows are applied) to a directory or .zip, with per-file counts of updated, added, and deleted rows. Check the result with MobilityData's gtfs-validator; the README shows a CI recipe. - New rule TODS-E314: a supplement row references a route, service, trip, or stop that does not exist in the supplemented feed.
- The CLI gains explicit
validateandmergesubcommands.tods-validate PATHstill validates, so existing invocations and the GitHub Action are unaffected.
Full changelog: https://github.com/ChelseaKR/tods-validate/blob/main/CHANGELOG.md
v0.2.0
Rule suppression and reporting:
--ignore TODS-Wxxx(repeatable) suppresses specific rules.- Optional
tods-validate.tomlconfiguration file (ignore,fail-on), discovered in the working directory or passed with--config, so CI policy lives in the repo instead of the command line. --format markdownemits a report suitable for pasting into an issue or working-group thread.
Full changelog: https://github.com/ChelseaKR/tods-validate/blob/main/CHANGELOG.md