Contributing
Thanks for looking. This file covers the dev setup, how the tests run, where the two most common kinds of contribution go, and how decisions get recorded.
Dev setup
The repository is a uv workspace with two packages:
packages/truewire: the CLI, spec loader, checks, mock server and Python generator.packages/core-python:truewire-core, the runtime that generated clients import.
git clone https://github.com/truewire-dev/truewire
cd truewire
uv sync --all-packages
uv run truewire --helpPython 3.11 or newer for the toolchain (the runtime supports 3.10). uv sync creates .venv and installs both packages in editable mode.
Running tests
uv run pytest # everything
uv run pytest packages/truewire/test # toolchain only
uv run pytest packages/core-python/test # runtime only
uv run pytest -k pagination # by keywordLint and format with ruff, using the config in the repository root:
uv run ruff check .
uv run ruff format .CI runs all three. A pull request that fails any of them will not be reviewed until it passes.
Code style is short: 2-space indentation, single quotes, typing_extensions over typing, TypedDict and Literal over dataclasses and enums, a one-line docstring on every function, class and module. Read a file next to the one you are editing and match it.
Adding a spec check
A check is a function in packages/truewire/src/truewire/spec/authoring.py that takes an endpoint's operation (a plain JSON view of its request and response schemas) and returns a list of violations. Each violation carries a rule id, a JSON path into the spec, and a message.
- Write the rule in
docs/spec/authoring.mdfirst. State what is checked, why, and one concrete example of a wrong and a right spec. If you cannot write the "why" in two sentences, the check is probably a preference, not a rule. - Add
check_<name>toauthoring.py. Reusenodes()to walk schemas andis_ref()to skip references. Keep it operation-local if you can; a check that needs the whole project (likecheck_meta) is wired separately in the CLI. - Append it to the
CHECKStuple. - Decide the severity. A check that can be wrong (a name heuristic, like the timestamp-suffix rule) goes into
WARNING_RULESand reports as a warning. A check that is never wrong when it fires is an error. Warnings do not failtruewire check; errors do. - Add tests in
packages/truewire/test/test_authoring.py: one endpoint that violates the rule, one that does not, and one that looks like a violation but is not. - Run
truewire checkagainst the example projects underexamples/and fix or justify every new finding.
Adding a pagination strategy
Pagination is a discriminated union on strategy in packages/truewire/src/truewire/spec/endpoint.py. Each strategy is a closed pydantic model. Adding one touches four places, in this order:
- Model. Add a
<Name>Paginationclass inendpoint.pywithstrategy: Literal['<name>'], the parameters the walk needs, and adoneterminator restricted to the kinds that strategy can actually decide. Add it to thePaginationunion. - Audit. Extend
check_paginationinspec/authoring.pyso every parameter the block names is a real request parameter, every response path resolves against the response schema, and any parameter the walk does arithmetic on is numerically typed. - Generator. Add the walk in
packages/truewire/src/truewire/codegen/python.py.Generator.paged_methodrenders the async-generator shape;Generator.paged_response_methodand its siblings render the awaitablePaginatedResponseshape. Prefer the second where the strategy can compute(rows, next_state)per page, since it is strictly more useful to callers. - Tests. A model test (
test_endpoint_pagination_model.py) for the declaration, a generator test for the emitted code, and a mock-backed test that walks at least three pages. One page cannot tell a correct walk from one that stops immediately.
Then document the strategy in docs/spec/authoring.md rule 8 with a worked pagination block, and open an ADR if the strategy changes what an existing declaration means.
Architecture Decision Records
docs/adr/ holds decisions that are hard to reverse, cost real evidence to reach, or trade one guarantee for another. Not a changelog. Not a restatement of what the code makes obvious.
Write one when a decision would otherwise survive only in a commit message or a pull request thread. Copy docs/adr/0000-template.md, take the next number, add a row to docs/adr/README.md. Three sections: Context (what forces were at play, what evidence prompted this), Decision (stated as a decision, not a description of the mechanism), Consequences (what gets easier, what gets harder, what is left open).
An ADR is never edited in place. A changed decision gets a new ADR that supersedes the old one, and the old one's Status line is updated to say so.
Commits and pull requests
- One logical change per commit. Subject line under 72 characters, imperative mood, prefixed with the area:
spec:,check:,mock:,codegen:,core:,cli:,docs:. - The body says why, not what. The diff already says what.
- A pull request that changes generated output includes the regenerated example projects in the same PR, so reviewers see the effect.
- A pull request that changes the spec format includes the ADR, the authoring rule and the check together.
Contributions written with Claude or another coding agent
Welcome. Much of Truewire was written this way. Two requests:
- Say so in the pull request description, and say what you checked yourself. "Generated with Claude, I ran the tests and read the diff" is a fine sentence. "Generated with Claude" alone is not enough.
- Every claim in a docstring, a doc page or an ADR has to be true of the code in the PR, not of the code the agent imagined. Reviewers will run the examples.
Agent-written PRs get the same review as any other: tests, a read of the diff, and a check that the ADR or doc rule matches the behavior. They are not held to a higher bar, and they are not waved through.
Reporting problems
Open an issue with the spec (or a minimal endpoint.json), the command you ran, and what you expected. For a wrong generated type, include the recorded example that shows the real wire shape. That example is usually the whole fix.
Releasing
Each package releases from its own pull request, merged into main:
- Branch
release/core(fortruewire-core) orrelease/truewire(fortruewire) offmain. - Bump
versionin the package'spyproject.tomland add the entry to itsCHANGELOG.md. - Open the pull request; its body becomes the top of the GitHub release notes.
- Merge.
release-core.yml/release-truewire.ymlrun the package's tests, build it, publish to PyPI (Trusted Publishing, no tokens), push thetruewire-core-v<version>/truewire-v<version>tag, and create the GitHub release. A version already on PyPI is skipped, so re-merging is safe.
truewire depends on truewire-core, so when both change, release core first.