<!-- OpenSmartRoute: Contributing. Source https://github.com/isathish/OpenSmartRoute/blob/main/CONTRIBUTING.md; HTML https://opensmartroute.ai/docs/contributing -->
# Contributing to OpenSmartRoute

Thanks for helping build an open, intelligent route to the right decision.

## Ground rules

- **Core stays dependency-free.** Anything needing torch, numpy, FastAPI, etc. goes behind an
  optional extra or into `opensmartroute-contrib-*`.
- **Every strategy is inductive.** It must work for a target it has never seen, using only
  the target's declared capabilities/examples.
- **Every decision is explainable.** New strategies return a `rationale` string.
- **Every learner is persistable.** Implement `state()` / `load()`.
- **No raw request text in logs.** Hash + length only.

## Workflow

```bash
python3.12 -m venv .venv && source .venv/bin/activate      # Windows: .venv\Scripts\Activate.ps1
pip install -e '.[dev]'
ruff format src tests scripts
ruff check src tests scripts && mypy && bandit -q -r src -c pyproject.toml
pytest -q
osr -t examples/targets.yaml -r examples/rules.yaml eval examples/eval_dataset.jsonl --min-accuracy 0.85
osr -t examples/targets.yaml safety --learned-guard
python scripts/release.py check
```

CI runs the same commands on Linux (Python 3.10 to 3.13) and Windows (3.12), then builds the wheel,
sdist and container image. Pull requests must keep:

- eval accuracy >= 0.85 on `examples/eval_dataset.jsonl` (`--min-accuracy` exits non-zero otherwise);
- the safety-routing suite green (`osr safety --learned-guard`);
- the public API snapshot (`tests/public_api.json`) unchanged, or updated deliberately in the same PR
  with a CHANGELOG entry;
- `pyproject.toml`, `opensmartroute.__version__` and the top CHANGELOG section in agreement
  (`scripts/release.py check`);
- every exported name with a one-line summary (a docstring, or a same-line comment for constants)
  and `docs/REFERENCE.md` regenerated (`python scripts/api_reference.py`; `tests/test_docs.py`
  fails on a stale reference or an undocumented name; `... report` lists the gaps).

Every user-visible change gets a bullet under `## [Unreleased]` in `CHANGELOG.md`
(Keep a Changelog format: Added / Changed / Deprecated / Removed / Fixed / Security).

If you touch the hot path (`signals`, `policy`, `router`, any `Strategy.score`), run
`python scripts/bench.py` before and after and paste both tables in the PR.
If you touch brand assets, edit `scripts/brand_build.py` and run it with `--export`; never edit
the generated SVG/PNG files by hand.

## Public API and deprecation policy

The public surface is every name in the `__all__` of the packages listed in `tests/public_api.json`
plus the keyword arguments of `Router.__init__`. `tests/test_public_api.py` fails on any addition or
removal, so every change to that surface is a deliberate, reviewed diff of the snapshot.

- **Additions** are allowed in any minor release and get a CHANGELOG `Added` bullet.
- **Behaviour changes** to an existing public symbol need a `Changed` bullet; if the old behaviour
  can be kept behind a parameter, keep it as the default for one minor.
- **Removals and renames** go through a deprecation: the old symbol keeps working for at least one
  minor release, calls `opensmartroute.errors.deprecated(name, since=, removal=, replacement=)`
  (which emits `OpenSmartRouteDeprecationWarning`, a `DeprecationWarning` subclass), is listed
  under `Deprecated` in the CHANGELOG, and is deleted no earlier than the `removal` version named in
  the warning, with a `Removed` bullet. Before 1.0 the minimum notice is one minor; from 1.0 on it is
  two minors, and removals happen only in a major release.
- **State formats** (`state()` / `load()` payloads, `VersionedStateStore` schema) are versioned; a
  newer library must load older state, and loading newer state raises rather than guessing.
- **Settings** (`OSR_<GROUP>_<FIELD>`) follow the same rules as symbols; renamed keys are read under
  both names for the deprecation window.
- `tests/public_api.json` is regenerated from the actual `__all__` lists, never edited by hand, and
  the regenerating PR must show only additions unless it also carries the matching `Removed` entry.

## Adding a strategy

1. Subclass `opensmartroute.strategies.Strategy`, set `name`, implement `score()`.
2. If it learns, implement `update(outcome)` and, if it has a model, `model.state()/load()`.
3. Add a unit test with a deterministic seed.
4. Add a row to the strategy table in `README.md` and the idea-to-module map in `docs/RESEARCH.md`.

## Adding an adapter

Adapters live in `src/opensmartroute/adapters/`. Anything needing a third-party package must be
imported lazily inside the function/class and raise `ConfigurationError` naming the extra to install.
Test transports against a real in-process server (see `tests/test_adapters.py`), not by mocking
`urllib`.

## Adding math

Put it in `src/opensmartroute/math/`, cite the source in the docstring, include the formula,
and add a numerical test against a known value.

## Adding or editing an agent skill

