Skip to content

Canonical PAX/RTP data model, corrected 2024/2026 data, SCCA classification, and automated updates - #1

Merged
andrewdefilippis merged 8 commits into
masterfrom
feature/canonical-data-v2
Oct 1, 2026
Merged

andrewdefilippis merged 8 commits into
masterfrom
feature/canonical-data-v2

Conversation

@andrewdefilippis

@andrewdefilippis andrewdefilippis commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Problem / background

This repository turns the hand-maintained PAX/RTP pages at solotime.info/pax into machine-readable files. A review against the live source found that the data and tooling had drifted:

  • Stale data. solotime.info revised the 2024 index on 2023-12-20, ten days after it was scraped. XS/SSR were removed, XU 0.869 was added, EVX went 0.830 → 0.834 and XA 0.842 → 0.844. The repo never picked this up. The same thing happened to 2026: master added it on 2026-01-04, and the page was then revised on 2026-03-20 (CAM-C 0.827 → 0.828, GS 0.793 → 0.809, SS 0.837 → 0.840, XU 0.869 → 0.872). This branch merges master and supersedes those values.
  • Lossy scraping. Labels like ST (STS) (2009) and AST(STR) (2025) mark renames and redistributions. The scraper either dropped the annotation, which spliced unrelated classes into one series (STS before and after 2009 are different classes), or kept it in the key (AST(STR)). All errors were swallowed.
  • Non-idempotent combined file. rtp_files_generator.py merged with an append strategy, so every re-run duplicated a year and a revised page could not replace one.
  • Wrong categories. Regex-derived categories put XP in Xtreme Street and XS in Street, and left 31 classes blank.
  • Other issues. YAML year keys were integers while JSON's were strings, rtp_form_generator.py did not run, and nothing was automated.

