Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a3314bc
refactor(src): dissolve basic.py into calibration + statistics
cailmdaley Jun 20, 2026
1ff913c
felt: reserved-sasha — document Sasha's hands-off zones
cailmdaley Jun 20, 2026
322a589
test(statistics): pin the extracted helpers; drop dead code in calibr…
cailmdaley Jun 20, 2026
e8ad1d0
refactor(src): delete dead info.py, fix cat.py version import
cailmdaley Jun 20, 2026
7a5e488
refactor(src): make package __all__ honest
cailmdaley Jun 20, 2026
9a41f4a
refactor(cosmo_val): hoist b_modes imports to module top level
cailmdaley Jun 20, 2026
8da5d8b
refactor(run_joint_cat): drop shadowed confusion_matrix def
cailmdaley Jun 20, 2026
7a531f1
refactor: remove dead cluster/convergence-map helpers
cailmdaley Jun 20, 2026
3bea490
refactor(src): rename util -> format; drop dead transform_nan import
cailmdaley Jun 20, 2026
bd41a13
refactor(src): move SquareRootScale to plots, re-export from rho_tau
cailmdaley Jun 20, 2026
0b1d2f5
fix(papers/harmonic): repoint SquareRootScale off dead utils_cosmo_val
cailmdaley Jun 20, 2026
02c4050
refactor(src): rename run_joint_cat -> catalog_builders
cailmdaley Jun 20, 2026
575c3f4
Extract masks.py from catalog_builders.py
cailmdaley Jun 20, 2026
9bfee81
Rename cat.py -> catalog.py: catalogue data layer
cailmdaley Jun 20, 2026
34740e1
style(ruff): repo-wide ruff format pass (mechanical, no semantic change)
cailmdaley Jun 20, 2026
f0cd70f
chore(ruff): wire ruff (pre-commit + CI) + region-aware lint policy
cailmdaley Jun 20, 2026
ef98dba
fix(ruff): region-aware lint fixes (behavior-preserving, adversariall…
cailmdaley Jun 20, 2026
98ab9c3
chore(ruff): scope CI lint to library; broaden E402 ignores for per-p…
cailmdaley Jun 20, 2026
4f9f099
chore(ruff): pivot lint gate to warn-locally / account-on-develop
cailmdaley Jun 30, 2026
c77fa63
chore(ruff): clear lint baseline to the 5 genuine undefined names
cailmdaley Jun 30, 2026
bb4583a
Merge develop into chore/ruff
cailmdaley Jun 30, 2026
21c7d96
Fix ruff hooks. Removed consistency not book and moved it to my perso…
sachaguer Jun 30, 2026
e2399b7
chore(ruff): PR feedback as a PR comment, issue only for develop pushes
cailmdaley Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Commits to skip when assigning blame — mechanical, repo-wide reformats that
# touch many lines without changing behaviour. Listing them here keeps
# `git blame` pointing at the commit that actually wrote each line.
#
# Enable locally with:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# (GitHub's blame view honours this file automatically.)

# style(ruff): repo-wide ruff format pass (mechanical, no semantic change)
34740e11893fb90766d2139ebbb9d10b8b5d4091
244 changes: 244 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
name: Lint

# Lint gate for `develop` — warn locally, account here.
#
# The model (settled with Cail + Sacha, 2026-06-23/30):
# * Locally, ruff auto-applies safe fixes and only WARNS on the rest (see
# .pre-commit-config.yaml) — you commit freely.
# * Getting into `develop` is gated. This job runs the FULL ruff policy
# (`ruff check .` + `ruff format --check .`, region-aware per pyproject.toml)
# and on any failure it (1) fails the job RED so the check blocks the merge,
# and (2) tells the author what to fix — in the surface that fits the event:
# - On a PR → posts/updates a COMMENT on the PR itself, with the full
# violation list (and ruff annotations in the run's Checks view). The
# author sees it where they already are; no disconnected issue. The
# comment turns green when ruff passes.
# - On a direct push to develop → there's no PR to comment on, so it opens
# (or updates) ONE lint-debt issue for the committer, @-mentioning and
# assigning them. Auto-closes when their next push is clean.
# * If ruff itself can't run (network, bad version), the job goes red but says
# nothing — that's infra, not the committer's lint debt.
#
# Triggers: pushes to `develop` and PRs targeting `develop` only — other
# branches stay quiet (too noisy otherwise). `workflow_dispatch` is for manual
# testing of the gate itself.
#
# Why `pull_request_target` for PRs: commenting on a PR needs a write token, and
# PRs from forks (e.g. sachaguer/) get a read-only token under the plain
# `pull_request` event. `pull_request_target` runs this workflow from the BASE
# branch (so the workflow definition is trusted) with a write token, while we
# check out the PR head ONLY to lint it. Two hardening measures make running
# tooling over untrusted PR code safe here: ruff is a static analyzer (it parses
# files, never imports/executes them), and `uvx --no-config` makes uv ignore any
# `uv.toml`/`[tool.uv]` in the PR tree, so a malicious PR can't redirect ruff's
# download to a trojaned index. The checkout also drops its git credentials.
# (A PR can still edit `[tool.ruff]` to weaken its own policy, but that's visible
# in the diff and reviewed like any other change.)

on:
push:
branches: [develop]
pull_request_target:
branches: [develop]
workflow_dispatch:

# One lint run per branch / PR; newer pushes cancel older in-flight runs.
concurrency:
group: lint-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read
issues: write # develop-push lint-debt issue
pull-requests: write # PR lint comment

jobs:
lint:
name: Ruff (check + format)
runs-on: ubuntu-latest

steps:
- name: Checkout (PR head on pull_request_target, else the pushed ref)
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@v3

# Run the checks WITHOUT failing the step — we post feedback before turning
# the job red. `ruff@<pin>` matches the pre-commit version; `--no-config`
# neutralizes any uv config in the (untrusted) PR tree.
# ruff exit codes: 0 = clean, 1 = violations, >=2 = ruff/uvx error.
- name: Run ruff
id: ruff
shell: bash
run: |
set +e
# Inline annotations on the diff (ruff's GitHub format → step log).
uvx --no-config ruff@0.15.18 check . --output-format=github
# Readable output for the PR comment / issue body, plus exit codes.
uvx --no-config ruff@0.15.18 check . --output-format=concise > check.txt 2>&1; check_rc=$?
uvx --no-config ruff@0.15.18 format --check . > format.txt 2>&1; fmt_rc=$?

{
echo "### \`ruff check .\`"
if [ "$check_rc" -eq 0 ]; then echo; echo "✅ clean"; else echo; echo '```'; cat check.txt; echo '```'; fi
echo
echo "### \`ruff format --check .\`"
if [ "$fmt_rc" -eq 0 ]; then echo; echo "✅ clean"; else echo; echo '```'; cat format.txt; echo '```'; fi
} > report.md

# A tooling error (rc >= 2) is not lint debt — block, but say nothing.
if [ "$check_rc" -ge 2 ] || [ "$fmt_rc" -ge 2 ]; then
echo "tool_error=true" >> "$GITHUB_OUTPUT"
else
echo "tool_error=false" >> "$GITHUB_OUTPUT"
fi
if [ "$check_rc" -eq 0 ] && [ "$fmt_rc" -eq 0 ]; then
echo "passed=true" >> "$GITHUB_OUTPUT"
else
echo "passed=false" >> "$GITHUB_OUTPUT"
fi

# Feedback is a side effect — never let it red a clean run.
- name: Tell the author (PR comment) or record it (develop-push issue)
if: steps.ruff.outputs.tool_error == 'false'
continue-on-error: true
uses: actions/github-script@v7
env:
PASSED: ${{ steps.ruff.outputs.passed }}
with:
script: |
const fs = require('fs');
const passed = process.env.PASSED === 'true';
const { owner, repo } = context.repo;
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const report = passed ? '' : fs.readFileSync('report.md', 'utf8');

// ---- PR: speak on the PR itself (comment, auto-updating) ----------
if (context.eventName === 'pull_request_target') {
const pr = context.payload.pull_request;
const author = pr.user.login;
const MARKER = '<!-- ruff-lint-gate -->';
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100,
});
const mine = comments.find(c => c.body && c.body.includes(MARKER));

