Skip to content

Contributing

Welcome to chanx-kit. All contributions are welcome, as long as they give real usage to the community. To keep the quality of the package high, there are a few prerequisites you should follow:

  • Coding with an AI assistant is acceptable, as long as you review every line carefully, without blindly accepting the changes. Your name is on the PR: if it breaks, you fix it.
  • It should come from your own need. Use the kit yourself before proposing it, in a real deployment; a hobby project counts. Code that someone actually runs is better than code that merely passes review, and it is what these kits are for.
  • Do not leave trivial comments in the code. A comment earns its place when it explains complex maths, a workaround, or some dark magic (we do not like dark magic, but occasionally we need it). AI assistants write far too many of the other kind, so prune those before you commit.

Read a neighbouring kit before you start. Generated code that ignores the topic conventions or the message style will be sent back.

Everything else follows from how kits are delivered: everything in kits/ is copied into user projects, and nothing here is installed. A change reaches people only when they run copit update, and it must not silently break them. Runtime helpers live in chanx; the test harness is itself a kit (kits/chanx_testing/). A helper only one kit needs belongs in that kit.

New kits start at tier: contrib. One that proves useful and stable is promoted to core by changing that one line: no move, no id change, and nothing breaks for anyone who already installed it. The flat layout is deliberate; see kits/README.md.

Setup

uv sync
uv run pre-commit install

uv sync installs both frameworks, lint, docs and tooling. Pre-commit formats, and when you touch a kit checks that copit-registry.json is still current, which is the thing easiest to forget and most annoying to discover in CI.

For the sandbox's cross-process demos and live provider checks, copy .env.example to .env (git-ignored) and docker compose up -d for Redis. The test suite reads neither.

Checklist for a new kit

  1. kits/<package_name>/ with a directory name that is a valid Python identifier (room_chat, while its id is room-chat)
  2. kit.yaml, the registry metadata; see below
  3. messages.py, giving every message an action Literal and a Pydantic payload
  4. topics.py; see docs/authoring-a-kit.md
  5. README.md with an install line, minimal usage, a hook table and production caveats
  6. tests/test_<kit>.py, which must pass on both backends. If the kit wraps a third-party library, cover the default path too, not just a stubbed override, and add its dependencies to the dev group in pyproject.toml so both backend legs can import it. A kit that genuinely cannot run on both declares only_variants instead; see below
  7. Mount it in sandbox/consumers.py, so its messages reach the AsyncAPI schema that freezes the wire contract. If it has a UI kit, give it a page in sandbox/ui/src/demos.tsx
  8. A feature users install gets a line in docs/recipes.yaml and the README's feature table: the one command that installs its server and UI halves
  9. uv run python scripts/registry.py build to regenerate the index

The README and the tests are not optional: scripts/registry.py refuses to build an index for a kit missing either.

kit.yaml