What is changing

  1. Canonical CSV (CSV/):
    • rtp.csv: each index exactly as published, plus its verbatim label.
    • sources.csv: URL, the page's "Last update" date, fetch time and SHA-256 for each year.
    • classes.csv: SCCA name and category per year, with a citation and verified/unverified status.
    • lineage.csv: renamed and derived classes.
  2. Typed, strict tooling.
    • Scripts/rtp_model.py parses CSVs into domain types (Parse, don't validate).
    • The scraper fails loudly on anything unexpected, honors the site's 10 s crawl delay, and replaces whole years on revision.
    • The generator rebuilds all outputs from CSV, so it is idempotent.
  3. Published files.
    • JSON/<year>.json keeps its shape.
    • New JSON/latest.json / YAML/latest.yaml.
    • YAML year keys are now strings.
    • rtp.json v2 maps each class's years to Index, Name, SoloCategory, ClassificationVerified, plus Predecessors/Successors and per-year source metadata. JSON Schemas are in Schema/.
  4. Automation.
    • ci.yml runs the tests and checks that generated files match the CSV.
    • update-rtp.yml runs weekly and opens a PR on any source change. PRs that need a new class classified open as drafts labeled needs-classification.
  5. uv. pyproject.toml + hash-locked uv.lock (one-week exclude-newer, no-build) replace requirements.txt. Dependabot covers actions and uv.
  6. Docs. README data contract and maintainer notes, plus a CHANGELOG listing the breaking changes.

Why

  • Clubs' scoring software needs data that is correct, current and stable to consume.
  • The source is edited by hand and revised mid-season, so revisions have to be detected automatically.
  • Provenance has to be recorded.
  • Class identity has to be explicit, because SCCA reuses codes:
    • 2014: every Stock class became Street-R for one year, while AS–HS were reused for the new Street category.
    • 2025: Street Touring was redistributed. For example, most of STR went to CST, while the ND MX-5 went to AST.

Expected outcome

  • Every year 1995–2026 matches solotime.info exactly. This was checked with two independent parsers against all 32 pages.
  • Running the generator twice produces no diff, and CI enforces this.
  • 1,092 of 1,311 class-years (83%) have SCCA name and category verified against a source. The rest are marked unverified with a reason, and 10 early class-years that no source names are null.
  • A source revision shows up as a weekly PR with a diff summary. A layout change fails the job instead of committing bad data.

Breaking changes for consumers (see CHANGELOG.md):

  • rtp.json/rtp.yaml have a new shape.
  • The 2025 keys AST(STR) etc. become AST etc.
  • YAML year keys are strings.

Actions / tests

  • uv run pytest: 183 tests covering:
    • the parser (every known label and value quirk, using hand-written fixtures);
    • the model, including CSV injection, Unicode and URL validation;
    • the fetcher (crawl delay, retries, redirects, size and time limits);
    • main() modes, including year rollover;
    • the generator (schemas, idempotency, consistency of the real data);
    • the PR-job patch guard, run against real git with 20+ bypass attempts.
  • The CI steps were simulated in a fresh checkout with uv sync --locked.
  • Full live re-fetch of 1995–2026. The diff against the old JSON was exactly the expected set listed above.
  • actionlint is clean.
  • Security audit (security-auditor), repeated until nothing Medium or above remained. The resolutions include:
    • splitting the update workflow by privilege;
    • hash-locked dependencies and a checksum-pinned uv binary;
    • a stdlib-only, index-based patch guard;
    • redirect, size and time limits;
    • CSV formula and markup rejection;
    • sanitizing the PR body.
  • Staff-level code review. It approved with comments, and all comments were addressed or documented.

Owner follow-ups after merge (repository settings):

  • Settings → Actions → General: enable "Allow GitHub Actions to create and approve pull requests".
  • Protect master and require the CI / test check, because bot PRs opened with GITHUB_TOKEN don't trigger CI on their own.
  • Run "Update RTP data" once with workflow_dispatch to confirm the end-to-end flow.

References

🤖 Generated with Claude Code

andrewdefilippis and others added 8 commits October 1, 2026 02:41
The old scraper discarded rename annotations (e.g. "ST (STS)", "AST(STR)"),
truncated values by string slicing, and swallowed every parse error, so bad
pages produced silently wrong data. The generator merged into rtp.json with an
append strategy, which duplicated a year on every re-run and could not apply a
revised page, and its regex-derived categories were wrong for XP, XS and 31
other classes. The form generator never ran.

The scraper now parses each page strictly into typed values (year, class
code, verbatim label, exact decimal index, "Last update" date) and fails
loudly on anything unexpected. It honors solotime.info's 10 s crawl delay,
restricts redirects to the source host, and bounds response size and time.
A revised page replaces its whole year. The generator rebuilds every output
from the canonical CSV, so running it twice is a no-op, and JSON Schemas
describe the published files.

Dependencies move to uv: pyproject.toml plus a hash-locked uv.lock with a
one-week release-age limit and source builds disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rules

The per-year JSON had drifted from the source: solotime.info revised the 2024
index on 2023-12-20, ten days after it was scraped (XS and SSR removed, XU
0.869 added, EVX 0.830 -> 0.834, XA 0.842 -> 0.844), and 2026 was never added.
Every year 1995-2026 was re-fetched; apart from those changes the only
differences are verbatim labels now recorded (2006, 2007, 2009, 2010, 2025).

CSV/rtp.csv holds each index exactly as published plus its page label.
CSV/sources.csv records the URL, the page's own "Last update" date, the fetch
time and a SHA-256 for each year, so later revisions are detectable.

CSV/classes.csv assigns SCCA class names and categories by year from the
National Solo Rules (and SCCA's own web excerpts before 2005), with a citation
and a verified/unverified status: 1,092 of 1,311 class-years are verified.
Notable: Stock became Street in 2014 (the old Stock classes ran one year as
Street-R). The 2024 rulebooks independently confirm the December 2023
revision.

CSV/lineage.csv links renamed and redistributed classes (2008 Formula Junior,
2009 ST/STS/SSM, 2014 Street-R, 2023 EVX, 2025 Street Touring), because SCCA
reuses class codes and a code's history alone is misleading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consumers, expected to be other clubs' scoring software, get corrected data
and a combined file that can be queried by year:

- JSON/<year>.json keeps its shape. 2024 reflects the December 2023 revision,
  2026 is added, and 2025 keys drop the "(STR)"-style suffixes (AST, BST, DST,
  EST, GST) that were scraping artifacts.
- JSON/latest.json and YAML/latest.yaml always hold the newest year.
- YAML year keys are now strings, matching JSON (they were integers).
- rtp.json / rtp.yaml (SchemaVersion 2) map each class's years to Index,
  Name, SoloCategory and ClassificationVerified, replacing a list of
  single-key objects that allowed duplicates, and add Predecessors /
  Successors plus per-year source metadata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
solotime.info publishes a new year each autumn and revises indices into the
season; the 2024 revision went unnoticed for almost three years. A weekly
workflow now fetches the newest published year and the one before it and
opens a reviewable PR when anything changes. PRs that introduce an
unclassified class open as drafts labeled needs-classification.

Security design (audited over several passes):
- A read-only "build" job runs uv, the scraper, the generator and tests and
  hands off a git patch. The write-scoped "open-pr" job runs no third-party
  Python; it applies the patch with a stdlib-only guard that inspects git's
  index (byte-exact paths, regular-file modes only) so a compromised build
  cannot touch workflows or plant symlinks, then sanitizes the PR body.
- Actions and the uv binary are pinned by SHA/checksum; workflow inputs reach
  the shell only through validated env vars.

CI runs the tests and fails if generated files differ from the CSV.
Dependabot tracks actions and uv.lock with a seven-day cooldown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Third-party software needs to know which files are canonical vs generated,
stable URLs, what Name/SoloCategory/ClassificationVerified mean (including
null for classes no source names), and that class codes are reused across
SCCA restructures. The CHANGELOG lists the breaking v2 changes and data
corrections for existing consumers. Maintainer notes cover the GITHUB_TOKEN
CI caveat, scheduled-workflow auto-disable, required repository settings,
and bumping the pinned uv version with its checksum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
master added 2026 on 2026-01-04, before solotime.info revised the 2026 index
(page "Last update March 20, 2026"): CAM-C 0.827 -> 0.828, GS 0.793 -> 0.809,
SS 0.837 -> 0.840, XU 0.869 -> 0.872. Conflicts in JSON/2026.json,
YAML/2026.yaml, rtp.json and rtp.yaml are resolved by regenerating from the
canonical CSV, which holds the current page's values and the AST/BST/DST/
EST/GST keys without "(STR)"-style suffixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
master already carried 2026 data from January 4; this branch updates it to
the March 20, 2026 revision of the source page, which changed four indices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
master's 2026 files used the same suffixed keys, so consumers reading 2026
are affected by the rename too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@andrewdefilippis
andrewdefilippis merged commit 741d6cd into master Oct 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant