Repository navigation
Run Pyright in CI for every package, in its own locked environment - #864
Merged
Merged
Conversation
policyengine-observability annotates its scope managers' __exit__ as returning bool, although it always returns False. Pyright reads a bool result as "this context manager may suppress the exception", so it reports every variable assigned inside `with runtime.span(...)` as possibly unbound afterwards. scopes.span returns the runtime's own scope, typed as a context manager whose __exit__ returns bool | None, which type checkers don't treat as suppressing exceptions. The entry's Stage 12 dispatch uses it, which fixes its two Pyright errors (invocation_id possibly unbound). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Pyright rule's enforcer now lists the package directories it checks (`projects`), replacing the single `config` file: in this repository each package configures Pyright in its own pyproject.toml. The guard checks that each package exists, declares what Pyright checks with a [tool.pyright] include, and appears in the matrix of the rule's CI job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`python -m scripts.rules pyright <package>` finds the Pyright rule that lists the package, runs the pinned Pyright version (1.1.414) in the package directory against the interpreter of the package's locked environment, and compares the errors with the rule: - under an enforced rule the package may have no errors; - under a ratcheted rule it may have exactly the errors its baseline lists. A new error fails, and so does an entry for an error that no longer occurs, so the baseline only shrinks. Pinned Pyright 1.1.409 to 1.1.414 have no --baselinefile option, and their messages differ for the same error, so the baseline is a text file of `<path> :: <rule> :: <first message line>` entries, one per error and without line numbers, compared by this standard-library script. --write-baseline drops entries that no longer occur and never adds one. Without a package argument every listed package is checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every package's pyproject.toml now has a [tool.pyright] include: src for the libraries and services, and the integration tests for apis-integ, whose code has no src directory. Without one, Pyright checks the whole package directory, tests included: the contract library reported 32 errors in 25 files instead of none in its 14 source files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entries were written in the order Pyright reports errors, which moves an entry whenever code above it moves. Sorted, a fix's diff only removes lines, and identical errors sit next to each other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new pyright job in pr.yml checks each uv package in its own locked environment with `python -m scripts.rules pyright`. It generates the API client first for apis-integ, which installs it from a path. CI complete needs the job, and it runs only at the bottom of a stack, like the other jobs. Rules: - sim.quality.pyright-libraries-and-entry (enforced): the contract, observability, Stage 12 persistence and fastapi libraries and the entry have no errors. - sim.quality.pyright-gateway (ratcheted, 81 errors), sim.quality.pyright-executor (ratcheted, 178) and sim.quality.pyright-apis-integ (ratcheted, 4): only the errors their baselines list. Each rule's proof runs the comparison on recorded Pyright reports from main at ceb30e1. The enforced rule's bad fixture is the entry's real pre-fix report. docs/engineering/skills/type-checking.md explains the check, the pinned version and the baselines. The registry now has 64 rules, past the renderer's limit of 60, so the AGENTS.md rules block lists checked rules by id per category. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`python -m scripts.rules pyright --sync` runs `uv sync --frozen --all-extras` in each package before checking it, and fails the package when the sync fails. make check now runs it for every listed package, after generating the API client that apis-integ installs, instead of running `uv run pyright <dir>` from the repository root, which used no package's environment. It also runs every check and fails at the end if any failed; before, only the last directory's result counted. The CI job runs the same command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pyright-coverage guard lists every uv package (a directory under libs/ or projects/ with a pyproject.toml) and fails for each one that no Pyright rule lists, naming the registry entry and the CI matrix to update. Registered as sim.quality.every-package-type-checked, with a proof fixture holding a package no rule lists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`python -m scripts.rules pyright --write-baseline` with no package
argument tried every listed package, failed for each enforced one
("is enforced, so ... has no baseline") and exited 1, even though it
had rewritten the ratcheted baselines. Without packages it now rewrites
the baseline of every package a ratcheted rule lists, and leaves the
enforced packages alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs and help text said --write-baseline never adds an error. It refuses an error an existing baseline doesn't list, but it records every error when the baseline file doesn't exist yet, which is how a package is first ratcheted. The docs now say so, and that the rules guard's count comparison is what stops a baseline growing. New tests pin two paths that had none: a run with both a new error and an entry that no longer occurs reports both and both fixes, and --sync with --write-baseline syncs before rewriting. Both pass on the existing code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both targets ran `uv run ruff` from the repository root, which runs whatever ruff is on PATH. With ruff 0.16.8 there, make check reported 154 CPY001 findings that CI's ruff 0.15.22 doesn't select, and failed. The Makefile now reads the version from the lint job's `uv tool install ruff==<version>` step in pr.yml and runs `uv tool run ruff@<version>`, and stops with a message if it can't read one. With 0.16.8 still first on PATH, make check now passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 2026
anth-volk
added this pull request to stack #867
October 21, 2026 14:53
This was referenced Oct 21, 2026
The guard compared baseline counts with the base branch only when the base rule already had a baseline, and counted only a move to advisory or doc-only as a weakening. So an enforced rule could become ratcheted with a baseline of any size and nothing would object. Moving from enforced to ratcheted now needs a [rule.downgrade] reason, like the other weakenings, and the registry docs say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two gaps in what the guard checks about a Pyright rule's packages: - A package listed by two Pyright rules was checked against the first only, and twice in a default run. Registry validation now reports the second listing. - A pyrightconfig.json in a package takes precedence over the [tool.pyright] table the guard inspects, so it could switch checking off unseen (`typeCheckingMode: off` gave 0 errors on a file with one). The guard now fails when a listed package has one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `pyright --write-baselines` (the ci-scripts flag) ran a plain check without writing anything; it is now a usage error that names --write-baseline. - `--list-packages` ignored package arguments, --sync and --write-baseline; combining them is now a usage error. - `--write-baseline` with no package argument and no ratcheted Pyright rule wrote nothing and said only "pyright: passed"; it now says there is no baseline to write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
make check ran `ruff check` on each src directory and no format check, while CI's lint job runs `ruff format --check projects/*/src libs/*/src` and `ruff check .`, so a passing make check didn't mean a passing job. It now runs those two commands, with CI's version. The version is read from the first `uv tool install ruff==` line in pr.yml; before, a second such line would have produced two versions and a broken command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The type-checking skill said deleting and rewriting a baseline would pass the guard only if the count stayed the same, as though entries were compared. The guard compares counts, so a change that fixes one error and adds another passes it; the skill now says so, names code-owner review of rules/ as what catches that, and lists the package settings (ignore, typeCheckingMode, rule severities) that could weaken the check without failing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 2026
Started in a package directory without --project, Pyright 1.1.414 also uses a pyrightconfig.json from a parent directory, such as the repository root, libs/ or projects/, instead of the package's [tool.pyright]. A parent file with `typeCheckingMode: off` reported 0 errors on a file with one. The check now passes `--project <package>`, so Pyright reads only the package's own configuration; the guard already rejects a pyrightconfig.json inside a package. A new test runs the pinned Pyright for real on a package with parent pyrightconfig.json files: it found no errors before and finds the one error now. The quality-guards job installs uv so the test runs there; it fails in CI if uv is missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The duplicate-package check normalised paths with rstrip("/"), but the
Pyright check normalises with os.path.normpath, so ./libs/alpha in one
rule and libs/alpha in another passed validation and were then checked
as one package against the first rule only. Both now use one function,
normalized_package, so the second listing is reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
October 21, 2026 20:22
anth-volk
added a commit
that referenced
this pull request
Oct 21, 2026
Main now runs Pyright on every package (#864). Against the executor's baseline, this branch added seven errors: - The run's new reads of `dataset_selection`, `region_resolution` and `simulation_params` were reported as possibly unbound, because `runtime.span(...)` is typed as possibly suppressing exceptions. The run's spans now open with `scopes.span(runtime, ...)`, as docs/engineering/skills/type-checking.md says. `_build_simulation` is annotated with the `StageReportingSimulation` it returns, which the progress callback needs. - `build_state_datasets` couldn't be passed as a `RemoteFunction`, because Pyright doesn't resolve `modal` in src/modal to the Modal package. The precompute app casts its six Modal functions to that protocol. - The manifest's artifact list is built with `ManifestEntry` elements, and the district-code check passes `isin` a sorted list. 54 baseline entries no longer occur, so the baseline drops from 178 to 124 entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 tasks done
anth-volk
added a commit
that referenced
this pull request
Oct 21, 2026
…#879) * Report the US state code when resolving a state or district region RegionResolution gains state_code: the two-letter code of the US state a state or congressional-district region lies in, taken from the .py region. National, UK and region-group resolutions leave it None. The baked per-state datasets added next are looked up by this code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let callers choose the folder _load_dataset reads a year file from _load_dataset takes an optional data_folder that replaces the folder the dataset selection would otherwise use (the image's data folder for the default dataset, a temporary folder for any other). Callers are unchanged; the main path and Stage 12 will each pass the folder of a baked per-state dataset when one exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Key and lay out per-state datasets for the main path's precompute A per-state dataset is a regional dataset's year file filtered to one state's households. Its content key (schema sds1) covers the regional dataset's name, revision-pinned URI and certified SHA-256, the year, the state code and FIPS, the filter variable, the model and .py versions and the SPM selection. Its store path is state-datasets/<country>/<digest>/<stem>_year_<year>.h5, keeping the basename ensure_datasets resolves. state_datasets.py gives each state's folder under the image data folder, state-datasets/<dataset>/<STATE>, so the manifest builder and the runtime lookup agree on where a state's files live. The default dataset's key and identity are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Plan a per-state dataset for each state that loads a regional dataset state_dataset_targets resolves every US state region exactly as a request for it is resolved and keeps the states whose dataset is not the bundle default, with their row filter (state_fips) and state code. The plan lists one StateDatasetPlanEntry per precompute year and target state, year-major and by state code, with its key, store path, runtime filename, image folder and store presence; select_work keeps the missing ones. A bundle that sends states back to the default dataset plans none. Nothing builds or publishes these entries yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Build, check and upload one year's per-state datasets build_state_datasets_impl re-derives each planned state's key in its own container and refuses to build when the planner disagreed, then has .py's ensure_datasets prepare the regional dataset's full-country year file once, collects garbage, and reloads that one year. It refuses data in which a household's congressional district code names another state, since district requests read their state's file. For each state it keeps the rows the state region's own scoping strategy keeps, writes them with .py's save(), copies the year-file stamp that save() drops, confirms that .py's reuse checks (stamp and renames record, then the legacy SPM record) accept the file, and uploads it write-once. Weights are unchanged. The tests write a three-household year file with .py's own save, stamp and SPM record, and show that .py's real ensure_datasets reuses a state file without rebuilding it. The identity stubs now import .py's US dataset module before patching the manifest, so a stubbed test cannot bind the stub into it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Place manifest artifacts that name a folder in that folder of the image The image's artifact fetch now puts an artifact with a "folder" at <data folder>/<folder>/<filename>, so per-state datasets, which all share the year file's basename, land in their own state folders. Artifacts without a folder still go straight into the data folder. Before anything downloads, it refuses absolute folders, folders with "." or ".." parts, empty parts or backslashes, filenames that are not bare names, and two entries for the same file. The fetch layer's timeout rises from 900 s to 3,600 s for the several GB of per-state datasets. No manifest lists such artifacts yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run the per-state dataset builds in the main path's precompute The precompute app gains build_state_datasets (8 CPUs, 96 GiB, two hours), one container per year, validating plan entries in and dumping build results out like the other wrappers. run_precompute spawns the state builds with the dataset builds and waits for them only before publishing the manifest, so baseline computation never waits for them, then reports how many state datasets were built and uploaded and their total size. The manifest does not list the state datasets yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * List the per-state datasets in the main path's deploy manifest build_manifest appends a state_dataset entry for every planned state file, with its folder and state code, after the datasets and baselines. The new entries use their own StateDatasetArtifact model, chosen by "type", so a plan without per-state datasets publishes byte-for-byte the manifest it always did (the pinned wire-shape test is unchanged). The deploy-time manifest validation accepts the new entries and hands them to the fetch layer, which places each in its state's folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read a state's baked dataset for US state and district requests Before loading a non-default dataset, the main path's runtime asks state_datasets.baked_state_data_folder for the image folder of the request's state (a district passes its state's code). It returns the folder only when the image holds that state's year file for the requested year and .py's reuse checks (stamp and renames record, legacy SPM record) accept it; a file .py would rebuild is skipped, so ensure_datasets never rebuilds the full regional dataset inside a baked folder. _load_dataset then reads from that folder, and the district filter still applies at run time. Otherwise the request prepares the regional dataset on demand, as before. Runs whose region loads a non-default dataset record dataset_source as baked_state or on_demand. National and default-dataset runs are unchanged, and Stage 12 still loads without a folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse only district codes that name another state's district The per-state build refused the whole year whenever any household's congressional_district_geoid did not start with its own state FIPS, including codes that name no district, such as an unassigned 0. Such a household matches no district request on either path, so it cannot change a result, yet it would have stopped the precompute and the deploy waiting on it. The build now refuses only codes that name a registered district of a different state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Build state datasets per year and dataset, and report what was uploaded run_precompute grouped state entries by year only, while one build prepares a single dataset's year file and refuses entries for two. A bundle sending different states to different datasets would have failed every year's build; builds are now grouped by year and dataset. Each state build result records how long the year's build had run when the file was uploaded, and the precompute log now reports, per build, how many files were built and uploaded, the bytes actually uploaded (files already stored no longer count), and the build time, for tuning the two-hour limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test district loading end to end and state file rows against the source A district request is now followed from region resolution, with the real dataset selection and baked-folder lookup over a stamped Utah file, to the folder _load_dataset is given; the old test only repeated a state lookup. Each state file's rows are now compared with .py's filter applied to the full source frames, instead of with the file filtered again, which would not have noticed another state's rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Type the state target's row filter and refuse a target without one StateDatasetTarget.scoping_strategy is typed as an optional RowFilterStrategy instead of Any, and a build whose target has none raises a clear error before writing anything, instead of failing on None. A comment in artifact_keys now says that per-state datasets are fetched into their state's folder rather than straight into the data folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Load on demand when .py's year-file reuse checks cannot be imported The baked-folder lookup runs .py's reuse checks, one of which imports a private .py helper. If a .py upgrade moved it, every state and district request would have failed with ImportError while loading its dataset. The lookup now logs a warning and returns no folder, so the request prepares its dataset on demand as it would without a baked file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse a state without a row filter before preparing the year file The check for a state target without a row filter ran inside the per-state loop, after the full-country year file was prepared and earlier states were uploaded. _checked_state_targets now refuses it with the other in-container checks, before anything is prepared, and returns each state's row filter. Docstrings now say builds run one container per year and dataset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix the executor Pyright errors this branch adds Main now runs Pyright on every package (#864). Against the executor's baseline, this branch added seven errors: - The run's new reads of `dataset_selection`, `region_resolution` and `simulation_params` were reported as possibly unbound, because `runtime.span(...)` is typed as possibly suppressing exceptions. The run's spans now open with `scopes.span(runtime, ...)`, as docs/engineering/skills/type-checking.md says. `_build_simulation` is annotated with the `StageReportingSimulation` it returns, which the progress callback needs. - `build_state_datasets` couldn't be passed as a `RemoteFunction`, because Pyright doesn't resolve `modal` in src/modal to the Modal package. The precompute app casts its six Modal functions to that protocol. - The manifest's artifact list is built with `ManifestEntry` elements, and the district-code check passes `isin` a sorted list. 54 baseline entries no longer occur, so the baseline drops from 178 to 124 entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #863
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Summary
Runs Pyright on every pull request for every uv package, each in its own locked environment, and holds each package to a registered rule. This is the base of a stack (#867): #866, #869, #871, #873, #875 and #877 above it reduce the remaining errors to zero and make every rule enforced.
sim.quality.pyright-libraries-and-entry(enforced)sim.quality.pyright-libraries-and-entry(enforced)sim.quality.pyright-gateway(ratcheted)rules/baselines/pyright-gateway.txtsim.quality.pyright-executor(ratcheted)rules/baselines/pyright-executor.txtsim.quality.pyright-apis-integ(ratcheted)rules/baselines/pyright-apis-integ.txtA fifth rule,
sim.quality.every-package-type-checked, fails the quality guards when a package underlibs/orprojects/has no Pyright rule.How the check works
python -m scripts.rules pyright [--sync] [<package>...](standard library only, inscripts/rules/pyright.py):enforcer.projectslists the package;--sync, runsuv sync --frozen --all-extrasin the package;uv tool run --from pyright==1.1.414 pyright --outputjson --pythonpath <package>/.venv/bin/python --project <package>) in the package directory, so the package's[tool.pyright] includedecides what is checked and imports resolve against its own dependencies.--projectmakes Pyright read only the package's own configuration: without it, Pyright also uses apyrightconfig.jsonfrom a parent directory;The
pyrightjob inpr.ymlruns it once per package in a matrix, after generating the API client for apis-integ.CI completeneeds the job, and it has the same stacked-PR condition as the other jobs.make checkruns it for every package.Decisions
Baselines: a standard-library comparison, not
--baselinefile. None of the pinned versions (1.1.409, 1.1.411, 1.1.414) accepts--baselinefileor--writebaseline: both reportUnexpected option. Those options exist only in the basedpyright fork. So the script readspyright --outputjsonand compares it with a text baseline: one line per error,<path> :: <Pyright rule> :: <first line of the message>, sorted, without line numbers so edits elsewhere in a file don't change it. One line per error keeps the registry's[rule.baseline] countequal to the number of tolerated errors, so the guard's "baselines may only shrink" comparison counts errors.--write-baselinerewrites the file as the errors that occur now and refuses when one is new, so it can only remove entries.Pyright version: one pinned version (1.1.414) for every package, not each lock file's. Results are not stable across the versions in use: for the same two executor errors in
stage12_parity.py, 1.1.409 printsSeries | Unknown | DataFrameand 1.1.414 printsSeries | Unknown | Self@DataFrame, and baseline entries quote messages. Also, the daily dependency update relocks every package withuv lock --upgrade, so the locked Pyright versions move whenever Pyright releases; pinning keeps dependency updates from changing type-check results, as the ruff pin does. 1.1.414 is the newest release and already the stage12-persistence lock's version. Error counts were the same under every version, and 1.1.414 reported exactly the same errors as each package's locked version except for that executor message. The script ignoresPYRIGHT_PYTHON_FORCE_VERSIONand fails if Pyright reports any other version. Each package'sbuildextra still installs Pyright for editors.The span scope type. policyengine-observability annotates its scope managers'
__exit__as returningboolalthough it always returnsFalse. Pyright reads aboolresult as "may suppress the exception", so it reports every variable assigned insidewith runtime.span(...)as possibly unbound afterwards. This causes the entry's two errors, and most of the gateway's 66 and the executor's 60 possibly-unbound errors. The newpolicyengine_simulation_observability.scopes.span(runtime, name, attributes=...)returns the runtime's own scope unchanged, typed asAbstractContextManager[object], whose exit type defaults tobool | None(the typecontextlib.contextmanagerscopes have, which type checkers don't treat as suppressing). The executor's Stage 12 code already types its span wrappers this way (coordination.py). This PR uses it at the entry's one call site; the fix PRs use it in the gateway and executor. No runtime behaviour changes: the same object is returned. A precise annotation upstream (-> Literal[False]) would be the more direct fix, in the policyengine-observability repository.apis-integ is included. Generating its client takes about 8 seconds once the entry's
buildenvironment is installed, the same steptest-integrationruns. Its code has nosrcdirectory, so its[tool.pyright] includeispolicyengine_api_full_integandtests. Its 4 errors are the integration tests passingClient | AuthenticatedClientto generated endpoints typed as needingAuthenticatedClient; a PR above this one resolves them.Each package declares what Pyright checks. Every
pyproject.tomlnow has a[tool.pyright] include. Without one, Pyright checks the whole package directory, tests included: the contract library reported 32 errors in 25 files instead of none in its 14 source files. The rules guard now fails when a package a Pyright rule lists has noinclude, or is missing from the CI job's matrix.Changes to the rules tool
pyrightenforcer now listsprojects(package directories) instead of oneconfigfile, since each package configures Pyright in its ownpyproject.toml. No existing rule used the old field.main, and the renderer collapses the block above 60 (AGENTS_COLLAPSE_AT); this PR adds 5.make checkmake checkgenerates the API client, runs ruff, thenpython -m scripts.rules pyright --sync. It runs every check and fails at the end if any failed; before, a ruff failure counted only in the last directory.Its ruff step, and
make format's, used to runuv run rufffrom the repository root, which runs whatever ruff is onPATH. With 0.16.8 there,make checkreported 154CPY001findings that CI's ruff doesn't select. The Makefile now reads the version from the lint job'suv tool install ruff==<version>step inpr.yml, so the two can't drift, and runsuv tool run ruff@<version>. It stops with a message if it can't read a version.Commits (red, then green)
tests/test_scopes.pyfailed withImportError: cannot import name 'scopes'; Pyright reported the entry's 2 errors. Green: observability library 35 passed; entry Pyright 0 errors; entry unit tests 652 passed.unknown key 'projects' in enforcer (pyright), and the old description). Green: 120 rules-tool tests passed.No module named 'scripts.rules.pyright'. Green: 29 tests passed, using a fakeuvonPATH; a run on the real entry reported version 1.1.414 and 0 errors withPYRIGHT_PYTHON_FORCE_VERSION=latestset.test_pr_workflow_is_gatedfailed withci-complete does not need pyright; the rules guard reported thepyrightjob as enforcing no registered rule; the three ratchet proofs failed without fixture baselines. Green:test_ci_complete_gate.py, 10 passed; quality guards andpython -m scripts.rules guard --base-ref origin/mainpassed;python -m scripts.rules pyrightpassed for all eight packages.--synctests failed withunrecognized arguments: --sync. Green: 157 rules-tool tests passed;make checkexited 0 with ruff 0.15.22 first onPATH.cannot import name 'check_coverage', then the guard runner reportedquality guard 'pyright-coverage' ... has no rule. Green: tests and guards pass; a temporaryprojects/probe-package/pyproject.tomlmade the guard fail, naming the package.--write-baselinerefuses an error an existing baseline doesn't list, but records every error when the file doesn't exist yet (adopting a rule). They also say the guard's count comparison is what stops growth. Two new tests pin a run with both a new error and a stale entry, and--syncwith--write-baseline; both pass on the existing code.PATH,make checkfailed with 154CPY001findings. Green: with the samePATH,make checkexits 0 running ruff 0.15.22. A missing version stopsmakewith a message (checked withmake require-ruff-version RUFF_VERSION=).status weakened from enforced to ratchetedunless the rule has a[rule.downgrade] reason.pyrightconfig.json(which Pyright reads instead of[tool.pyright];typeCheckingMode: offthere gave 0 errors on a file with one), got no violation. Green: both are reported.pyright --write-baselinesran a plain check;--list-packagesignored packages,--syncand--write-baseline;--write-baselinewith no ratcheted rule printed only "pyright: passed". Green: usage errors for the first two, and a message for the third.make -n checkshowedruff checkper src directory and no format check, unlike the lint job; and the versionsedreturned both versions from a workflow with twouv tool install ruff==lines. Green:make checkrunsruff format --check projects/*/src libs/*/srcandruff check .with the first pinned version, and exits 0.ParentConfigurationTestbuildslibs/alphawith one return-type error, puts apyrightconfig.jsonwithtypeCheckingMode: offin the repository root and inlibs/, and runs the real check with the pinned Pyright; it got no errors. Green: it reports thereportReturnTypeerror inlibs/alpha/src/alpha.py. Thequality-guardsjob now installs uv so this test runs in CI. Elsewhere it is skipped when uv is missing; in CI (GITHUB_ACTIONSset) a missing uv fails it.libs/alphaby one rule and as./libs/alphaby another got no violation. Green:normalized_packageinscripts/rules/model.py(os.path.normpath, with/separators), used by the duplicate-package check and by the Pyright check, reports it.Proof fixtures
Under
rules/fixtures/<rule id>/{bad,good}/, recorded with Pyright 1.1.414 onmainat ceb30e1 (the checkout path replaced by/repository). The enforced rule's bad fixture is the entry's real pre-fix report:invocation_idpossibly unbound atstage12_backend.py:550and:563. Each ratchet's bad fixture has a baseline that omits one real error and lists one that no longer occurs. The proof tests are inscripts/rules/tests/test_pyright.pyand run in thequality-guardsjob.Environment variables
The
pyrightjob has no secrets and reads:PACKAGEstrategy.matrix.packageenv:(${{ matrix.package }}), so no expression is interpolated into shellPYRIGHT_PYTHON_FORCE_VERSIONscripts/rules/pyright.pyremoves it from Pyright's environment if presentPYRIGHT_PYTHON_IGNORE_WARNINGSscripts/rules/pyright.pyThe
quality-guardsjob now installs uv withastral-sh/setup-uv@v8.1.0(no inputs) and still has no secrets.ParentConfigurationTestreadsGITHUB_ACTIONS, which GitHub Actions sets in every step, to fail instead of skipping when uv is missing. Itsuv tool run --from pyright==1.1.414downloads the pyright package from PyPI and Pyright from npm, using Node from theubuntu-latestimage; neither download needs credentials../scripts/generate-clients.sh(apis-integ only) builds the entry app, whoseSettings.from_env()readsAPP_ENVIRONMENT,SIMULATION_ENTRYPOINT_AUTH_*,OLD_GATEWAY_*,STAGE12_*andK_REVISION. None is set in this job; each has a default, and thetest-integrationand dependency-update jobs already run the same script without them.uv,pythonand Node (used by the pyright package) come fromastral-sh/setup-uv,actions/setup-pythonand theubuntu-latestimage; the pyright package downloads Pyright from npm on first use.Self-review findings and how they were resolved
review-coderan twice on this PR, the second time from a separate agent over the whole stack. Neither run found a bug. Code-quality findings:--write-baselinewith no package argument failed for every enforced package, although the ratcheted baselines were written. Fixed (commit 9).--write-baselinenever adds an error, but a missing baseline is recorded in full. Corrected (commit 10).pyrightconfig.jsonis now rejected (commit 13).ignore, a lowertypeCheckingModeor rule severities in[tool.pyright]are documented as review-only (commit 16). Judging those settings would mean encoding a policy for every Pyright option.make check'ssedcould read two versions, and its ruff scope differed from CI's. Fixed (commit 15).A later
review-codepass over the whole stack found two more problems in this PR. Both are fixed:pyrightconfig.jsonin a parent directory still replaced a package's[tool.pyright]settings, because the guard looked for one only inside the package and Pyright searches parent directories too. Fixed by running Pyright with--project <package>, proven with the real Pyright (commit 17)../libs/alphaandlibs/alphapassed it as two packages. Fixed (commit 18).A
review-codepass over commits 17 and 18 found no bugs. Minor findings, not changed:ParentConfigurationTestproves that a parentpyrightconfig.jsonis ignored, but not that the package's own[tool.pyright]is read: its only setting,include = ["src"], gives the same result as Pyright's defaults.Suppressions added
None.
Real bugs found
None.
Testing
python -m unittest discover -s scripts/rules/tests -t .: 170 tests pass, includingParentConfigurationTestwith the real Pyright 1.1.414.python scripts/run_quality_guards.py: rules-registry, ci-scripts and pyright-coverage pass;python -m scripts.rules guard --base-ref origin/mainpasses.python -m scripts.rules pyright: all eight packages pass.uvx ruff@0.15.22 format --check projects/*/src libs/*/srcanduvx ruff@0.15.22 check .: clean.make check: exits 0.test_ci_complete_gate.pyandtest_modal_scripts.py: 73 passed.🤖 Generated with Claude Code