Each kit describes itself in one file. scripts/registry.py reads every kits/*/kit.yaml plus the repo-wide registry.yaml, and generates the committed copit-registry.json that the CLI fetches.

name: room-chat
title: Room chat
description: >-
  A chat room: persisted history replayed on connect, plus a live roster from the
  presence kit. Pluggable store.
version: 0.1.0
tier: core
role: feature
area: messaging
tags: [chat, rooms, history, persistence]
authors: ["Your Name <you@example.com>"]

requires:
  - presence

dependencies:
  - "chanx>=2.10.0,<3"

optional:
  tests:
    requires:
      - chanx-testing
  • name: the registry id, in dashes. The directory must be name.replace("-", "_"), because that is the package a user imports.
  • title, description: what the docs site and the kit index show. The description is folded to a single line when the index is built, so a YAML block scalar is fine.
  • version: the kit's own version, independent of any repo tag. Bump it whenever you change the kit; it is how a user can tell what they copied.
  • tier: core or contrib, defaulting to contrib. A core kit may only require other core kits, which is what keeps the maintained core self-contained.
  • role, area: where the docs file the kit. role is feature, store, foundation (installed for you by other kits), tooling or provider. area is messaging, presence, agents, voice or tooling. A provider has no area: it implements a contract and is listed under Providers, one kit per vendor with a part per capability. Neither field is published to the index.
  • defines, contract_topic: the wire contract the kit owns (chat@1), and the topic class the UI kits' contract.ts is generated from.
  • implements: contracts the kit speaks without owning them, as a provider does.
  • parts: pieces of one kit a user can install alone (--only stt), each with its include files and the requires, dependencies and implements only it needs. Parts are subtractive: everything a part lists is also on the kit, so a copit that predates parts installs the whole kit. registry.py check enforces it.
  • tags: keywords, shown on the kit page. authors: metadata only.
  • requires: other kits in this registry, resolved transitively at install time and checked for cycles. This applies to every install, so only list what the kit itself imports.
  • dependencies: real packages, installed by the user's package manager.
  • optional: what a file group needs in order to work, resolved only when someone installs with --with <group>. The tests group is where chanx-testing belongs, since a user who did not ask for tests should not get a test harness. Which files the group contains is decided by registry.yaml, not here.
  • only_variants: the frameworks the kit needs. Omit it unless the kit truly cannot run on both; see below.

Nothing about which files get copied lives here: the include and exclude rules, the tests optional group and the install target are registry-wide, in registry.yaml, and the file list is materialised at build time. See docs/registry-conventions.md for those choices and why they are made once rather than per kit.

Framework-specific kits

The both-backends rule is what keeps kits reusable, and it is enforced by the CI matrix rather than by a lint: a kit importing django simply fails the fast-channels leg.

Some kits cannot follow it. Django models, migrations, admin and DRF views have no fast-channels equivalent, and a fake one helps nobody. Those declare what they need:

only_variants:
  - django

That one line does three things: copit refuses to install the kit into a project that has not selected the variant, the test suite stops collecting it on the other backend, and the docs group it separately. So opting out of a backend is also what lifts the "import only chanx.core" rule, because you are no longer claiming to run there.

Use it only when porting is genuinely impossible. A pluggable store with a framework-specific implementation (as django-message-store is for room-chat) is the shape that works: the portable kit keeps the wire contract, and the restricted one is an implementation of its store protocol.

Provider kits

A provider plugs a vendor into a contract (transcriber@1, synthesizer@1, ag-ui@1).

  • One kit per vendor, with a part per capability (stt, tts). A new capability is a new part, not a new kit.
  • Subclass the contract's topic and run its shared suite against it (TranscriberContract and friends); see writing a provider.
  • Tests never call the vendor: fake its server, as kits/deepgram/tests/fake_listen.py does.
  • Read the API key when it is used, never at import: an api_key class attribute with a key() falling back to the environment variable.
  • uv run python scripts/live_check.py <provider> round-trips it live with the key from .env, for a few cents. Run it whenever you change a provider.

Before you open a PR

Run the hooks, both backends, the type checker and the registry checks. CI runs the same things, one framework at a time and across Python 3.11 to 3.14.

uv run pre-commit run --all-files

CHANX_KIT_BACKEND=fast_channels uv run pytest kits tests
CHANX_KIT_BACKEND=channels      uv run pytest kits tests

uv run pyright
uv run python scripts/registry.py check
uv run python scripts/recipes.py check    # recipes and every `copit add` in the docs
uv run mkdocs build --strict

pyright runs in strict mode and no suppressions are accepted, because composing kits must not force users to disable error classes.

Two checks are slower and easy to leave to CI, but worth running when the change touches installs or the framework boundary:

# one framework installed, which is what actually proves a kit does not need the other
CHANX_KIT_BACKEND=channels uv run --no-default-groups \
    --extra django --group dev-django --group registry pytest kits tests

uv run python scripts/conformance.py    # install every kit with copit and import it

Conformance needs copit 0.9.1 or newer for its parts check, which uv sync installs. To check against a local copit build instead: COPIT=../copit/target/release/copit uv run python scripts/conformance.py. Or run the whole matrix with tox.

Voice changes are worth one browser run: scripts/sandbox_voice_e2e.mjs drives the sandbox's voice demos with Chrome's fake microphone (see its header for how to run it).

A weekly job re-resolves every dependency to its newest version and runs the suite against it. Kits are copied, so upstream breakage reaches users through their next install rather than through a release here.

UI kits

UI kits live in ui/, a second copit registry (ui/registry.yaml, published as ui/copit-registry.json). Each binds to a contract, not a server kit: contract.ts is generated, core.ts is framework-free and tested with vitest, react/ ships only with the react variant, and styles are one CSS file driven by --chanx-* variables.

A UI kit may build on others: list them in requires and import their cores relatively (../transcriber/core), never through an alias. A kit that only combines others, like voice-agent, declares no consumes; its contracts come from the kits it requires. Its kit.yaml also takes a role and an area, as a server kit's does.

npm ci --prefix ui
npm --prefix ui run typecheck
npm --prefix ui test
uv run --extra fastapi --group dev-fastapi python scripts/contracts.py check

The sandbox installs every UI kit through copit, and tests fail when one is missing or a copy drifts from ui/. Add a new one with cd sandbox/ui && copit add @chanx-kit-ui/<kit>, and refresh the copies after a change with copit update-all.

Commit messages

Conventional Commits, which is what [tool.commitizen] in pyproject.toml expects when it derives a version and a changelog. The type prefix is what matters; the scope is optional and is usually the kit id.

feat(presence): add a Redis-backed store
fix(room-chat): replay history before the roster
docs: explain the topic pattern namespace
chore: bump the chanx floor

Changing an existing kit

  • Changing a message shape changes the generated TypeScript types, so CI fails until you regenerate them (npm --prefix sandbox/ui run gen). Bump the kit's version and explain the migration in its README.
  • If the kit defines a contract, regenerate the UI kits' copies of it (uv run --extra fastapi --group dev-fastapi python scripts/contracts.py build, after npm ci --prefix ui) and run npm --prefix ui test. A change that breaks a consumer bumps the contract version (notification@1 to notification@2) in defines and in every implements / consumes.
  • Adding an optional field is backwards compatible. Renaming or removing one is not; prefer adding the new field and deprecating the old.
  • New hooks need defaults that preserve current behaviour.

Review

Expect questions about the store abstraction (is the default honest about its limits?), group naming (is it namespaced?), and whether hooks are overridable without forking. Those three decide whether a kit is reusable or just published.