if (passed) {
// Only update an existing comment to green; don't post on a PR
// that was never dirty.
if (mine) {
await github.rest.issues.updateComment({
owner, repo, comment_id: mine.id,
body: `✅ **ruff is clean** — nothing to fix here. ${MARKER}`,
});
}
core.info('PR clean.');
return;
}

const body = [
`### 🔴 ruff found lint / format issues`,
``,
`@${author} — these block the merge into \`develop\`. Full list below (also surfaced as annotations in the CI run):`,
``,
report,
``,
`---`,
`[CI run](${runUrl}) · _Updates on every push and turns green when ruff passes — nothing else to do._`,
MARKER,
].join('\n');

if (mine) {
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
core.info(`Updated PR comment ${mine.id}.`);
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
core.info('Posted PR comment.');
}
return;
}

// ---- Direct push to develop: per-committer issue (no PR exists) ---
const login = context.actor;
const where = `push to \`${context.ref.replace('refs/heads/', '')}\` (\`${context.sha.slice(0, 7)}\`)`;
const title = `Lint debt: @${login}`;
const LABEL = 'lint-debt';

try {
await github.rest.issues.getLabel({ owner, repo, name: LABEL });
} catch {
try {
await github.rest.issues.createLabel({
owner, repo, name: LABEL, color: 'd93f0b',
description: 'Auto-filed lint failures from the develop gate',
});
} catch (e) { core.info(`label create skipped: ${e.message}`); }
}

const open = await github.paginate(github.rest.issues.listForRepo, {
owner, repo, state: 'open', labels: LABEL, per_page: 100,
});
const existing = open.find(i => i.title === title && !i.pull_request);

if (passed) {
if (existing) {
await github.rest.issues.createComment({
owner, repo, issue_number: existing.number,
body: `✅ Lint is clean as of ${where}. Closing — thanks!`,
});
await github.rest.issues.update({ owner, repo, issue_number: existing.number, state: 'closed' });
core.info(`Closed #${existing.number} (clean).`);
} else {
core.info('Clean, no open lint-debt issue to close.');
}
return;
}

const body = [
`@${login} — ruff flagged lint issues in **${where}** (pushed straight to \`develop\`).`,
``,
`This doesn't block local work (pre-commit only warns), but \`develop\` stays red until it's clean. Fix and push again — **this issue auto-closes when CI goes green.**`,
``,
report,
``,
`---`,
`[CI run](${runUrl}) · _Auto-filed by the lint gate; updated in place, closed when clean._`,
`<!-- lint-debt-for: ${login} -->`,
].join('\n');

let number;
if (existing) {
await github.rest.issues.update({ owner, repo, issue_number: existing.number, body });
await github.rest.issues.createComment({
owner, repo, issue_number: existing.number,
body: `🔴 Still failing as of ${where}. [run](${runUrl})`,
});
number = existing.number;
core.info(`Updated #${number}.`);
} else {
const created = await github.rest.issues.create({ owner, repo, title, body, labels: [LABEL] });
number = created.data.number;
core.info(`Opened #${number}.`);
}

try {
await github.rest.issues.addAssignees({ owner, repo, issue_number: number, assignees: [login] });
} catch (e) {
core.info(`assign skipped (${login} not assignable): ${e.message}`);
}

# Red → blocks the merge. `always()` so a hiccup in the feedback step above
# can't suppress the red on genuine lint debt; a tooling error also reds.
- name: Fail the job if the gate didn't pass
if: always() && steps.ruff.outputs.passed == 'false'
run: |
if [ "${{ steps.ruff.outputs.tool_error }}" = "true" ]; then
echo "::error::ruff could not run (network / version) — gate inconclusive, blocking."
else
echo "::error::ruff found lint/format issues — see the PR comment (or lint-debt issue)."
fi
exit 1
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ uv.lock
# mkdocs documentation
/site

# ruff
.ruff_cache/

