diff --git a/CHANGELOG.md b/CHANGELOG.md index cd06ae7..c1c36ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ semantics have been stable since 1.0.0. ## [Unreleased] +### Changed — documentation +- **README onboarding and claim wording.** "Try it in 30 seconds" is now "Try it": the recipe starts + in a fresh virtual environment (`python -m pip install dorian-vwp==1.4.0`) and commits with a + command-local Git identity, so it needs no prior install and no global Git configuration; the + documented exits stay `0` (verify) and `4` (revalidate). The opening states what dorian checks + and what its strength depends on; "read-only" now describes only C1, C3, and typed C5; the + benchmark entry is labelled a historical synthetic result, reproducible from a source checkout; + the httpx entry is described as a retrospective, scoped reproduction. + `tests/test_readme_example.py` now mirrors the recipe's commit, runs without inherited Git + identity or global Git config, and pins the recipe's install line to the package version. +- **`docs/BENCHMARK_CURRENT.md`** is retitled "Archived benchmark results and compatibility notes"; + the figures are unchanged. +- **`docs/releases/v1.4.0.md`** matches the published release body: a narrower summary, the pane/TUI + deferral stated plainly, and documentation links pinned to the `v1.4.0` tag. +- **`CONTRIBUTING.md`** added: setup, `make lint` / `make test`, the README-example test, the + zero-runtime-dependency rule, security reporting, and commit conventions. + ## [1.4.0] — 2026-07-02 ### Added — Dorian Loop Guard (1.4.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c5c9e64 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to dorian + +Small, focused pull requests with tests are welcome. This page covers the development setup, the +checks a pull request must pass, and the rules the codebase keeps. + +## Set up + +You need Python 3.11+, Git, and [uv](https://docs.astral.sh/uv/). + +```bash +git clone https://github.com/ajaysurya1221/dorian.git +cd dorian +make install # uv sync: the package plus the dev dependency group +``` + +## Checks + +Run both before you open a pull request; CI runs the same suite on Python 3.11, 3.12, and 3.13. + +```bash +make lint # ruff check and ruff format --check over src, tests, and bench +make test # the full pytest suite, slow tests included +``` + +While iterating, `make test-fast` skips the slow tests (benchmark runs, the wheel build, and real +pytest subprocesses), and `make fmt` applies the linter fixes and the formatter. + +**The README example is a test.** `tests/test_readme_example.py` runs the README's "Try it" recipe as +a black box and checks that the README still shows those commands. If you change the recipe, change +the test in the same pull request and run it: + +```bash +uv run pytest tests/test_readme_example.py +``` + +Other documentation guard tests pin public wording (for example `tests/test_docs_polish.py` and +`tests/test_benchmark_evidence.py`). If you change pinned text on purpose, update its guard in the +same pull request and say why. What results and docs may claim is set by +[`docs/VALIDATION_HONESTY.md`](docs/VALIDATION_HONESTY.md). + +## Rules + +- **Zero runtime dependencies.** `[project].dependencies` in `pyproject.toml` stays empty and the + core imports only the standard library. Optional features belong in an extra (`data`, `extract`); + development tools belong in the `dev` dependency group, locked in `uv.lock`. + `make dependency-report` prints the current posture. +- **No model on the verification path.** `tests/test_firewall_import_closure.py` fails if a model SDK + or network client is imported at module level anywhere in the verdict path's import graph. +- **Benchmark contributions carry aggregate numbers only.** +- **Conventional Commits.** Write commit subjects as `type: summary`, using the types already in the + history: `feat`, `fix`, `docs`, `test`, `chore`, `ci`, `release`. Explain the why in the body. + +## Security + +Do not report vulnerabilities in public issues. Follow [SECURITY.md](SECURITY.md): open a +[GitHub security advisory](https://github.com/ajaysurya1221/dorian/security/advisories/new), or a +regular issue without exploit details asking for a private channel. diff --git a/README.md b/README.md index 7d8480d..9fc5f86 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,9 @@ **Hold AI agents to what they said they did.** -*The summary still reads perfectly. Its portrait doesn't.* -

Quickstart - Demo + Demo GitHub Action

