Contributing to Pounce
July 8, 2026 ยท View on GitHub
Short, recipe-oriented guide for contributors and agents working on pounce. For the why behind the design rules, read AGENTS.md.
Setup
Pounce targets free-threaded CPython (3.14t). One-time venv creation, then
editable install with dev dependencies:
make setup # uv venv --python 3.14t .venv
make install # uv sync --active --group dev --frozen
uv is the package manager. If make setup fails, uv python install 3.14t
first.
Feedback loops
make test # full pytest suite (~15s)
make lint # ruff + silent/raise diagnostics + import architecture
make format # ruff format . (writes)
make ty # ty check src/pounce/
Faster while iterating: uv run pytest tests/unit/test_<area>.py -x --timeout=10.
A change is not done until make lint and make ty are clean with no new
# type: ignore or # noqa: S110 suppressions.
make lint also runs the strict .importlinter contracts for
protocols/asgi/net ownership and the raise-message debt ratchet. If an
actionable raise message is fixed, remove its resolved fingerprint from
docs/design/raise-message-baseline.txt; do not regenerate the baseline to
admit new debt.
For new public-path raises, name the offending value when one exists, state
what was expected, and identify the governing field, ASGI message, protocol,
or spec. The syntactic ratchet checks statically visible minimum context and
punctuation; semantic PounceError code/catalog tests cover operator routing.
Recipes
Add a test
Use the @with_lifespan decorator from tests/conftest.py to skip lifespan
boilerplate:
from tests.conftest import with_lifespan
@with_lifespan
async def _echo_app(scope, receive, send):
await receive()
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"ok"})
Unit tests live in tests/unit/, integration tests in tests/integration/.
For tests that need a running worker, copy the _start_supervisor(...)
helper in tests/integration/test_multi_worker.py.
Add a config field
Pounce's TOML loader and the /info redaction allowlist are both derived
from dataclasses.fields(ServerConfig) โ fail-closed. Three steps:
- Add the field to
ServerConfiginsrc/pounce/config.pywith a type annotation and a default. - Classify it in
INFO_ALLOWLISTinsrc/pounce/_config_schema.pyasEXPOSE,REDACT_TO_BOOL, orOMIT. Missing entries fail thetest_allowlist_covers_every_config_fieldtest. - If it needs a CLI flag, add it to
serve/checkinsrc/pounce/_cli.py.
TOML loading and pounce config schema pick the field up automatically.
Add or expand a public feature
Start with the ownership gate in docs/design/core-contract.md. Classify the feature before writing code:
- Core: ASGI, HTTP/1.1, worker lifecycle, config validation, or operator diagnostics.
- Optional protocol: HTTP/2, WebSocket, HTTP/3, TLS, or another install-gated protocol/transport path.
- Helper: static files, middleware, compression, rate limiting, request queueing, debug pages, or another convenience layer.
- Tooling/integration: testing helpers, benchmarks, OpenTelemetry, Sentry, or external observability systems.
Every public feature PR should answer:
- Why should Pounce own this instead of app middleware, a reverse proxy, a process manager, or deployment tooling?
- Which surfaces change:
ServerConfig,pounce.run, CLI, TOML, schema, redaction/info, logs, metrics, error codes, docs, examples, tests, benchmarks, or changelog? - What proof is included, and what collateral moves with it?
- What limitations or degraded behavior remain?
If a PR only narrows public claims or adds contributor-process docs, say
no runtime behavior changed and name the docs/site parity checks used.
For numeric performance claims, either reference a benchmark artifact that
follows benchmarks/artifact-schema.json or explicitly label the number as a
local snapshot, tuning example, or historical note.
Add an error
Every raise of a PounceError subclass must pass a
code="POUNCE_<CATEGORY>_<SPECIFIC>" literal. The AST test
tests/unit/test_error_codes.py enforces the scheme at collection time.
raise TLSError(
f"TLS certificate not found: {path}",
code="POUNCE_TLS_CERT_FILE_NOT_FOUND",
hint="Check the --ssl-certfile path.",
)
Then add a matching entry in docs/troubleshooting.md so the coverage test
(tests/unit/test_troubleshooting_catalog.py) stays green. See
docs/design/error-codes.md for the naming scheme.
Releasing
Releases publish to PyPI from a GitHub release (see
.github/workflows/python-publish.yml). The publish workflow asserts the
release tag equals v<pyproject version> and runs twine check --strict on
the built artifacts, so a mistagged release or a metadata-broken wheel fails
before anything is published.
Cut a release:
- Add a Towncrier news fragment for each change in
changelog.d/(<ISSUE>.<added|changed|deprecated|removed|fixed|security>.md). - Compile the changelog:
poe changelog(runstowncrier build --yes). - Bump
versioninpyproject.toml([project]). - Commit the changelog + version bump, then tag:
git tag vX.Y.Zmatching the newpyproject.tomlversion (the publish workflow rejects a mismatch). - Run
make gh-release(derivesv$VERSIONfrompyproject.tomland creates the GitHub release, which triggers the PyPI publish workflow).
make gh-release keeps the tag and pyproject.toml version in lockstep; a
hand-cut GitHub release works too, but you must tag vX.Y.Z exactly.
PR expectations
- Tight diff. One concern per PR. Section headers in a diff mean it's two PRs. Exceptions: refactors that rename a concept across many files.
- Executable issue acceptance. A PR that says
Closes #123carries a@pytest.mark.issue(123)acceptance test. For non-testable docs or positioning work, recordAcceptance: n/a (reason)in the PR body. See docs/backlog-automation.md. - No
type: ignore. Target is zero. Narrow the type or fix the code. - No silent excepts. S110 is enforced in CI. If you must swallow, log what and why in one line.
- Benchmarks for hot-path changes. Touching
_fast_h1.py,sync_worker.py, or the protocol parsers needs a before/after frompounce bench. - Error messages say what to do next.
hint=is as important as the message itself. - PR body explains why. The diff explains what.
See AGENTS.md for the full list of stop-and-ask escape hatches.