Contributing¶
Thanks for considering a contribution. This page is the short version; the
normative documents in the repository are
ARCHITECTURE.md (what the contracts are) and AGENTS.md
(how to work in the repository).
Set up¶
Install the git hooks once:
Run the checks¶
Fast iteration uses the checked-in environment:
Repository-level gates run through tox, exactly as CI does:
uv run --no-sync tox run -e py314 # tests on one interpreter
uv run --no-sync tox run -e pre-commit # lint, typing, imports, hygiene
uv run --no-sync tox run -e build # wheel and sdist validation
uv run --no-sync tox run -e docs # strict documentation build
The full matrix is py311, py312, py313, py314; CI additionally tests the
built wheel and sdist on Linux, macOS and Windows.
Two convenience targets exist:
For a public API change, check compatibility against the intended baseline (the
newest v* tag is used when API_BASELINE is omitted):
Work on the documentation¶
uv run --no-sync tox run -e docs-serve # live reload on http://127.0.0.1:8000
uv run --no-sync tox run -e docs # strict build, as CI runs it
The site is built with MkDocs and Material; the API reference is generated from docstrings by mkdocstrings. Because the build is strict, a broken internal link or an unresolvable reference fails CI.
Prose pages live in docs/. docs/architecture.md and docs/changelog.md
include the repository files rather than duplicating them, so edit
ARCHITECTURE.md and the changelog fragments instead.
The internal engineering notes — docs/README.md, docs/adr/ and
docs/use_case/ — are excluded from the published site via exclude_docs in
mkdocs.yml. They stay in the repository for contributors; remove them from that
list if you decide to publish them.
Architectural guardrails¶
A change is much easier to accept when it respects these:
- Public factories create immutable nodes; fluent methods return new ones.
- Semantic models hold intent only — no coordinates, resolved layout, caches or engine-native values.
- Dependency direction is
api → core/_internal,_internal → core,testing → core/_internal.corenever importsapi,_internal, testing, OpenPyXL or XlsxWriter;_internalnever importsapi. - Column
id,sourceandtitlestay distinct. - Coercion and structural validation never consume rows;
REITERABLE/ONE_SHOT/UNKNOWNbehaviour is preserved and hidden extra passes are rejected. - Errors are stable
CaxtonErrorsubclasses with semantic context, chained to the original cause. - Treat deferred capabilities as absent — a name in a design note does not reserve a public API.
These are enforced in part by import-linter contracts and by the Griffe API
compatibility check, so breaking one usually fails the pre-commit tox env.
Workflow¶
- Inspect the affected public contract and tests; preserve unrelated changes.
- Add or update a focused test at the narrowest meaningful boundary — semantic model, layout, renderer or artifact.
- Make the smallest coherent change.
- Run focused tests, then checks proportional to the change.
- Review the final diff for accidental API exposure, eager data consumption, engine leakage, generated artifacts and stale documentation.
Changelog and commits¶
Every user-visible change needs one Towncrier fragment:
Types are breaking, feature, bugfix, doc, generation and ci. Edit
fragments, never the generated CHANGELOG.md.
PR titles and all non-merge commits follow Conventional Commits:
CI validates both the commit format and the presence of a fragment.