Contributing¶
Conventions for human contributors and AI coding agents working in this repository. The docs site renders this file as its contributing guide.
Project¶
CatSmoothing smooths Shapely geometries with Catmull-Rom splines and computes tangent
angles at vertices. The spline math is a Rust crate in src/, exposed to Python through
PyO3 bindings in src/python.rs as catsmoothing._catsmoothing; catsmoothing/api.py
wraps them with the public functions smooth_linestrings, smooth_polygon,
linestrings_tangent_angles, and the CatmullRom class.
Design record¶
docs/design.md holds every design decision (D-), the lessons from past mistakes
(L-), and the open questions (Q-), each with its rationale.
- Before designing a component or proposing an alternative to a rule below, search it. Do not reopen an accepted decision without evidence it does not already account for.
- When a change makes or reverses a decision, answers a question, or exposes a mistake
worth not repeating, update
docs/design.mdin the same change, following the rules at its top.
Design rules¶
One-line summaries of the decisions any code change can break. The IDs point into
docs/design.md.
- Smoothing functions fit centripetal splines (
alpha=0.5) (D-02). - Resampled points are equally spaced by arc length, and
distancescaps the gap in coordinate units (D-04, D-05). - Caller input errors return
SplineErrorin Rust and raiseValueErrorin Python; Rust never panics on input a caller controls (D-07). - Repeated consecutive vertices are dropped when the grid is generated, and vertex arrays must have two columns (D-08, D-09).
- Gaussian pre-smoothing point-reflects open ends and wraps rings; Taubin and Gaussian smoothing do not combine (D-10, D-11).
- Continuity splits keep every vertex and its tangent (D-12).
- The last point of a curve is its last vertex, so rings close exactly (D-13).
- Batch functions release the GIL, pass coordinates in one stacked array, and use Rayon's global pool, running serially when it has one thread (D-14).
- A public argument is removed only after a release in which passing it raises a
DeprecationWarning(D-06). - Rust follows the Google Rust style guide: the clippy lints in
Cargo.tomlplus.agents/skills/rust-style/SKILL.md(D-16).
Commands¶
Install pixi and run everything through it; each environment is
created on first use. Never call bare pytest, python, or pyright. Rust checks run
through cargo.
| Task | Command |
|---|---|
| Lint (pre-commit) | pixi r lint |
| Dependency audit (Python and Rust) | pixi r audit |
| Type check | pixi r typecheck |
| Test, Python 3.13 | pixi r -e test313 test |
| Test, Python 3.15 | pixi r -e test315 test |
| Coverage report / HTML | pixi r -e test315 report / html |
| Rust format check | cargo fmt --check |
| Rust lint | cargo clippy --features python --all-targets -- -D warnings |
| Rust tests | cargo test |
| Benchmarks | pixi r benchmark |
| Run Python | pixi r -e dev python ... |
| Spell check (writes fixes) | pixi r spell |
| Changelog preview | pixi r changelog |
| Docs, strict build | pixi r docs |
| Docs, live preview | pixi r docs-serve |
| Notebooks, sync and run | pixi r nb-run |
| Notebooks, pair a new one | pixi r nb-pair |
Environments: dev, test313, test315, benchmark, typecheck, lint, docs.
A change is done when lint, typecheck, and the tests on both test313 and test315 pass,
plus the three Rust checks when it touches src/, tests/, or Cargo.toml, and
pixi r docs when it touches docs/, README.md, CHANGELOG.md, or zensical.toml.
.github/workflows/test.yml runs the tests on Linux, macOS, and Windows with the Rust,
typecheck, lint, and audit jobs; docs.yml builds the site on every PR and deploys
main to GitHub Pages; release.yml builds the wheels on every PR; and codspeed.yml
runs the benchmarks.
Code style¶
- Rust:
cargo fmtdefaults and the[lints.clippy]table inCargo.toml. Before writing or reviewing Rust, read.agents/skills/rust-style/SKILL.md, the Google style rules no lint checks. - Python >= 3.13, with
from __future__ import annotationsin every module. - ruff with 100-character lines and
select = ["ALL"]; pyright in strict mode oncatsmoothing/. Both are configured inpyproject.toml. - NumPy-convention docstrings.
- Rust in
src/with integration tests intests/; Python incatsmoothing/with tests incatsmoothing/tests/.
Gotchas¶
test,typecheck, andbenchmarkfirst runpreinstall(maturin develop --uv), which builds the optimizedreleaseprofile that[tool.maturin]names (L-03).- The
testtask appends coverage across runs. Delete.coveragebefore trusting a number. - pyright checks only
catsmoothing/and skips its tests. pixi r lintruns vulture at 80% confidence, which catches only unused imports, unused arguments, and unreachable code. pre-commit's--all-filessees only tracked files, sopixi r lintthen runsscripts/lint_untracked.pyon untracked, non-ignored files.- mdformat rewraps markdown to 88 columns. Keep
mdformat-frontmatterin its hook, or it mangles the frontmatter of skills and rules (L-02). - Each example in
docs/examples/is a notebook paired with a jupytextpy:percentscript; edit either and commit both, the notebook with its outputs.pixi r nb-runsyncs them and executes the notebooks against a release build. ssort skips this folder, since it can move code between cells (L-04). pixi r docsconverts each notebook into a Markdown page beside it without running it, since zensical renders no notebooks. The generated.mdfiles and*_files/folders are git-ignored, apart fromindex.md. The site also publishes the.ipynband.pyfiles, so a link toother.ipynbdownloads the notebook; linkother.mdto reach its page.- Each notebook gets a card in
docs/examples/index.mdwith a thumbnail indocs/examples/images/. The strict build fails if a card links to a notebook that does not exist. docs/index.md,docs/changelog.md,docs/contributing.md, anddocs/license.mdonly includeREADME.md,CHANGELOG.md, this file, andLICENSE. Edit the sources.docs/examples/whats_new.ipynbreadsdata/v041.npz. Regenerate it only with the command indocs/examples/data/make_v041_data.py, then rerun the notebook (D-21).- Links in the included files must be absolute URLs: a relative link resolves against the site, where its target does not exist, and the strict build does not catch it.
release.ymlcomes frommaturin generate-ci github. After regenerating it, restore thetestandrelease-notesjobs,release-notesin the release job'sneeds, pushes tomainonly, trusted publishing without a token, theinterpreterentries of the 32-bit Linux targets, the commit-SHA pins, andpersist-credentials: falseon each checkout (D-23).- The version lives in
pyproject.tomlandCargo.toml; bump both in the release PR. - Skills live in
.agents/skills/..claude/skillsis a symlink to it for Claude Code, and.claude/rules/rust-style.mdpoints Claude Code to the Rust skill when it opens a.rsfile.CLAUDE.mdis git-ignored because Claude Code readsAGENTS.mdonly when noCLAUDE.mdexists (D-17). - The ruleset on
mainnames its required checks by job:Test (test313, <os>)andTest (test315, <os>)forubuntu-latest,macos-latest, andwindows-latest, plusRust,Typecheck, andLintfromtest.yml, andDocsfromdocs.yml. Renaming a job or a matrix entry leaves the old name unreported, so the renaming PR and every PR after it wait forever. Swap the name underSettings > Rules > Rulesetsas that PR merges.
Git¶
- Conventional Commits:
<type>[optional scope]: <description>, with type one offeat,fix,docs,style,refactor,perf,test,build,ci,chore. - Subject under 72 characters, imperative mood.
- Every change reaches
mainthrough a PR from its own branch, named<type>/<short-description>(fix/ring-closure). A ruleset blocks direct pushes, force pushes, and deletion ofmain(D-22). - PRs fill in
.github/PULL_REQUEST_TEMPLATE.mdand name the issue they finish withCloses #N. CHANGELOG.mdis hand-written through 0.5.0, whose release PR replaces the20XX-XX-XXplaceholder in its heading with the date. Later releases get their notes from git-cliff and the tag message; after each one,pixi r changelog-updateprepends them toCHANGELOG.md(D-19).pixi r release vX.Y.Z [notes-file]tags and pushes that tag, which publishes to PyPI. Run it only when a maintainer asks.
Writing style¶
These rules cover code comments, docstrings, commit messages, PR descriptions, issues, and docs.
- American spelling. Fix British spellings on sight.
- Active voice, present tense, concise.
- Use an em or en dash only when no comma, colon, semicolon, parentheses, or new sentence works.
- Be concrete. A number, name, or mechanism beats an abstraction: "handles bad input"
becomes "raises
ValueErrorfor vertex arrays without two columns". - No contrastive framing. Say what a thing is and stop, without a negated counterpart added for rhythm or emphasis: "the launch is checked rather than trusted" reads "the launch is checked". The ban covers "rather than", "not X but Y", "instead of", and "X, not Y" in any position. A real either/or, where both options are live and the reader needs to know which was picked, stays.
- Keep terms consistent. Once something has a name, reuse it, because a reader takes a
new word for a new thing. If the text calls it a
grid, later sentences do not switch to "knots" or "parameters". - Cut filler ("it's worth noting", "in order to", "when it comes to"), puffery ("plays a vital role", "stands as a testament"), and closing summaries that restate what came before.
- Banned words: delve, foster, empower, streamline, facilitate, cutting-edge, seamless, multifaceted, nuanced, intricate, meticulous, paramount, transformative, elevate, embark, supercharge.
Comments and commits:
- Comment only what the code does not say: a decision a reader would not guess, or behavior a reader would not see from reading it.
- Keep a comment or commit body to a line or two. Cut the rationale to the sentence that stops the next reader from undoing the change.
- Comments describe the code as it stands. History ("used to", "the old code", the bug that prompted the change) goes in the commit message.
Reviews and reports:
- A code review follows
REVIEW.md: what failure looks like here, what to skip, how to report, and how deep to go. - Say which claims you checked against the source and which you infer.
- Reserve prose for what changes the conclusion. Minor issues go in a numbered list at the end.