# mypy
.mypy_cache/
.dmypy.json
Expand Down Expand Up @@ -192,3 +195,6 @@ papers/catalog/plots/*.pdf

# SLURM run logs from cosmo_val validation runs
papers/cosmo_val/logs/

# Ignore scratch notebooks
scratch/*/*.ipynb
45 changes: 42 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,46 @@
# Bloat guards: notebook outputs are stripped on commit, and oversized files
# are rejected. Activate once per clone with `pre-commit install`
# (see CONTRIBUTING.md).
# Pre-commit hooks. The posture: auto-apply what ruff can fix safely, WARN
# (never block) on what it can't, and hard-block only on repo bloat.
#
# * ruff auto-applies everything that's automatically applicable — `ruff
# format` (layout) and `ruff check --fix`'s SAFE fixes (import sorting,
# unused imports, …). When it rewrites a staged file the commit stops once
# so you re-`git add` the result; that's pre-commit's modify→re-stage step,
# not a lint failure.
# * What ruff WON'T safely fix — undefined names (F821), unused variables
# (F841), and other judgement calls — is printed as a WARNING and never
# blocks the commit. Those get enforced on the `develop` gate
# (.github/workflows/lint.yml), not at your keyboard.
# * Bloat guards (notebook outputs, oversized files) DO block — heavy content
# is expensive to undo once in history.
#
# ruff is pinned to the SAME version CI uses — keep them in lockstep.
# Activate once per clone with `pre-commit install` (see CONTRIBUTING.md).
repos:
# --- ruff: auto-apply the safe stuff, warn on the rest, never hard-block ----
- repo: local
hooks:
# Linter: applies ruff's SAFE autofixes (and re-stages like a formatter).
# Everything it won't safely fix is printed as a warning; `|| true` keeps
# those from ever blocking the commit. Runs before the formatter so layout
# gets the final word.
- id: ruff-fix
name: ruff check --fix (safe fixes auto-applied; the rest only warn)
entry: bash -c 'ruff check --fix "$@" || true' --
language: python
additional_dependencies: ["ruff==0.15.18"]
types_or: [python, pyi]
require_serial: true
verbose: true
# Formatter: auto-applies. If it reformats your staged files the commit
# stops once so you re-`git add` — not a lint failure, just a re-stage.
- id: ruff-format
name: ruff format (auto-applies)
entry: ruff format
language: python
additional_dependencies: ["ruff==0.15.18"]
types_or: [python, pyi]

# --- Bloat guards: BLOCKING (intentional) ----------------------------------
- repo: https://github.com/kynan/nbstripout
rev: 0.8.1
hooks:
Expand Down
41 changes: 32 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,47 @@ Tests live in `src/sp_validation/tests/`. The default options (configured in
inside the freshly-built container image *before* publishing it, so a failing
test blocks the image push.

## Code style
## Code style and the lint gate

We use [`ruff`](https://docs.astral.sh/ruff/) for linting and import sorting,
with a line length of 88:
We use [`ruff`](https://docs.astral.sh/ruff/) for both formatting and linting
(line length 88). The policy lives in `pyproject.toml` and is region-aware:
`src/sp_validation/` is strict, while the analysis/workflow/script trees waive a
few intentional patterns (`sys.path` edits before imports, star-imports).

```bash
ruff check # report issues
ruff check # report lint issues
ruff check --fix # auto-fix what it can
ruff format # format the tree
```

Please run `ruff check` before opening a pull request.
**Local hooks auto-fix the safe stuff, warn on the rest.** The
[`pre-commit`](https://pre-commit.com/) hooks auto-apply everything ruff can fix
safely — `ruff format` plus `ruff check --fix`'s safe fixes (import sorting,
unused imports). When that rewrites a staged file the commit stops *once* so you
`git add` the result and commit again. Anything ruff *won't* safely fix —
undefined names, unused variables, other judgement calls — is printed as a
**warning** and never blocks the commit. Judgement-call lint stays out of your
way locally; the gate below is where it's enforced.

**`develop` is the gate.** On every push to `develop` and every PR into it, CI
runs the full ruff policy. If it fails, the check goes **red and blocks the
merge**, and the bot tells you what to fix where you already are:

- **On a PR** → it posts (and keeps updating) a **comment on the PR** listing the
violations (also surfaced as annotations in the CI run). Push a fix and the
comment turns green.
- **On a direct push to `develop`** (no PR) → it opens (or updates) a single
**lint-debt issue assigned to you**, which auto-closes when CI is green.

So: warn while you work, clean before it lands.

## Commit hygiene (notebooks & large files)

The repository's history is heavy from committed notebook outputs; two
[`pre-commit`](https://pre-commit.com/) hooks guard against more of it:
`nbstripout` (strips notebook outputs on commit) and a large-file check
(2 MB). Activate them once per clone:
The repository's history is heavy from committed notebook outputs. Alongside the
warn-only ruff hooks above, two **blocking** `pre-commit` hooks guard against
more of it: `nbstripout` (strips notebook outputs on commit) and a large-file
check (2 MB). These block because heavy content is expensive to undo once it is
in history. Activate everything once per clone:

```bash
pre-commit install
Expand Down
Loading
Loading