Skills live in `.claude/skills/<name>/SKILL.md` and follow the Agent-Skills format the SDK loads
with `load_skills`. The directory name must equal `name`; every fact in the body must be
verifiable against the code (tests assert the packages load and route). Keep `osr-domains` to
ontology names (`opensmartroute.signals.ontology.DOMAIN_LEXICON` or `general`) and omit
`osr-actions` for broad skills. Validate with `osr skills` and run `tests/test_skills.py`; when a
skill's behaviour changes, update the matching request in `REPRESENTATIVE` too. Every skill is
published on the documentation site at `/docs/skills/<name>`; no web change is needed.

## Editing documentation

The documentation site in `platform/web` (`/docs/<slug>`; see "Documentation site" in
`platform/README.md`) is for end users of the hosted platform, the Python SDK and the pip package. It
renders `docs/*.md` (except `docs/sales/`), the root `README`/`CHANGELOG`/`SECURITY`/this file,
`deploy/README.md`, `spec/ocm/README.md`, the skills, and a REST API reference generated from
`platform/api/openapi.json`. Internal documents (`docs/PLATFORM_PLAN.md`, `docs/GO_TO_MARKET.md`,
`docs/BRAND.md`, `docs/sales/`, `platform/README.md`) stay in the repository and are linked from
`README.md` only. Keep one H1 per file, use H2/H3 for structure (they become the table of contents and
the search index), give fenced code a language and use relative links to other Markdown files. A new
`docs/*.md` needs a link in `README.md` (`tests/test_docs.py`) and, when it is user-facing, an entry in
`platform/web/src/lib/docs/catalogue.ts`; run `npm run build` in `platform/web` to preview
(`npm run dev` for live reload). `docs/REFERENCE.md` is generated; never edit it by hand. After changing
a platform route or model, regenerate the OpenAPI snapshot with
`python platform/api/scripts/export_openapi.py` (CI runs `--check`). Documentation may only name things
that exist: `tests/test_docs_claims.py` verifies every `osr <command> --flag` and every `OSR_*` variable
mentioned in the Markdown against the CLI parser and the code, and
`platform/api/tests/test_platform_docs_claims.py` verifies documented endpoints against the app's routes,
`/dashboard/<page>` paths against the web app, the site catalogue against `docs/` and the CHANGELOG
sections (date, body, compare link). Coding agents should start from `AGENTS.md`.

## Commit style

Conventional Commits (`feat:`, `fix:`, `docs:`, `perf:`, `refactor:`, `test:`, `chore:`).

## Releasing

Releases are cut by GitHub Actions; nobody edits version numbers or tags by hand.

1. **Prepare.** Run the *Prepare release* workflow (Actions tab, `workflow_dispatch`) with a bump
   (`major` / `minor` / `patch`) or an explicit version. It runs `scripts/release.py prepare`, which
   bumps `pyproject.toml` and `__version__`, turns `## [Unreleased]` into a dated section and rewrites
   the compare links, then opens a pull request labelled `release`.
2. **Review.** The PR diff is the release: version bump plus CHANGELOG. CI runs on it like any other PR.
3. **Merge.** Merging a `release`-labelled PR into `main` triggers the *Release* workflow, which
   re-verifies version consistency, runs the test suite, builds the sdist and wheel (with build
   provenance attestations), creates the annotated tag `vX.Y.Z` and the GitHub release with the
   CHANGELOG section as notes, publishes to PyPI through trusted publishing (no API token) and pushes
   `ghcr.io/isathish/opensmartroute:X.Y.Z` and `:latest`.

The *Release* workflow can also be dispatched manually on `main` when the tree is already at a
releasable version (used for the first release, or to retry a failed publish; it refuses to run if the
tag already exists).

One-time repository setup for maintainers:

- PyPI: add a trusted publisher for project `opensmartroute` (owner `isathish`, repository
  `OpenSmartRoute`, workflow `release.yml`, environment `pypi`).
- GitHub: create the `pypi` environment (optionally with required reviewers); allow Actions to create
  pull requests (Settings > Actions > General) so *Prepare release* can open the PR. Optionally add a
  fine-grained `RELEASE_TOKEN` secret (contents and pull-requests write) so CI runs on the release PR
  even though it was opened by automation.
- Tags for historical versions so compare links resolve: `git tag -a v0.3.0 <sha> -m "opensmartroute 0.3.0"`.

`scripts/release.py` has no dependencies beyond the standard library and is covered by
`tests/test_release_tooling.py`.

**1.0.0.** `python scripts/release.py readiness` evaluates the v1.0 readiness table in
[docs/ROADMAP.md](https://opensmartroute.ai/docs/ROADMAP.md) from the repository. *Release* rows (frozen API, performance envelope,
reference deployments, security review pack, published leaderboard run, two consecutive non-breaking
releases verified against the tagged `tests/public_api.json`) block any `1.x` version in `release.py check`;
*adoption* rows (an independent report row in `docs/SECURITY_REVIEW.md`, an `accepted` listing in
`examples/leaderboard/results/README.md`, two rows in `ADOPTERS.md`) are tracked and printed but never
block. The *Release* job prints the table in its summary on every run.

## Code of conduct

We follow the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
