Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
145 commits
Select commit Hold shift + click to select a range
368eede
chore(scraper): drop unused lab dependency
valmathieu May 12, 2026
dffe1bb
docs(workspace): reflect core completion in README
valmathieu May 12, 2026
fb68470
docs: refresh workspace architecture overview
valmathieu May 12, 2026
e5ed9f0
docs(core): document current contrai-core API
valmathieu May 12, 2026
f316ee2
docs(engine): align engine package doc with current layout
valmathieu May 12, 2026
019445d
docs(diagrams): note Mermaid alongside PlantUML
valmathieu May 12, 2026
d253067
docs(diagrams): add analyzer class diagram
valmathieu May 14, 2026
af6e1fa
docs(diagrams): add scraper observation sequence diagram
valmathieu May 14, 2026
d756ee2
docs(diagrams): rewrite diagrams page for MkDocs and embed Phase 1 pair
valmathieu May 14, 2026
ee787b2
docs(mkdocs): add site configuration
valmathieu May 14, 2026
11036ed
docs(mkdocs): scope plantuml_markdown source= lookups to docs/diagrams
valmathieu May 14, 2026
f68cbee
docs(diagrams): repurpose page as conventions hub
valmathieu May 14, 2026
8f80440
docs(analyzer): embed class diagram in package overview
valmathieu May 14, 2026
5012b22
docs(scraper): embed observation sequence diagram in package overview
valmathieu May 14, 2026
edffc0a
docs(diagrams): add core domain class diagram
valmathieu May 14, 2026
52f0490
docs(core): embed class diagram in package overview
valmathieu May 14, 2026
c0b1ee0
docs(diagrams): add engine model + MVC class diagram
valmathieu May 14, 2026
ed4eae9
docs(diagrams): add round lifecycle sequence diagram
valmathieu May 14, 2026
ea92e93
docs(diagrams): add bidding cycle sequence diagram
valmathieu May 14, 2026
294fc95
docs(diagrams): add single trick sequence diagram
valmathieu May 14, 2026
7c19126
docs(engine): embed class + 3 sequence diagrams in package overview
valmathieu May 14, 2026
b2b0d69
docs(diagrams): add workspace overview class diagram
valmathieu May 14, 2026
edca776
docs(architecture): embed workspace overview diagram
valmathieu May 14, 2026
1dcdca6
docs(diagrams): backfill Phase 1 PNGs and complete the catalogue
valmathieu May 14, 2026
44b434d
docs: reorganize ai/ → ai-ladder/ and packages/* → top-level docs layout
valmathieu May 17, 2026
0348c0a
docs(mkdocs): add homepage, API pages, theme assets, README usage
valmathieu May 17, 2026
41154fd
docs(rules): add Contrée domain knowledge reference
valmathieu May 17, 2026
bf906eb
ci: add GitHub Actions test workflow (matrix: core + engine)
valmathieu May 17, 2026
6a577c1
build: declare root workspace as a real project with docs dep group
valmathieu May 17, 2026
5a23923
build(scraper): declare empty py-modules for setuptools flat layout
valmathieu May 17, 2026
bd816a1
style(core): add missing type hints to Deck.is_empty / Hand.__getitem__
valmathieu May 17, 2026
b5cc72c
docs: add CONTRIBUTING.md
valmathieu May 17, 2026
5f53b8f
fix(core): reject Capot-over-Capot and repair RedoubleBid lookup
valmathieu May 17, 2026
6ac83df
test(core): add bid module tests
valmathieu May 17, 2026
aeebbfb
test(core): add contract module tests
valmathieu May 17, 2026
5e1a96f
test(core): add trick module tests
valmathieu May 17, 2026
89b7816
test(core): expand types, exceptions, card and base_player coverage
valmathieu May 17, 2026
d92a232
feat(engine): allow AI to bid Capot
valmathieu May 17, 2026
4beeaff
test(engine): cover AiPlayer Capot bidding
valmathieu May 17, 2026
44c2b80
fix(engine): consume real Trick API in AiPlayer trick-taking
valmathieu May 17, 2026
0aca325
docs(engine): add Rich TUI design handoff
valmathieu May 17, 2026
7caaa9d
feat(core): add Hand.copy() list-shim
valmathieu May 17, 2026
ff20622
fix(engine): consume Contract object in AiPlayer.choose_card
valmathieu May 17, 2026
dcff60a
fix(engine): lazy-init AiPlayer card tracking in choose_card
valmathieu May 17, 2026
3dab836
feat(engine): add on_trick_complete view hook to Round
valmathieu May 17, 2026
9944310
feat(engine): Rich-based terminal UI ("contrai" CLI)
valmathieu May 17, 2026
c2c731d
docs(diagrams): refresh engine class diagrams for Rich CLI
valmathieu May 17, 2026
7038e61
docs(diagrams): refresh engine sequence diagrams for Rich CLI
valmathieu May 17, 2026
54f3bd0
docs(diagrams): add CLI screen-flow state diagram
valmathieu May 18, 2026
c7471b9
docs: refresh engine + core narratives for Rich CLI
valmathieu May 18, 2026
73db967
refactor(core): move trick winner logic into Trick.get_current_winner
valmathieu May 18, 2026
ad2adf1
fix(engine): force over-trump when trump is led
valmathieu May 18, 2026
004eb14
fix(engine): partner-master replaces partner-leader for trump exemption
valmathieu May 20, 2026
69ba55e
refactor(engine): use English double/redouble vocabulary in CLI
valmathieu May 20, 2026
c31474d
feat(engine): prompt redouble when contract has been doubled
valmathieu May 20, 2026
9276212
feat(engine): show round number in Round panel title
valmathieu May 20, 2026
0cfe033
feat(engine): mark bidding-round boundaries in bidding history
valmathieu May 20, 2026
f2464e9
feat(engine): pause between AI actions (tunable via env)
valmathieu May 21, 2026
ae8fac6
feat(engine): persistent event log panel
valmathieu May 21, 2026
ad0118b
feat(engine): show round recap screen between rounds
valmathieu May 21, 2026
6e9275a
feat(engine): announce belote and rebelote during card play
valmathieu May 21, 2026
bcab664
docs: refresh engine + core narratives and diagrams for the new CLI
valmathieu May 21, 2026
300d664
fix(engine): unify belote/rebelote seat badge to '★ Belote'
valmathieu May 21, 2026
47a8c4a
style(engine): use #N format for round and trick panel titles
valmathieu May 21, 2026
0852073
feat(engine): break down round score by component in recap panel
valmathieu May 21, 2026
4ca296f
feat(engine): auto-pass when partner has doubled or redoubled
valmathieu May 21, 2026
9299229
fix(core): freeze the auction after a Double or Redouble
valmathieu May 21, 2026
3945058
feat(engine): bookmark the established contract in the event log
valmathieu May 21, 2026
96db90d
feat(engine): AI cuts with lowest trump on partner-master non-trump t…
valmathieu May 21, 2026
c67c380
Revert "feat(engine): AI cuts with lowest trump on partner-master non…
valmathieu May 23, 2026
66395f0
fix(engine): AI no longer wastes trumps when partner is master
valmathieu May 23, 2026
68ee363
feat(engine): add Contract row to the round-recap breakdown
valmathieu May 23, 2026
337ed4a
feat(engine): show round recap before the game-over screen
valmathieu May 23, 2026
52c4487
fix(engine): keep hand panel visible across non-player turns
valmathieu May 23, 2026
03b0620
docs: scrub scraper-target URL and CLAUDE.md cross-references
valmathieu May 23, 2026
e4593e4
feat(core): add Suit.ALL_TRUMP for the tout-atout variant
valmathieu May 23, 2026
fa13c57
style(core): pad Card.__str__ with a space between rank and suit
valmathieu May 25, 2026
5342ba5
fix(core): track Card.__str__ via str(Card) in test_deck
valmathieu May 25, 2026
c13cf42
refactor(core): standardize core test imports on the public API
valmathieu May 25, 2026
d7b6f9e
style(core): hoist function-local imports and normalize import order
valmathieu May 25, 2026
bf6021e
fix(core): correct contract.py TYPE_CHECKING alias for BasePlayer
valmathieu May 25, 2026
442e744
test(core): assign a real Team in BasePlayer settability test
valmathieu May 25, 2026
28f5566
refactor(core): drop unused Bid.can_be_doubled abstract method
valmathieu May 25, 2026
d8ccd8c
feat(core): introduce Auction state object for bidding rules
valmathieu May 26, 2026
8fa4507
refactor(engine): drive bidding through Auction, no more silent force…
valmathieu May 26, 2026
2200b2b
refactor(core): freeze Bid variants as dataclasses, drop is_valid_aft…
valmathieu May 26, 2026
b5c8b01
docs: reflect Auction-based bidding in diagrams + module docs
valmathieu May 26, 2026
d0b0389
test(core): assert real Team identity in BasePlayer settability test
valmathieu May 27, 2026
e397430
refactor: rename Capot to Slam across core + engine
valmathieu May 27, 2026
13c8803
feat(core,engine): introduce Solo Slam, adopt symmetric Slam-family s…
valmathieu May 27, 2026
bd296a8
docs: document Slam and Solo Slam in domain doc and diagrams
valmathieu May 27, 2026
5933d68
refactor: model Slam-family scoring as contract base + flat trick sub…
valmathieu May 27, 2026
c86108a
docs(core): tidy Auction module docstring
valmathieu May 27, 2026
efe1cd6
refactor(core): factor out suit-agnostic contract legality probe
valmathieu May 27, 2026
466e6e5
refactor(core): short-circuit legal_actions once a value clears
valmathieu May 28, 2026
b6618d0
feat(core,engine): extend numeric bid ladder to 170 and 180
valmathieu May 28, 2026
5087096
fix(core): allow Coinche/Surcoinche after intervening passes
valmathieu May 28, 2026
8167edc
test(core): align auction test sequences with anticlockwise turn order
valmathieu May 28, 2026
e3fa00c
refactor(engine): use English-only vocabulary in the CLI view
valmathieu May 28, 2026
10efcc5
feat(engine): show per-seat bids in the auction diamond
valmathieu May 30, 2026
ad2d9f0
fix(engine): handle Coinche/Surcoinche freeze in AiPlayer bidding
valmathieu May 30, 2026
562a37a
feat(engine): lay out bidding history one round per line with aligned…
valmathieu May 30, 2026
4c38a39
fix(engine): drop redundant empty-hand hint in the hand panel
valmathieu May 30, 2026
b86c4ac
feat(core): track Coinche/Surcoinche caller on the contract
valmathieu May 30, 2026
7f6bc49
feat(engine): name the taker and coincheur in the contract label
valmathieu May 30, 2026
779152b
fix(engine): reject illegal human bids in the prompt loop instead of …
valmathieu May 31, 2026
67fd727
feat(engine): suggest the cheapest legal raise in the bidding prompt
valmathieu May 31, 2026
04a2044
docs(engine): note the dynamic bidding-prompt example
valmathieu May 31, 2026
4836f12
fix(engine): keep the bid/card rejection notice inside the prompt frame
valmathieu May 31, 2026
81784e6
feat(engine): enrich the round-recap contract block (verbose markers …
valmathieu May 31, 2026
588f675
fix(engine): correct round scoring — belote by holder, doubled is win…
valmathieu May 31, 2026
df43848
fix(engine): align the round recap with corrected scoring
valmathieu May 31, 2026
e2cfe67
feat(engine): split round recap into Outcome and Scoring tables
valmathieu May 31, 2026
64e8148
docs(engine): document the two-table round recap layout
valmathieu Jun 2, 2026
6c940dd
fix(engine): dash the Outcome table on all-passed rounds
valmathieu Jun 2, 2026
005e75c
refactor(core): unify annotation handling with from __future__ import…
valmathieu Jun 2, 2026
a9c445f
refactor(core): drop unused Contract helpers
valmathieu Jun 3, 2026
b84fd47
refactor(core): drop incorrect Contract.is_made
valmathieu Jun 3, 2026
eae37e8
refactor(core): derive Contract double/redouble from the caller
valmathieu Jun 3, 2026
4a016ad
docs: harmonize game name to lowercase "contrée" in comments and docs…
valmathieu Jun 3, 2026
b426404
refactor(core): drop unused Trick.size/get_winner/is_empty
valmathieu Jun 4, 2026
435bfd8
refactor(core): make Trick.get_current_winner trump a required arg
valmathieu Jun 4, 2026
371db23
refactor(engine): reuse Trick.get_current_winner for trick winner
valmathieu Jun 4, 2026
aa6c38e
docs: sync Trick winner ownership in diagrams and package notes
valmathieu Jun 4, 2026
e5a3c61
docs(core): drop contree-domain.md references from docstrings and com…
valmathieu Jun 4, 2026
965f15d
refactor(core): add ContraiError base, type hints, and domain excepti…
valmathieu Jun 4, 2026
713aedf
feat(engine): raise IllegalPlayError on illegal card plays
valmathieu Jun 4, 2026
3fc3180
docs: document ContraiError hierarchy and IllegalPlayError play boundary
valmathieu Jun 4, 2026
59038bd
refactor(core): make Card a frozen value object with (suit,rank) equa…
valmathieu Jun 5, 2026
2fd37e6
test(core): drop str()/tuple Card-equality workarounds in deck/round …
valmathieu Jun 5, 2026
366d6e8
docs(core): mark Card as a frozen value object in the class diagram
valmathieu Jun 5, 2026
04cf7bb
test: enable importlib import mode for whole-workspace pytest runs
valmathieu Jun 5, 2026
01cbd5b
feat(core): add Hand.has_suit and route has_card through Card membership
valmathieu Jun 5, 2026
9fd8ada
refactor(engine): route AiPlayer hand queries through the Hand API
valmathieu Jun 5, 2026
41ba090
refactor(engine): adopt Hand query API in Round legality and belote t…
valmathieu Jun 8, 2026
28932d6
refactor(engine): use Hand.has_suit in RichView lead detection
valmathieu Jun 8, 2026
bc4f275
refactor(engine): reorganize round recap into factual Outcome and rol…
valmathieu Jun 14, 2026
31234e7
refactor(engine): catch InvalidContractError in wire_to_bid, not Valu…
valmathieu Jun 20, 2026
286dadf
feat(engine): score unannounced capot as contract + 250
valmathieu Jun 20, 2026
79ab75b
refactor(core): model Slam/SoloSlam as a typed SlamLevel enum
valmathieu Jun 20, 2026
7d2f84f
refactor(engine): adopt SlamLevel and couple the unannounced-capot su…
valmathieu Jun 20, 2026
6bd14a6
docs(core): update class diagram for SlamLevel
valmathieu Jun 20, 2026
0f75554
refactor(engine): replace the unannounced-capot string tag with an Un…
valmathieu Jun 20, 2026
7e0caf2
refactor(engine): add Outcome total, drop +signs, dash unscored round…
valmathieu Jun 20, 2026
3c2d7dd
style(engine): separate trick count from point rows in recap outcome
valmathieu Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: tests

on:
pull_request:
branches: [main]
push:
branches: [main]

# Cancel stale runs when you push new commits to the same PR/branch.
# Saves CI minutes and gives you a faster signal on the latest code.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test:
name: pytest (${{ matrix.package }})
runs-on: ubuntu-latest

strategy:
# Don't abort the whole matrix if one package fails;
# you want to see which packages broke independently.
fail-fast: false
matrix:
package:
- contrai-core
- contrai-engine

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
# Invalidate the cache when the lockfile changes.
cache-dependency-glob: "uv.lock"

- name: Set up Python
# No arg = uv reads requires-python / .python-version
# and installs the matching interpreter (3.14 for ContrAI).
run: uv python install

- name: Sync workspace
# --all-packages installs every workspace member, including
# dev dependencies. Needed because contrai-engine depends on
# contrai-core in editable mode within the workspace.
run: uv sync --all-packages --all-extras

- name: Run pytest
# --package scopes the run to one workspace member, so we get
# a clean per-package status check in the PR UI.
run: uv run --package ${{ matrix.package }} pytest
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ build/
dist/
wheels/

# MkDocs build output.
site/

# ─── Virtual environments ─────────────────────────────────────────────────
.venv/

Expand Down
269 changes: 269 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
# Contributing to ContrAI

ContrAI is a personal research project on AI for the French card game
*Contrée*. It's a learning vehicle as much as an engineering project, so the
conventions here lean toward "make it easy for future-Valentin (or any
collaborator) to pick up the thread six months later" rather than ceremony for
its own sake.

If you've forked or cloned this repo to play around, build on top of it, or
contribute back: welcome. This file is the handbook.

## What's in the repo

ContrAI is a [uv](https://docs.astral.sh/uv/) workspace with four packages:

- **`contrai-core`** — Shared domain types (cards, suits, contracts, players).
No game logic, no UI. Other packages depend on this.
- **`contrai-engine`** — The Contrée game engine. CLI, MVC architecture,
rule-based AI players, pytest-tested.
- **`contrai-analyzer`** — A Streamlit dashboard for hand analysis using
hypergeometric probabilities. Deliberately independent of `contrai-core`
(different abstractions for a different question — don't try to unify them).
- **`contrai-scraper`** — A Playwright-based spectator scraper for an online
Contrée site, persisting observed games to SQLite.

Specs and the LaTeX report live in a **separate** repo, `contrai-docs`. Don't
propose changes to `Specs_fonctionnelles.md` or `Specs_logicielles.md` in PRs
here — those live there.

## Getting started

Requirements:

- Python 3.14 (uv will install it for you)
- [uv](https://docs.astral.sh/uv/getting-started/installation/)

Clone and set up:

```bash
git clone https://github.com/<your-fork>/contrai.git
cd contrai
uv sync --all-packages --all-extras
```

Run the tests:

```bash
uv run --package contrai-core pytest
uv run --package contrai-engine pytest
```

Run the CLI engine:

```bash
uv run --package contrai-engine python -m contrai_engine
```

Run the analyzer dashboard:

```bash
uv run --package contrai-analyzer streamlit run src/contrai_analyzer/app.py
```

(Adjust the entrypoint paths once the actual modules are in place.)

## Branching

ContrAI follows **GitHub Flow**: `main` is always working, every non-trivial
change lives on a short-lived feature branch, and changes land via pull
requests.

Branch names mirror the Conventional Commit type and the scope of the work:

```
<type>/<scope>-<short-description>
```

Examples:

```
feat/engine-bidding-double
fix/core-suit-equality
refactor/engine-extract-trick-class
test/core-deck-shuffle-edge-cases
ci/add-ruff-check
```

Types and scopes come from the Conventional Commits convention (see below).

Don't commit directly to `main`. The only exception is the very first
scaffolding commit on an empty repo; after that, everything goes through a
branch.

## Commits

Every commit follows
[Conventional Commits](https://www.conventionalcommits.org/):

```
<type>(<scope>): <subject>

[optional body explaining what and why]
```

Allowed types:

- `feat` — A new feature
- `fix` — A bug fix
- `refactor` — Code change that neither fixes a bug nor adds a feature
- `docs` — Documentation only
- `test` — Adding or fixing tests
- `chore` — Build, tooling, maintenance
- `perf` — Performance improvement
- `style` — Formatting only (no code change)
- `build` — Build system or dependency changes
- `ci` — CI configuration

Scopes usually map to packages: `core`, `engine`, `analyzer`, `scraper`. In
the `contrai-docs` repo, scopes are `specs` or `report`.

**Atomic commits.** One logical change per commit. The rule of thumb: if
you'd want to revert two things together, they belong in one commit; if you'd
ever want to revert one without the other, split them. Adding a feature with
its tests is one logical change. Refactoring AND adding a feature is two.

**No AI co-authorship trailers.** Commits are attributed to the human author
only, regardless of whether an AI assistant helped draft the change.

## Pull requests

Open a PR for anything beyond a one-line fix or typo. Yes, even when you're
the only person on the project — the PR is your **self-review ritual**.

What a self-review catches:

- Forgotten `print()` or `breakpoint()`
- Stale docstrings
- Commented-out code that should have been deleted
- Inconsistent naming
- Missing tests
- Files you forgot to add to the staging area

Process:

1. Push your branch to GitHub.
2. Open a PR with a description that explains *what* and *why* (the *how* is
in the diff).
3. Use **draft PRs** for work-in-progress you want CI to run on without
claiming it's ready.
4. Review your own diff in the GitHub UI before merging.
5. Wait for CI to pass.
6. Merge.

PR titles should match the eventual commit subject — Conventional Commits
format.

## Merging

The default merge strategy is **rebase-merge**: each commit on the feature
branch is replayed onto `main`. This keeps the history linear and preserves
the atomic commits you carefully crafted.

Use **squash-merge** when the branch has messy intermediate commits (`wip`,
`fix typo`, `actually fix it`) or when the whole PR is one logical change and
the individual commits aren't worth keeping.

Avoid **merge commits**. They add noise without information for a project
this size.

## Tests

`contrai-core` and `contrai-engine` use pytest. Tests are mandatory for:

- New Model-layer code in the engine (non-negotiable per project rules)
- New types or invariants in `contrai-core`
- Bug fixes — write a test that fails before the fix and passes after; it's
the cheapest insurance against regression

`contrai-analyzer` and `contrai-scraper` don't currently have test suites.
When they grow them, this section gets updated and they join the CI matrix.

CI runs pytest on `core` and `engine` on every PR. The merge button is
blocked until tests pass.

Run locally before pushing:

```bash
uv run --package contrai-core pytest
uv run --package contrai-engine pytest
```

## Code style

- **Type hints everywhere.** Function signatures, class attributes, return
types. `ruff`, `mypy`, or `pyright` may join CI later.
- **Google-style docstrings** on every public class, method, and function.
- **Didactic comments are welcome.** This is a learning project — explain
non-trivial logic, especially anything probability-, combinatorics-, or
ML-related. Comments that explain *why* (not *what*) are gold.
- **English in code.** Identifiers, comments, docstrings — all English.
Reports and specs live in the `contrai-docs` repo and may be bilingual.

## Architecture rules

The engine uses **MVC**: Model (game state, rules), View (CLI), Controller
(orchestration). Don't bypass it silently. If a feature seems to require
crossing the boundaries — for instance, a Gym-style env wrapper for RL
training accessing internal state — raise it in an issue or PR description
first. MVC is explicitly on the table for re-discussion when ML training
arrives; until then, respect it.

The **analyzer** has its own conventions and abstractions (e.g., `SuitSlot`).
Don't try to unify them with engine abstractions like `Suit` — they answer
different questions, and the deliberate separation is the point.

## Dependencies

Don't add a dependency to any `pyproject.toml` without flagging it first
(open an issue or mention it in the PR description before adding). Every
dependency is a long-term commitment — preferable to reach for the stdlib or
a small focused library before pulling in a heavy one.

When you do add a dependency: `uv add --package <pkg> <dep>` and commit the
updated `pyproject.toml` and `uv.lock` together.

## Diagrams

- **PlantUML** for sequence and class diagrams. Source files end in `.puml`.
- **Mermaid** for everything else (component, state, flowchart, ER,
deployment, mindmap, etc.). Source files end in `.mmd`.
- **Use color.** Distinguish MVC layers, package boundaries, actors,
hot/cold paths. Plain black-and-white renders are dispreferred.

Rendering commands:

```bash
plantuml -tpng diagram.puml # → diagram.png
mmdc -i diagram.mmd -o diagram.png # → diagram.png
```

Commit the source (`.puml` or `.mmd`) and the rendered `.png` **together in
the same commit**. The PNG is what readers see on GitHub and in the report;
the source is what gets edited.

## Releases and versioning

ContrAI is pre-1.0, so versions are `0.x.y`. Breaking changes are allowed in
minor bumps. All four packages move in **lockstep** — same version, same
release — until something external (the planned multiplayer web server, a
published artifact, a friend's fork pinning a specific version) forces
independence.

Releases are **milestone tags**:

```bash
git tag -a v0.1.0 -m "First playable CLI engine"
git push --tags
```

GitHub Releases (with notes) for the bigger milestones — these double as
report material later. There's no fixed release cadence; tag when something
worth tagging happens.

## Questions

Open an issue. Tag it with the package involved, or `meta` for cross-cutting
questions.

#
Loading
Loading