Skip to content

Releases: ChelseaKR/tods-validate

v0.11.0

Choose a tag to compare

@ChelseaKR ChelseaKR released this 02 Sep 03:30
v0.11.0
cc5d0ca

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

Choose a tag to compare

@ChelseaKR ChelseaKR released this 22 Aug 06:45
v0.10.0
a019bbe

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 them ran. The same shape reached two TODS-internal
    checks: an unreadable run_events.txt or vehicles.txt produced invented
    TODS-E301/TODS-E303 findings the same way. All three now treat an
    unreadable file the same as a missing one — the rule is skipped
    (skipped:needs_gtfs_table for the companion-GTFS case), and TODS-W302
    discloses that the file could not be read (pointing to TODS-E103 for the
    reason on the TODS side), instead of silently reporting has no <file> or,
    worse, inventing errors against it.
  • diff OLD NEW reported 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, and diff could not tell them apart: comparing
    tests/fixtures/invalid/TODS-E307 (a bad trip_id reference) against the
    same package with the companion trips.txt removed reported fixed: 1
    and exit 0, when the bad reference was never re-checked, let alone fixed.
    diff now uses run_with_coverage and only counts an OLD-only finding
    fixed when its rule ran in NEW; otherwise it lands in a new unknown
    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.
  • batch used the two-tuple run() 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 read 0 0 0 pass — exactly the numbers a
    fleet compliance artifact is read for, with nothing saying the run was
    partial. batch now uses run_with_coverage. Every format carries the
    manifest: text and Markdown gain a "checks not run" column beside each
    feed's status plus a fleet-wide Rule-set coverage line in the roll-up
    (pooling every feed's outcomes, the same disclosure a single-feed report
    already carries); --format json adds a per-feed checksNotRun count and
    a coverage block matching validate --format json's.
    --require-complete-run (#124) is now available on batch too: a feed
    with an unrequested skip (missing/unreadable companion GTFS) fails that
    feed, the same as it does for validate.
  • uv.lock pins pip at 26.2.1, past PYSEC-2026-3721 (disclosed after
    26.1.2 was pinned). Vendored only as a transitive build/audit tool, never
    imported by tods_validate itself, but it was failing make audit (and
    would fail it for any PR, unrelated to that PR's own change) until bumped.
  • fix -o OUT and anonymize -o OUT no 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. fix compounded it
    by printing Nothing 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 --encoding if the file is deliberately not UTF-8.
    Packages that load cleanly are unaffected.
  • --format github now 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_github took no coverage argument
    at all, so a feed validated without a companion GTFS feed printed
    0 error(s), 0 warning(s), 0 info and 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 deviation docs/adr/0001 had
    recorded since 2026-07-09. docs/adr/0006-python-312-floor.md supersedes
    1. README.md and CONTRIBUTING.md now say "Requires Python 3.12 or
      newer"; CI's test matrix is 3.12/3.13 (3.11 dropped). Installed
      releases are unaffected; this binds new installs, upgrades, and local dev.

Added:

  • TODS-E207 checks that routes_supplement.txt's route_color and
    route_text_color are 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 typed Text by
    schema._supplement() regardless of the base file's real GTFS type, so
    these two carried no format check at all before this; _supplement()
    gained a field_types override used only for these two fields, rather
    than transcribing the full GTFS field-type inventory for a single rule.
    (#101)
  • --require-complete-run fails 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
    the require-complete-run input.
  • 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 leaving 0
    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

Choose a tag to compare

@ChelseaKR ChelseaKR released this 16 Aug 15:20
v0.9.0
ed96461

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 only TODS-W408 (WARNING), so on the default
    fail-on: error it did not fail the job. It does now. TODS-W408 is 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 single agency.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.txt modifies trips.txt; without trips.txt there
    is nothing to resolve a trip_id against). This also stops TODS-I501
    reporting trip coverage computed from supplement rows alone.
  • docs/report.schema.json now lists skipped:spec_version. The validator
    has emitted it since v0.8.0 without documenting it, so a report produced with
    --spec-version 1.0.0 failed 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, and routes.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 in tods_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
    with uv sync --frozen, which installs exactly what uv.lock records and
    exits 0 whether or not the lock still agrees with pyproject.toml. Measured:
    with pyproject.toml at 0.9.0 and uv.lock at 0.8.0, uv sync --frozen --extra dev exits 0 while uv lock --check exits 1. uv lock --check now
    runs before every uv sync in CI and as the first gate in make verify.
    ADR 0005, which claimed --frozen "fails on any lockfile drift", records the
    correction.
  • The accessibility gate now performs an accessibility check. make a11y began
    with npm 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, and make verify runs 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

Choose a tag to compare

@ChelseaKR ChelseaKR released this 17 Jul 03:53
v0.8.0
5f19d0b

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-ready profile.

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

Choose a tag to compare

@ChelseaKR ChelseaKR released this 11 Jul 17:44
v0.7.0
fd52665

tods-validate 0.7.0 validates TODS v2.1 feeds from the editor as well as the command line.

New

  • Language server. tods-validate lsp re-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 with pip install 'tods-validate[lsp]'. A thin VS Code client lives under editor/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 coverage block (report schema 1.2.0), SARIF records it under invocations, and the text, Markdown, and HTML reports add a one-line disclosure.
  • Local severity policy, disclosed. An optional [severity] table in tods-validate.toml remaps individual rule severities. Every remapped finding is disclosed in every report format, and downgrading a rule the spec declares ERROR requires an explicit acknowledged = true.
  • validate --suggest lists concrete, meaning-preserving fix suggestions after the report, each marked auto or review.
  • explain RULE_ID prints a rule's full detail offline, with a worked before/after example; --format markdown for 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.testing provides assert_feed_valid and assert_feed_produces so 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 helpUri and hover text.

Changed and fixed

  • fix now 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.txt references 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

Choose a tag to compare

@ChelseaKR ChelseaKR released this 30 Jun 02:22

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 --watch re-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 ingests stop_times, so this checks an operational
    consistency constraint no GTFS-only validator can see.
  • tods-validate fix applies 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 markdown prints 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 an expectations.json mapping 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

Choose a tag to compare

@ChelseaKR ChelseaKR released this 23 Jun 02:25

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_assignments primary keys when the
    optional service_id is blank (the common case). Previously a blank optional
    key component silently suppressed the whole uniqueness check, so real
    duplicate keys passed clean and coalesced during merge.
  • Time values with hours >= 100:00:00 are 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_supplement row whose trip was
    deleted via trips_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

Choose a tag to compare

@ChelseaKR ChelseaKR released this 22 Jun 19:52

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 rules lists the rule catalog from the tool itself
    (--format json for tooling, now including category, default-enabled, and
    spec-interpretation metadata).
  • Published JSON Schema for the --format json report
    (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.byRule breakdown, and a stable location pointer 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), and anonymize
    (pseudonymize person-identifying fields).
  • merge now writes a merge-report.json manifest alongside the merged feed.
  • The GitHub Action exposes error-count, warning-count, and info-count
    outputs and accepts an enable input.
  • 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 a SECURITY.md.
  • scripts/benchmark.py for throughput measurement on large synthetic feeds.

v0.3.0

Choose a tag to compare

@ChelseaKR ChelseaKR released this 12 Jun 05:18

The merge pipeline:

  • New merge subcommand 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 validate and merge subcommands. tods-validate PATH still 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

Choose a tag to compare

@ChelseaKR ChelseaKR released this 12 Jun 05:14

Rule suppression and reporting:

  • --ignore TODS-Wxxx (repeatable) suppresses specific rules.
  • Optional tods-validate.toml configuration 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 markdown emits a report suitable for pasting into an issue or working-group thread.

Full changelog: https://github.com/ChelseaKR/tods-validate/blob/main/CHANGELOG.md