Quality and release¶
How the project keeps its promises while it changes: the guardrails that run while code is written, the checks that run on every push, and the pipelines that publish a release and keep the project current afterwards.
Guardrails while working¶
The repository is developed with an AI coding assistant under a set of hooks that turn the hard rules into blocks. They are part of the repository (.claude/), so a reviewer can read exactly what was enforced.
flowchart TD
act["an edit, a shell command or a commit"]
pre["PreToolUse: guard.py"]
blk["blocked: message says why;<br/>write an ADR or ask the researcher"]
ok["allowed"]
post["PostToolUse: format.py<br/>(ruff on the edited file)"]
stop["Stop: stop_gate.py<br/>runs the fast tests"]
red["red tests: keep working"]
done["session may end"]
perm["permissions: deny reading secrets,<br/>deny editing data/ and docs/source/"]
act --> perm --> pre
pre -->|"locked file, force push, git add data/ or results/,<br/>recursive delete, unlisted download host"| blk
pre --> ok --> post --> stop
stop -->|"fail"| red
stop -->|"pass"| done
Figure 11. The development loop. The guard is a stdlib-only Python script that works from Git Bash, PowerShell and Linux CI; it fails closed when it cannot parse a command that touches a protected operation.
Layer |
What it enforces |
Test |
|---|---|---|
|
no edit to the pre-registration after its tag, tagged addenda or committed golden files; no force push; no |
|
|
ruff formats each edited file |
the format check in CI |
|
the fast tests must pass before a session can finish |
the fast suite |
|
secrets cannot be read; |
settings validated against the published schema (ADR-0001) |
|
the pre-registration equals its tag, no golden file changed, no |
runs on every stop and in CI |
Reviewer agents |
|
their findings are recorded under |
A block is a rule, not a bug: when the guard stops a change the response is an ADR or a question to the researcher, never a workaround.
Tests¶
Suite |
What it is for |
|---|---|
fast ( |
unit, property and integration tests on a synthetic fixture; the default and the Stop gate |
slow ( |
whole-grid synthetic runs, the table builders for E3 and E4, the mutation kill matrix, the distribution build |
golden files |
locked expected outputs, e.g. the smoke Table 1; the guard prevents edits |
mutation matrix |
deliberate breakages of the slicer; each must be caught (W-07) |
guardrail demo |
every guard scenario against a throw-away repository |
spy test |
held-back labels are reachable only through the queue (see simulator) |
clean-clone check |
a fresh clone installs from the lock file, passes the suites and reproduces Table 1 E-67 |
Per-module coverage is in reports/coverage.md.
Continuous integration and release¶
flowchart TB
tag["signed tag vX.Y.Z pushed"] --> chk["check_release.py<br/>version = tag = CHANGELOG = CITATION"]
chk --> bld["uv build, twine check --strict,<br/>no data in the files"]
bld --> smk["install the wheel in a fresh venv;<br/>driftwatch smoke on Linux and Windows"]
smk --> gate1{{"GO: environment pypi<br/>researcher approves"}}
gate1 --> pypi["upload to PyPI<br/>(Trusted Publishing, OIDC)"]
pypi --> draft["draft GitHub Release<br/>(notes from CHANGELOG, files attached)"]
draft --> gate2{{"GO: researcher publishes<br/>the draft release"}}
gate2 --> zen["Zenodo integration<br/>mints the DOI"]
Figure 12. The release path. Two irreversible steps, two separate decisions: approving the pypi environment uploads the files, and publishing the draft release is what makes Zenodo mint a DOI. No long-lived secret exists: publishing uses short-lived OpenID Connect tokens.
The other pipelines run on their own triggers:
Trigger |
Workflow |
What it does |
|---|---|---|
push or pull request |
|
lint, format check, fast and slow tests, guardrail demonstration, smoke reproduction on Linux; fast tests, guardrails and smoke on Windows |
push or pull request; weekly |
|
strict Sphinx build and external link check |
merge to |
|
deploys the build to |
tag |
|
deploys the build to |
manual |
|
rehearsal: the same build and wheel smoke, then upload to TestPyPI |
manual |
|
a fresh runner installs from TestPyPI or PyPI with no checkout and runs the smoke; optionally runs the full Table 1 from a clone |
monthly |
|
opens an issue listing new candidate papers |
Design choices in the release path:
The wheel must work alone. The smoke job installs the built wheel into an empty virtual environment on Linux and Windows, in a folder that is not a checkout, and runs
driftwatch smoke. A package that only works from a clone cannot be published.Nothing but code ships.
scripts/check_release.pyopens the wheel and the source archive and fails on any dataset, result, secret or course document, and on a wheel over one megabyte. The synthetic fixture travels only in the source archive.Irreversible steps are gated. The PyPI upload and the DOI cannot be undone, so each waits for an explicit decision; rehearsal on TestPyPI comes first (RELEASING).
Actions are pinned to commit hashes, with the version in a comment, so a moved tag cannot change what runs in the privileged job.
Maintenance¶
Recurring work is documented as prompts, commands and expected outputs in MAINTENANCE: a monthly arXiv audit (a script finds candidates, a skill triages them into the related-work addendum), a quarterly dependency refresh that must not move any table, a dataset-version watch, a release check and issue triage. The rule that binds all of them: a maintenance change never changes a reported number; if it would, it becomes a pre-registered change request instead.