@@ -27,26 +25,39 @@ -`dorian` holds an AI coding agent to what it *said* it did. The agent (or you) writes a `claims.json` -of checkable claims about a change — "`handler()` lives in `app.py`", "the login timeout is 30 -seconds", "`test_login_ratelimit` passes". `dorian verify` turns each claim into a deterministic -check, runs it against the real code, and seals the result beside the code in a git-committed -`.warrant` sidecar. On every later commit, `dorian revalidate` re-runs only the checks whose watched -files changed and flips the warrant to **REVOKED** the moment a claim stops being true — naming the -claim. No model is called at verification time (**zero model tokens at check time**), so the checker -cannot be talked past by the code it verifies. It ships as a CLI, a GitHub Action, and Claude Code -hooks, with **zero runtime dependencies**. +`dorian` turns explicit claims about a code change into executable checks and stores their results +in a `.warrant` beside the code. Later revalidation reruns affected checks and revokes warrants when +those checks fail. Verification makes no model calls (**zero model tokens at check time**); its +strength depends on the claims, checkers and trusted execution environment. Its question is: +**"Does the code still satisfy this recorded claim?"** + +The agent (or you) writes a `claims.json` of checkable claims about a change — "`handler()` lives in +`app.py`", "the login timeout is 30 seconds", "`test_login_ratelimit` passes". `dorian verify` runs +each claim's checker against the code and seals the results in a git-committed `.warrant` sidecar. On +each later commit, `dorian revalidate` re-runs only the checks whose watched files changed and flips +the warrant to **REVOKED** when a load-bearing claim's check fails, naming the claim. It ships as a +CLI, a GitHub Action, and Claude Code hooks, with **zero runtime dependencies**. -## Try it in 30 seconds -A self-contained run on a throwaway repo — copy-paste it; it leaves nothing behind but a -temp directory. (A black-box test pins this exact sequence, so it stays runnable.) +## Try it + +A self-contained run in a throwaway directory, with its own virtual environment and a command-local +Git identity, so it needs no prior install and no global Git configuration. The demo repository and virtual environment stay in the temporary directory. (A black-box test pins the commands and their exit codes.) ```bash -tmp=$(mktemp -d) && cd "$tmp" && git init -q +tmp=$(mktemp -d) +cd "$tmp" +python3 -m venv .venv +source .venv/bin/activate +python -m pip install dorian-vwp==1.4.0 +git init -q +printf '.venv/\n' > .gitignore + printf 'def handler():\n return 200\n' > app.py printf '# change note\n\n`handler()` lives in app.py.\n' > note.md -git add -A && git commit -q -m "app + note" +git add app.py note.md .gitignore +git -c user.name=Demo -c user.email=demo@example.invalid \ + -c commit.gpgsign=false commit -q -m "app + note" cat > claims.json <<'JSON' {"claims": [ @@ -63,19 +74,20 @@ printf 'def renamed():\n return 200\n' > app.py dorian revalidate --since HEAD # -> handler-exists BROKEN; WARRANTED -> REVOKED (exit 4) ``` -`note.md` never changed and `git`/CI stay quiet — but the warrant flips to REVOKED, naming -the exact claim that stopped being true. (Don't have `dorian` yet? See -[Install](#install).) +`note.md` never changed, but the warrant flips to REVOKED and names the claim whose check failed. +`python3` must be 3.11 or newer. For other ways to install, see [Install](#install). ## What it checks and what it does not -A claim is bound to one of four read-only checker families: **C1** (a quoted span of the artifact -itself), **C3** (a path, symbol, string, or regex in a file, plus the structural `py-signature:` / +A claim is bound to one of four checker families: **C1** (a quoted span of the artifact itself), +**C3** (a path, symbol, string, or regex in a file, plus the structural `py-signature:` / `py-const:` / `code:` / `config-value:` forms), **C4** (a `pytest:` node id), or **C5** (typed data -checks on CSV/parquet files, or a `shell:` command). `verify` is *born-verifiable*: it seals only if -every claim holds right now (exit 0) and writes nothing otherwise (exit 4). A warrant is born -**WARRANTED** and folds on each `revalidate` to **TRUSTED**, **DEGRADED**, **REVOKED**, or **UNKNOWN** -(a checker could not run — never counted as broken, never silently green). +checks on CSV/parquet files, or a `shell:` command). C1, C3, and typed C5 only read files; C4 and C5 +`shell:` execute code (see [Security](#security-claims-are-executable-input)). `verify` is +*born-verifiable*: it seals only if every claim holds right now (exit 0) and writes nothing +otherwise (exit 4). A warrant is born **WARRANTED** and folds on each `revalidate` to **TRUSTED**, +**DEGRADED**, **REVOKED**, or **UNKNOWN** (a checker could not run — never counted as broken, never +silently green). The honest limits: @@ -155,10 +167,13 @@ jobs: One command scaffolds a project-local skill: `dorian claude-code install-claim-warrants`. After a change, `/dorian-claim-warrants` drafts the change note + `claims.json` for the checkable facts the -agent claimed and prints the verify command — **the model only drafts; `dorian verify` proves**. -`dorian revalidate --since ` on later PRs REVOKEs whatever the code drifted away from. Add -`--with-hook` for an opt-in, reminder-only Stop hook. Paste-ready prompt, runnable example pack, and -`settings.json` sample: [`docs/USE_WITH_CLAUDE_CODE.md`](docs/USE_WITH_CLAUDE_CODE.md) and +agent claimed and prints the verify command. **The model drafts; deterministic checkers evaluate the +specified claims.** Review the drafted `claims.json` before running `verify`: C4 and C5 `shell:` +checkers execute code ([Security](#security-claims-are-executable-input)). On later PRs, +`dorian revalidate --since ` re-runs the affected checks and revokes a warrant when a +load-bearing check fails. Add `--with-hook` for an opt-in, reminder-only Stop hook. Paste-ready +prompt, runnable example pack, and `settings.json` sample: +[`docs/USE_WITH_CLAUDE_CODE.md`](docs/USE_WITH_CLAUDE_CODE.md) and [`examples/claude-code/`](examples/claude-code/). Using dorian inside AI coding loops: `dorian loop preflight --since ` re-checks the warrants a @@ -182,7 +197,8 @@ install` adds a `SubagentStop` gate and a fail-closed `PreToolUse` veto for unat claim and C5 checker suggestions to paste into `claims.json`. - `dorian report --audit` — the event log as byte-identical JSONL. - `dorian bench mutation` · `bench large-mutation` · `bench binding-lifecycle` · `bench public-repos` - — the reproducible benchmark suites. + — the benchmark suites; they need a source checkout with development dependencies (the wheel does + not ship `bench/`). Exit codes: `0` ok/TRUSTED · `2` usage/infra · `3` DEGRADED · `4` REVOKED/integrity · `5` ERRORED-only (checkers could not run; never conflated with broken) · `6` scope violation. @@ -195,15 +211,17 @@ Full reference: [`docs/COMMANDS.md`](docs/COMMANDS.md). recall **0.93**, versus file-change watchers at recall 1.00 but precision **0.34** (naive), **0.56** (path-scope), and **0.59** (line-aware) — **11.6x** fewer false alarms than the path-scope watcher (58 → 5) and **10.4x** fewer than the line-aware one (52 → 5). Synthetic, not your - repository; measured at v0.7.0 and **historical** — last re-run, unchanged, at v1.2.0 - ([`docs/BENCHMARK_CURRENT.md`](docs/BENCHMARK_CURRENT.md), - [`docs/BENCHMARK_v0.7.0.md`](docs/BENCHMARK_v0.7.0.md)). Reproduce: `dorian bench large-mutation`. -- **One real catch.** A load-bearing claim sealed against [`encode/httpx`](https://github.com/encode/httpx) - — `requires-python` is `">=3.8"` — was flipped WARRANTED → REVOKED (exit 4) by a later upstream PR - ([#3592](https://github.com/encode/httpx/pull/3592), "Drop Python 3.8 support") while httpx's own - test suite stayed green. Full output and a from-scratch reproduction: - [`docs/REAL_CATCH_LOG.md`](docs/REAL_CATCH_LOG.md). One documented catch — evidence, not universal - validation. + repository; measured at v0.7.0 ([`docs/BENCHMARK_v0.7.0.md`](docs/BENCHMARK_v0.7.0.md); + compatibility notes in [`docs/BENCHMARK_CURRENT.md`](docs/BENCHMARK_CURRENT.md)). Historical + synthetic result, last rerun at v1.2.0. To reproduce from a source checkout with development + dependencies installed, run `uv run dorian bench large-mutation`. +- **Retrospective public-repository check.** Revalidating a warrant across two pinned httpx commits + detected the documented Python-support change. This is a scoped reproduction, not evidence of + deployment adoption or a prevented production defect. The load-bearing claim was that + [`encode/httpx`](https://github.com/encode/httpx) declares `requires-python = ">=3.8"`; the upstream + commit for [#3592](https://github.com/encode/httpx/pull/3592) ("Drop Python 3.8 support") flipped the + warrant WARRANTED → REVOKED (exit 4). Full output and a from-scratch reproduction: + [`docs/REAL_CATCH_LOG.md`](docs/REAL_CATCH_LOG.md). One documented case, not universal validation. ## Docs @@ -213,7 +231,8 @@ Full reference: [`docs/COMMANDS.md`](docs/COMMANDS.md). - [`docs/BINDING.md`](docs/BINDING.md) — binding semantics: trigger vs. truth. - [`docs/AGENT_CLAIMS.md`](docs/AGENT_CLAIMS.md) and [`spec/checkers.md`](spec/checkers.md) — writing claims; the checker grammar. -- [`docs/BENCHMARK_CURRENT.md`](docs/BENCHMARK_CURRENT.md) — current-version benchmark reruns. +- [`docs/BENCHMARK_CURRENT.md`](docs/BENCHMARK_CURRENT.md) — archived benchmark results and + compatibility notes. - [`SECURITY.md`](SECURITY.md) and [`docs/SECURITY_BOUNDARY.md`](docs/SECURITY_BOUNDARY.md) — security. - [`docs/ROADMAP_BACKLOG.md`](docs/ROADMAP_BACKLOG.md) — the structured roadmap backlog. @@ -225,6 +244,7 @@ make install && make lint && make test ``` Small, focused PRs with tests are welcome. Benchmark contributions carry aggregate numbers only. +Development setup, checks, and conventions: [`CONTRIBUTING.md`](CONTRIBUTING.md). ## License diff --git a/docs/BENCHMARK_CURRENT.md b/docs/BENCHMARK_CURRENT.md index ac6ad8b..0245510 100644 --- a/docs/BENCHMARK_CURRENT.md +++ b/docs/BENCHMARK_CURRENT.md @@ -1,10 +1,13 @@ -# Current-version benchmark results +# Archived benchmark results and compatibility notes -Version-stamped reruns of dorian's reproducible benchmark suites on the **current** code, -so the published numbers track the implementation rather than lagging behind it. The older +The benchmark figures dorian publishes, with a note per release on whether that release could +move them. The suites were last re-run at v1.2.0; the version stamp below records the current +package version after a review of what each later release changed, not a new run. The older result docs ([`BENCHMARK_v0.7.0.md`](BENCHMARK_v0.7.0.md) = v0.7.0, [`BENCHMARK_BINDING_LIFECYCLE.md`](BENCHMARK_BINDING_LIFECYCLE.md) = 0.9.0) are **historical** -and are kept as-is for provenance. +and are kept as-is for provenance. The suites are synthetic, and reproducing them needs a source +checkout with development dependencies installed (`uv run dorian bench …`); the published wheel +does not ship `bench/`. ## Measurement environment diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index e723cf9..06bfd80 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -1,6 +1,6 @@ # dorian — the long-form tour -The [README](../README.md) is the 30-second version. This page keeps the full narrative it used to +The [README](../README.md) is the short version. This page keeps the full narrative it used to carry: the illustrative walkthrough, the story behind the name, the Claude Code and loop-guard integrations, the governance preview, the complete getting-started path, and the roadmap. The command reference is in [`COMMANDS.md`](COMMANDS.md); binding semantics (trigger vs. truth) are in @@ -22,7 +22,7 @@ now and is re-checked on every future change, so a confident summary doesn't qui ## The 60-second aha *(Illustrative — these files are not in your checkout; run the copy-paste demo in the -[README](../README.md#try-it-in-30-seconds) to try it yourself.)* +[README](../README.md#try-it) to try it yourself.)* An agent finishes a change and emits the claims it just made — a `claims.json` next to the work, each claim bound to a read-only deterministic checker: @@ -322,7 +322,7 @@ The workflow snippet, pinned to the released Action tag, lives in the those two copies are the ones the release tests keep in sync with the package version. Now that `dorian` is installed, the copy-paste runnable demo at the top — -[Try it in 30 seconds](../README.md#try-it-in-30-seconds) — runs end to end against a throwaway repo. +[Try it](../README.md#try-it) — runs end to end against a throwaway repo. ## Writing claims an agent can be held to diff --git a/docs/releases/v1.4.0.md b/docs/releases/v1.4.0.md index 1562a3c..d06e21d 100644 --- a/docs/releases/v1.4.0.md +++ b/docs/releases/v1.4.0.md @@ -9,12 +9,9 @@ the verification path** (a build-time import firewall now proves it). ## Summary -dorian is growing from *"deterministically verify AI claims about code"* into a **deterministic -governance kernel** for agentic engineering. The guiding thesis: *dorian does not train agents to be -good — it makes bad actions non-continuable, non-mergeable, or human-reviewable.* "Crime" means an -observable contract violation; "punishment" means a mechanical loss of affordance (a blocked tool -call, a forced repair, a failed gate, a revoked warrant, a human escalation). dorian is the court; -host adapters and CI/branch-protection are the enforcement. +This release adds optional loop steering and host hooks around claim revalidation. It reports +continue, repair or escalate from observable records; the host and CI configuration decide whether +execution or merging is blocked. ## What shipped @@ -51,10 +48,11 @@ host adapters and CI/branch-protection are the enforcement. - **Release hardening** — functional PreToolUse-veto subprocess tests (identity mismatch, godmode, freshness boundary, empty-path escalate, malformed-stdin open/closed) and an installed-wheel `dorian governance install` scaffold test. -- **Docs** — [`GOVERNANCE_DATA_MODEL.md`](../GOVERNANCE_DATA_MODEL.md), - [`DORIAN_PANE.md`](../../archive/docs/DORIAN_PANE.md) (pane vision — deferred), [`DORIAN_LOOP_GUARD.md`](../DORIAN_LOOP_GUARD.md), - and gate/C4-flakiness notes in [`SECURITY_BOUNDARY.md`](../SECURITY_BOUNDARY.md) / - [`VALIDATION_HONESTY.md`](../VALIDATION_HONESTY.md). +- **Docs** — [`GOVERNANCE_DATA_MODEL.md`](https://github.com/ajaysurya1221/dorian/blob/v1.4.0/docs/GOVERNANCE_DATA_MODEL.md), + [`DORIAN_PANE.md`](https://github.com/ajaysurya1221/dorian/blob/v1.4.0/docs/DORIAN_PANE.md) (pane vision — deferred), + [`DORIAN_LOOP_GUARD.md`](https://github.com/ajaysurya1221/dorian/blob/v1.4.0/docs/DORIAN_LOOP_GUARD.md), + and gate/C4-flakiness notes in [`SECURITY_BOUNDARY.md`](https://github.com/ajaysurya1221/dorian/blob/v1.4.0/docs/SECURITY_BOUNDARY.md) / + [`VALIDATION_HONESTY.md`](https://github.com/ajaysurya1221/dorian/blob/v1.4.0/docs/VALIDATION_HONESTY.md). ## Deterministic boundary (unchanged invariants) @@ -115,8 +113,5 @@ feature = delete `.claude/hooks/dorian_*.py` and the settings entries; the core ## Pane / TUI boundary -The **pane / TUI / brand** are **deferred**. [`DORIAN_PANE.md`](../../archive/docs/DORIAN_PANE.md) -(now archived) is a future-facing vision and data contract, **not** an implementation — no UI/TUI code -ships in this release. This release owns only the deterministic data contracts and governance -invariants; the visual design (layout, typography, color, interaction, banner, logo) is a separate, -later effort. +The pane and TUI are deferred. This release provides CLI commands and host-hook templates; it does +not include a user interface. diff --git a/tests/test_readme_example.py b/tests/test_readme_example.py index 0470eb8..94dbdf8 100644 --- a/tests/test_readme_example.py +++ b/tests/test_readme_example.py @@ -1,10 +1,16 @@ -"""The README's "Try it in 30 seconds" recipe, executed as a black box. +"""The README's "Try it" recipe, executed as a black box. -This pins the headline runnable example to reality: it runs the exact sequence the README +This pins the headline runnable example to reality: it runs the sequence the README shows (out-of-process, via `python -m dorian`) and asserts the observable result, and it -checks the README still contains that command — so the demo a new user copy-pastes can +checks the README still contains those commands — so the demo a new user copy-pastes can never silently become a broken claim. (A tool whose whole pitch is "don't ship false claims" must not ship a false claim in its own README.) + +The recipe commits with a command-local Git identity, so the test strips inherited identity +variables and ignores global/system Git config: the README's own commit command must supply +the identity (Git can still guess one from the hostname on some hosts). The recipe's +`pip install` step is not run here (no network); the test exercises this checkout and pins +the README's install line to its version. """ from __future__ import annotations @@ -13,16 +19,41 @@ import re import subprocess import sys +import tomllib from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] -_GIT_ENV = { - "GIT_AUTHOR_NAME": "t", - "GIT_AUTHOR_EMAIL": "t@t", - "GIT_COMMITTER_NAME": "t", - "GIT_COMMITTER_EMAIL": "t@t", -} +_IDENTITY_VARS = ( + "GIT_AUTHOR_NAME", + "GIT_AUTHOR_EMAIL", + "GIT_COMMITTER_NAME", + "GIT_COMMITTER_EMAIL", + "EMAIL", +) + +# the README's commit command, identity included (kept identical to the README block) +_COMMIT_ARGS = ( + "-c", + "user.name=Demo", + "-c", + "user.email=demo@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "-q", + "-m", + "app + note", +) + + +def _clean_env() -> dict[str, str]: + """No inherited identity, no global or system Git config: a machine without Git setup.""" + env = {k: v for k, v in os.environ.items() if k not in _IDENTITY_VARS} + env["GIT_CONFIG_GLOBAL"] = os.devnull + env["GIT_CONFIG_NOSYSTEM"] = "1" + return env + # the claims.json the README's recipe writes (kept identical to the README block) _CLAIMS_JSON = ( @@ -35,25 +66,26 @@ def _git(repo: Path, *args: str) -> None: - subprocess.run( - ["git", *args], cwd=repo, env={**os.environ, **_GIT_ENV}, check=True, capture_output=True - ) + subprocess.run(["git", *args], cwd=repo, env=_clean_env(), check=True, capture_output=True) def _dorian(*args: str, repo: Path) -> subprocess.CompletedProcess: # mirror the README recipe exactly: cd into the repo, default --repo=".", relative paths cmd = [sys.executable, "-m", "dorian", *args] - return subprocess.run(cmd, cwd=repo, capture_output=True, text=True, timeout=120) + return subprocess.run( + cmd, cwd=repo, env=_clean_env(), capture_output=True, text=True, timeout=120 + ) def test_readme_try_it_recipe_runs_end_to_end(tmp_path: Path) -> None: repo = tmp_path / "repo" repo.mkdir() _git(repo, "init", "-q") + (repo / ".gitignore").write_text(".venv/\n") (repo / "app.py").write_text("def handler():\n return 200\n") (repo / "note.md").write_text("# change note\n\n`handler()` lives in app.py.\n") - _git(repo, "add", "-A") - _git(repo, "commit", "-q", "-m", "app + note") + _git(repo, "add", "app.py", "note.md", ".gitignore") + _git(repo, *_COMMIT_ARGS) (repo / "claims.json").write_text(_CLAIMS_JSON, encoding="utf-8") # verify: the claim holds against the real code -> sealed, exit 0 @@ -78,6 +110,21 @@ def test_readme_still_contains_the_runnable_commands() -> None: assert "dorian verify note.md --claims claims.json" in readme assert "dorian revalidate --since HEAD" in readme assert "symbol:app.py::handler" in readme + assert "git add app.py note.md .gitignore" in readme + assert "git -c user.name=Demo -c user.email=demo@example.invalid" in readme + assert '-c commit.gpgsign=false commit -q -m "app + note"' in readme + + +def test_readme_recipe_installs_the_current_release() -> None: + """The recipe starts from a fresh venv with a pinned install; the pin must be this version.""" + readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") + version = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))["project"][ + "version" + ] + assert "python3 -m venv .venv" in readme + pins = re.findall(r"python -m pip install dorian-vwp==(\d+\.\d+\.\d+)", readme) + assert pins, "the Try it recipe must install a pinned dorian-vwp release" + assert all(pin == version for pin in pins), f"README pins {pins}, package is {version}" def _github_slug(heading_text: str) -> str: