Contributing¶
Development¶
uv sync
uv run pytest # test suite
uv run ruff check . # lint
uv run ruff format --check . # formatting (as CI runs it)
uv audit --preview-features audit --locked # known vulnerabilities in uv.lock
uv run skill-lens list ./examples
.python-version pins the interpreter to 3.13, so uv sync uses the same one CI does — no
per-job setup step, and no drift between your machine and the pipeline. The package itself
still supports >=3.11.4 (requires-python — the patch matters, because tarfile's
filter= keyword, which --baseline previous uses to extract a previous bundle safely,
only exists from 3.11.4); CI does not currently exercise that floor.
Tests marked integration hit real provider APIs and are deselected by default; run them with
uv run pytest -m integration. Tests marked cassette replay recorded provider traffic —
zero cost, no key needed, and selected by default. Everything else passes offline with no API
spend. uv run skill-lens run ./examples needs the pydantic-ai runner (see
Running against a real agent); the shipped examples now
assert real model behavior, so list is what dogfoods discovery for free.
test_a_baseline_run_reaches_the_provider_without_the_skill_name in tests/test_cassettes.py
proves the baseline no-name-leak rule survives the wire, but its cassette has not been recorded yet
— no API key was available when it was written. A missing cassette skips rather than
fails, by design, so a fresh clone is never red for this; whoever has a key next should
record it with:
uv run pytest tests/test_cassettes.py --record-mode=once
Development is test-driven: write the failing test first, then the implementation.
Documentation¶
uv sync --group docs
uv run mkdocs serve # live preview at http://127.0.0.1:8000
uv run mkdocs build --strict # as CI runs it
uv run pytest tests/test_docs.py
Documentation ships with the change, not as a follow-up. Two CI jobs enforce it: docs
builds the site with --strict and runs tests/test_docs.py with the docs group installed,
and docs-freshness fails a PR that changes
src/skill_lens/** without touching docs/, README.md, ARCHITECTURE.md or
mkdocs.yml. When a change genuinely needs no documentation — a pure refactor, a
dependency bump — add the no-docs-needed label to the PR.
tests/test_docs.py is the precise half of the same idea. It asserts that:
- every command, flag, config key,
EvalCasefield and assertion kind appears in the docs; - every page is in the nav, and linked from
README.md's Documentation table or the home page's "Where to go next" table; - no relative link is dead, and no
#anchornames a heading that does not exist. Renaming a heading means updating every link to it; - the install instructions are written once, in
docs/snippets/install.md, which pages include with--8<-- "docs/snippets/install.md".README.mdcannot include a file, so its copy of the command is compared with the snippet's; ARCHITECTURE.mdandCLAUDE.mdstate the three protocols assrc/defines them.
Heading ids are worked out the way Python-Markdown does it. With the docs group installed,
the same file also checks that rule against the real renderer; that is why the docs job
runs it.
tests/test_troubleshooting.py does the same for Troubleshooting.
Every message quoted there is listed in its QUOTED table with the module that emits it,
and the test fails if either side changes. A new message on that page needs a row there.
Conventional Commits are required¶
Every commit message and every pull request title must follow Conventional Commits:
<type>[optional scope][!]: <description>
Types: feat, fix, docs, refactor, test, perf, build, ci, chore,
style, revert. Use the imperative mood, lowercase, no trailing period.
This is not stylistic. cz bump derives the next version and the changelog from
commit history, so a non-conforming message silently breaks the release. Because
PRs are squash-merged, the PR title becomes the commit on main — so the
title is what release automation actually reads.
Install the hooks once per clone so bad messages are rejected before they land, and the dependency audit runs before every push:
uv run pre-commit install --hook-type commit-msg --hook-type pre-push
The audit hook runs at push time rather than commit time because it needs the network; see Security for what it checks and what to do when it finds something.
CI enforces the same rules on every PR: the title is checked with cz check, and
every commit on the branch with scripts/check_commits.py. Two docs-only commits
predating the convention are exempted in scripts/legacy-commits.txt; that list
should only ever shrink.
One-time repository settings¶
These settings live in the GitHub UI, not in this repo, so they are easy to miss when standing up a fork or a new instance of this project:
- Settings -> Code security -> Private vulnerability reporting = enabled.
SECURITY.mdand the Security page send reporters to the private advisory form; with the setting off, that form does not exist and the only remaining path is a public issue. -
Settings -> Code security -> Dependabot alerts and Dependabot security updates = enabled.
.github/dependabot.ymlturns on version updates by itself; alert-driven security updates — a pull request the day an advisory is published, rather than on the weekly schedule — are a separate switch. -
Settings -> Pages -> Source = "GitHub Actions". Without it,
docs.yml'sbuildjob succeeds butdeployfails with an opaque error, and every published docs link (including the ones inREADME.md) stays dead. - Copilot code review enabled on the repository. Without it,
.github/copilot-instructions.mdand.github/instructions/*are never read, so Copilot's PR reviews ignore this project's conventions.