pytest-given 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. {pytest_given-0.2.0 → pytest_given-0.3.0}/.gitignore +8 -0
  2. {pytest_given-0.2.0 → pytest_given-0.3.0}/AGENTS.md +32 -33
  3. {pytest_given-0.2.0 → pytest_given-0.3.0}/CHANGELOG.md +51 -1
  4. {pytest_given-0.2.0 → pytest_given-0.3.0}/GLOSSARY.md +21 -16
  5. pytest_given-0.3.0/PKG-INFO +131 -0
  6. pytest_given-0.3.0/README.md +103 -0
  7. pytest_given-0.3.0/THIRD-PARTY-LICENSES +137 -0
  8. pytest_given-0.3.0/docs/site/configuration/narration-lint.md +51 -0
  9. {pytest_given-0.2.0 → pytest_given-0.3.0}/noxfile.py +251 -20
  10. {pytest_given-0.2.0 → pytest_given-0.3.0}/pyproject.toml +23 -4
  11. {pytest_given-0.2.0/src/pytest_given/skills_data → pytest_given-0.3.0/src/pytest_given/.agents/skills}/pytest-given-authoring/SKILL.md +2 -2
  12. {pytest_given-0.2.0/src/pytest_given/skills_data → pytest_given-0.3.0/src/pytest_given/.agents/skills}/pytest-given-authoring/references/api.md +15 -18
  13. {pytest_given-0.2.0/src/pytest_given/skills_data → pytest_given-0.3.0/src/pytest_given/.agents/skills}/pytest-given-authoring/references/domain-storytelling.md +12 -11
  14. {pytest_given-0.2.0/src/pytest_given/skills_data → pytest_given-0.3.0/src/pytest_given/.agents/skills}/pytest-given-authoring/references/glossaries.md +13 -11
  15. {pytest_given-0.2.0/src/pytest_given/skills_data → pytest_given-0.3.0/src/pytest_given/.agents/skills}/pytest-given-authoring/references/scenarios.md +7 -7
  16. pytest_given-0.3.0/src/pytest_given/.agents/skills/pytest-given-authoring/references/stories.md +77 -0
  17. {pytest_given-0.2.0/src/pytest_given/skills_data → pytest_given-0.3.0/src/pytest_given/.agents/skills}/pytest-given-navigating/SKILL.md +8 -9
  18. {pytest_given-0.2.0/src/pytest_given/skills_data → pytest_given-0.3.0/src/pytest_given/.agents/skills}/pytest-given-navigating/references/report-json.md +32 -12
  19. {pytest_given-0.2.0/src/pytest_given/skills_data → pytest_given-0.3.0/src/pytest_given/.agents/skills}/pytest-given-reviewing/SKILL.md +21 -16
  20. pytest_given-0.3.0/src/pytest_given/.agents/skills/pytest-given-reviewing/references/pairs.md +64 -0
  21. pytest_given-0.3.0/src/pytest_given/.agents/skills/pytest-given-reviewing/references/story-coverage.md +24 -0
  22. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/__init__.py +4 -4
  23. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/__init__.py +11 -9
  24. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/collector.py +48 -82
  25. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/discovery.py +5 -8
  26. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/file_glossary.py +5 -10
  27. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/glossary.py +75 -77
  28. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/kind_inference.py +12 -12
  29. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/process_state.py +3 -4
  30. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/scenario.py +33 -41
  31. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/source.py +10 -12
  32. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/steps.py +76 -124
  33. pytest_given-0.3.0/src/pytest_given/capture/story.py +420 -0
  34. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/template.py +9 -10
  35. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/cli/__init__.py +0 -5
  36. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/cli/report.py +37 -33
  37. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/cli/skills.py +12 -10
  38. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/grouping/attachments.py +3 -1
  39. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/grouping/checks.py +132 -122
  40. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/grouping/columns.py +37 -46
  41. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/grouping/templatize.py +11 -10
  42. pytest_given-0.3.0/src/pytest_given/lint/__init__.py +25 -0
  43. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/lint/ast_rules.py +40 -27
  44. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/lint/base.py +4 -5
  45. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/lint/runner.py +13 -3
  46. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/lint/runtime_rules.py +56 -43
  47. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/model/__init__.py +41 -17
  48. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/model/errors.py +12 -0
  49. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/model/narration.py +3 -5
  50. pytest_given-0.3.0/src/pytest_given/model/runtime.py +67 -0
  51. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/model/schema.py +49 -24
  52. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/model/serde.py +77 -53
  53. pytest_given-0.3.0/src/pytest_given/model/text.py +50 -0
  54. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/plugin/fixtures.py +8 -11
  55. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/plugin/options.py +77 -45
  56. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/plugin/runtest.py +4 -28
  57. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/plugin/session.py +72 -8
  58. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/plugin/state.py +11 -8
  59. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/__init__.py +19 -7
  60. pytest_given-0.3.0/src/pytest_given/report/coverage.py +166 -0
  61. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/glossary_view.py +39 -34
  62. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/html_renderer.py +93 -51
  63. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/md_renderer.py +3 -2
  64. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/palette.py +26 -6
  65. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/sinks.py +51 -13
  66. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/slugs.py +21 -6
  67. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/source_link.py +11 -1
  68. pytest_given-0.3.0/src/pytest_given/report/story_view.py +170 -0
  69. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/templates/app.js +190 -153
  70. pytest_given-0.3.0/src/pytest_given/report/templates/fonts/source-code-pro-latin-wght-normal.woff2 +0 -0
  71. pytest_given-0.3.0/src/pytest_given/report/templates/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  72. pytest_given-0.3.0/src/pytest_given/report/templates/logo.svg +1 -0
  73. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/templates/report.html.j2 +121 -57
  74. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/templates/styles.css +313 -222
  75. pytest_given-0.3.0/src/pytest_given/report/text.py +8 -0
  76. pytest_given-0.3.0/src/pytest_given/report/theme.py +30 -0
  77. pytest_given-0.3.0/tests/conftest.py +35 -0
  78. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/integration/test_cli.py +40 -0
  79. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/integration/test_plugin.py +633 -126
  80. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/integration/test_plugin_file_glossary.py +4 -4
  81. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/integration/test_plugin_lint.py +56 -9
  82. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/ubiquitous_language.py +38 -18
  83. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_collector.py +99 -15
  84. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_discovery.py +13 -18
  85. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_file_glossary.py +14 -12
  86. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_glossary.py +43 -27
  87. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_kind_inference.py +75 -35
  88. pytest_given-0.3.0/tests/unit/capture/test_rootdir_isolation.py +17 -0
  89. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_source.py +11 -2
  90. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_step_descriptor.py +185 -169
  91. pytest_given-0.3.0/tests/unit/capture/test_story.py +871 -0
  92. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_template.py +39 -31
  93. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/lint/test_ast_rules.py +27 -11
  94. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/lint/test_config.py +26 -13
  95. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/lint/test_runtime_rules.py +60 -21
  96. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/lint/test_summary.py +2 -8
  97. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/model/test_schema.py +73 -70
  98. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/model/test_serde.py +106 -70
  99. pytest_given-0.3.0/tests/unit/model/test_text.py +22 -0
  100. pytest_given-0.3.0/tests/unit/report/test_coverage.py +624 -0
  101. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/report/test_glossary_view.py +91 -78
  102. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/report/test_html_renderer.py +180 -86
  103. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/report/test_md_renderer.py +16 -0
  104. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/report/test_palette.py +56 -8
  105. pytest_given-0.3.0/tests/unit/report/test_sinks.py +226 -0
  106. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/report/test_slugs.py +28 -0
  107. pytest_given-0.3.0/tests/unit/report/test_story_view.py +462 -0
  108. pytest_given-0.3.0/tests/unit/report/test_theme.py +22 -0
  109. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/test_grouping.py +65 -48
  110. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/test_percase.py +4 -2
  111. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/test_plugin.py +6 -16
  112. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/test_skills_data.py +14 -8
  113. pytest_given-0.3.0/tests/unit/test_skills_scripts.py +100 -0
  114. {pytest_given-0.2.0 → pytest_given-0.3.0}/uv.lock +197 -1
  115. pytest_given-0.2.0/PKG-INFO +0 -542
  116. pytest_given-0.2.0/README.md +0 -514
  117. pytest_given-0.2.0/THIRD-PARTY-LICENSES +0 -26
  118. pytest_given-0.2.0/src/pytest_given/capture/story.py +0 -306
  119. pytest_given-0.2.0/src/pytest_given/lint/__init__.py +0 -31
  120. pytest_given-0.2.0/src/pytest_given/model/runtime.py +0 -38
  121. pytest_given-0.2.0/src/pytest_given/model/text.py +0 -54
  122. pytest_given-0.2.0/src/pytest_given/report/coverage.py +0 -222
  123. pytest_given-0.2.0/src/pytest_given/report/story_view.py +0 -137
  124. pytest_given-0.2.0/src/pytest_given/skills_data/pytest-given-authoring/references/stories.md +0 -64
  125. pytest_given-0.2.0/src/pytest_given/skills_data/pytest-given-reviewing/references/story-coverage.md +0 -45
  126. pytest_given-0.2.0/tests/conftest.py +0 -3
  127. pytest_given-0.2.0/tests/unit/capture/test_story.py +0 -775
  128. pytest_given-0.2.0/tests/unit/report/test_coverage.py +0 -579
  129. pytest_given-0.2.0/tests/unit/report/test_sinks.py +0 -70
  130. pytest_given-0.2.0/tests/unit/report/test_story_view.py +0 -366
  131. {pytest_given-0.2.0 → pytest_given-0.3.0}/LICENSE.md +0 -0
  132. {pytest_given-0.2.0 → pytest_given-0.3.0}/conftest.py +0 -0
  133. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/markdown_glossary.py +0 -0
  134. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/capture/traceback.py +0 -0
  135. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/grouping/__init__.py +0 -0
  136. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/grouping/context.py +0 -0
  137. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/grouping/group.py +0 -0
  138. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/grouping/percase.py +0 -0
  139. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/lint/config.py +0 -0
  140. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/lint/summary.py +0 -0
  141. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/model/steps.py +0 -0
  142. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/plugin/__init__.py +0 -0
  143. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/plugin/collection.py +0 -0
  144. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/py.typed +0 -0
  145. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/inline_markdown.py +0 -0
  146. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/templates/_macros.html.j2 +0 -0
  147. {pytest_given-0.2.0 → pytest_given-0.3.0}/src/pytest_given/report/templates/alpine.min.js +0 -0
  148. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/__init__.py +0 -0
  149. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/integration/__init__.py +0 -0
  150. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/integration/test_plugin_session_isolation.py +0 -0
  151. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/integration/test_skills_cli.py +0 -0
  152. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/__init__.py +0 -0
  153. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/__init__.py +0 -0
  154. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_markdown_glossary.py +0 -0
  155. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/capture/test_traceback_parser.py +0 -0
  156. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/lint/__init__.py +0 -0
  157. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/model/__init__.py +0 -0
  158. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/model/test_errors.py +0 -0
  159. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/model/test_narration.py +0 -0
  160. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/model/test_steps.py +0 -0
  161. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/report/__init__.py +0 -0
  162. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/report/test_inline_markdown.py +0 -0
  163. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/report/test_source_link.py +0 -0
  164. {pytest_given-0.2.0 → pytest_given-0.3.0}/tests/unit/test_plugin_traceback.py +0 -0
@@ -21,3 +21,11 @@ docs/superpowers/plans/
21
21
  .claude/settings.local.json
22
22
  .playwright-mcp/
23
23
  .mcp.json
24
+
25
+ # Documentation site: build output and the copies docs_build stages into docs_dir
26
+ /site/
27
+ /docs/site/examples/
28
+ /docs/site/assets/pytest-given-diagram-full.svg
29
+ /docs/site/assets/fonts/source-sans-3-latin-wght-normal.woff2
30
+ /docs/site/assets/fonts/source-code-pro-latin-wght-normal.woff2
31
+ /docs/site/assets/logo.svg
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Project overview
4
4
 
5
- See [README.md](README.md) for the user-facing overview, public API, and CLI flags. The rest of this document is contributor-facing.
5
+ See [README.md](README.md) for the pitch and quick start, and the [documentation site](https://nwilbert.github.io/pytest-given/latest/) (sources under `docs/site/`) for the public API and CLI flags. The rest of this document is contributor-facing.
6
6
 
7
7
  ## Setup
8
8
 
@@ -16,7 +16,7 @@ uv sync --group dev
16
16
 
17
17
  ## Quality gates
18
18
 
19
- `uv run nox` runs the default gate — `format`, `lint`, `mypy`, `test`, `coverage` (a 100% target), `audit` (a `pip-audit` of the locked dependencies). The sessions below are on-demand; list them all with `uv run nox -l`.
19
+ `uv run nox` runs the default gate — `format`, `lint`, `mypy`, `test`, `coverage` (a 100% target), `audit` (a `pip-audit` of the locked dependencies). **Run it (or at minimum `uv run nox -s format lint mypy test`) before every commit.** The sessions below are on-demand; list them all with `uv run nox -l`.
20
20
 
21
21
  - `uv run nox -s examples` regenerates the JSON, HTML, and Markdown files under `examples/coffeeshop/`, `examples/hotel-booking/`, and `examples/file-glossary-booking/`. Run after changes to the renderer, templates, plugin output schema, or any example test file, and commit the updated outputs.
22
22
  - `uv run nox -s self_report` regenerates `examples/self-report/` — pytest-given applied to its own backend tests (see [Writing self-report scenarios](#writing-self-report-scenarios)). Run after decorating more tests or changing decorated ones, and commit the updated outputs.
@@ -24,15 +24,14 @@ uv sync --group dev
24
24
  - Every regeneration rewrites `commit_sha` (to current HEAD, including the SHA-pinned source-link URLs), `timestamp`, and `duration_ms` in the JSON and HTML, so a report your change didn't really touch still shows a diff — `git checkout` those files rather than committing the noise.
25
25
  - **Read the `.md` diff first**: the Markdown carries none of those fields, so it is the behavioral delta of your change in prose. An unchanged `.md` doesn't by itself prove the JSON/HTML are noise-only (glossary and story data never surface in the Markdown); a shifted source line does show up, in the `relpath:line::test_name` anchor under every heading.
26
26
  - Regenerate only the reports a change can affect — `examples` narrates `examples/**`, `self_report` narrates `tests/**`. A shifted line number in a decorated backend test is therefore a real self-report change worth committing, even when no example changed.
27
- - Both regeneration sessions run the narration lint (`--given-lint`; see [Narration lint](README.md#narration-lint) and the [design spec](docs/specs/2026-07-05-narration-lint-design.md)): in `self_report` the backend suite has no intentional failures, so an error finding **fails report regeneration** — a real gate. The `examples` session's intentional failures already return a tolerated exit 1 (`success_codes=[0, 1]`) that masks the lint exit code; there the printed "narration lint" summary is the signal. Keep the backend suite lint-clean; a step the lint mis-flags belongs on the `given_lint_ignore` list, whose entries must each suppress a finding (stale entries fail the run). The rule catalog and the ignore mechanics live in the [authoring skill](src/pytest_given/skills_data/pytest-given-authoring/references/scenarios.md) under "Mechanical counterparts"; the honest-two-phase test an ignored `missing-phase` has to pass is under "Phase structure" in the same file.
27
+ - Both regeneration sessions run the narration lint (`--given-lint`; see [Narration lint](docs/site/configuration/narration-lint.md) and the [design spec](docs/specs/2026-07-05-narration-lint-design.md)). `self_report` fails on any lint error — a real gate, keep the backend suite lint-clean. `examples` tolerates exit 1 for its intentional failures, which masks the lint exit code, so read the printed "narration lint" summary there. A step the lint mis-flags goes on the `given_lint_ignore` list (stale entries fail the run); the rule catalog and ignore mechanics are in the [authoring skill](src/pytest_given/.agents/skills/pytest-given-authoring/references/scenarios.md) under "Mechanical counterparts", and the honest-two-phase test an ignored `missing-phase` has to pass under "Phase structure".
28
28
  - `uv run nox -s benchmark` generates the large-scenarios suite and renders its JSON + HTML into `benchmarks/` (gitignored). Run it when a change could move report-generation cost; `benchmarks/bench.py` does size sweeps and cProfile runs directly.
29
- - `uv run nox -s build` builds the wheel + sdist and verifies them the way a consumer would: it checks the wheel carries `py.typed`, the report templates and the bundled skills, then installs it into a throwaway environment and runs a real scenario through it. The in-repo suite imports from `src/`, so it cannot see a packaging regression — this session is the only thing that can. CI runs it on every push; the release workflow runs the same session.
29
+ - `uv run nox -s build` builds the wheel + sdist and verifies them as a consumer would — `py.typed`, templates and bundled skills present, a real scenario run from a throwaway install, and [library-skills](https://library-skills.io) discovery of every bundled skill. The in-repo suite imports from `src/`, so this session is the only thing that catches a packaging regression. CI and the release workflow both run it.
30
+ - `uv run nox -s docs_build` builds the documentation site under `site/` with `zensical build --strict`, after copying the four example reports, the full-size diagram and the report package's two upright fonts into gitignored spots under `docs/site/` (Zensical builds every file in `docs/site/` and cannot exclude any, so those copies are how single-sourced files get in; `CHANGELOG.md` instead reaches the site through a `pymdownx.snippets` include in `docs/site/changelog.md`, between its `site` markers, and the Home-page diagram is inlined the same way from `docs/site/assets/pytest-given-diagram.svg` so page CSS can theme it). The README shows the same diagram as `docs/pytest-given-diagram.svg`, a derived copy with the font embedded and the colour tokens resolved, which the Home page also links to as its full-size view: after editing the source SVG or its tokens, run `uv run nox -s diagram` and commit the result. Run it after changing anything under `docs/site/`; a broken cross-page link fails it. Preview with `uv run --group docs zensical serve` afterwards. The site self-hosts its fonts (`docs/site/assets/fonts/`, latin subsets from Fontsource, OFL notices in `LICENSES.txt` there; the upright faces are the report package's own under `src/pytest_given/report/templates/fonts/`, staged in by the build, only the italics live in the site tree) so a visitor's browser contacts no third party — keep `font = false` in `zensical.toml` and don't `@import` a font CDN; a new face gets its `.woff2` and copyright line added there. `docs_deploy` is the CI-only counterpart that publishes with mike — see [docs/releasing.md](docs/releasing.md#documentation-site).
30
31
 
31
32
  ## Releasing
32
33
 
33
- Releases go to PyPI via a manually dispatched [Release workflow](.github/workflows/release.yml), authenticated with Trusted Publishing (no tokens anywhere) and always rehearsed on TestPyPI first. The step-by-step checklist lives in [docs/releasing.md](docs/releasing.md).
34
-
35
- The short version: bump `version` in `pyproject.toml` and add a matching `## [x.y.z]` section to `CHANGELOG.md`, land it on `main` (a PR is optional — CI gates direct pushes too), dispatch with `testpypi`, run `uv run nox -s check_release -- testpypi`, then dispatch with `pypi` and re-check with `uv run nox -s check_release`.
34
+ Releases go to PyPI via a manually dispatched [Release workflow](.github/workflows/release.yml), authenticated with Trusted Publishing (no tokens anywhere) and always rehearsed on TestPyPI first. The step-by-step checklist, including the version bump and CHANGELOG section it needs, lives in [docs/releasing.md](docs/releasing.md).
36
35
 
37
36
  ## Architecture
38
37
 
@@ -47,9 +46,7 @@ plugin/ cli/ the entry points; may import all five, and hold
47
46
  nothing the five could
48
47
  ```
49
48
 
50
- **Every module has a docstring saying what it is for and why it is shaped that
51
- way.** That is where the detail lives, and it stays true because it sits next
52
- to the code — read it before changing a module. What no filename tells you:
49
+ What no filename tells you:
53
50
 
54
51
  - `grouping/` — the parametrize pass: a scenario's cases collapsed into one
55
52
  narrated tree plus a parameter table, refusing the authoring forms that would
@@ -64,43 +61,44 @@ to the code — read it before changing a module. What no filename tells you:
64
61
  report carries and serde rebuilds, and `capture/glossary.py` subclasses it
65
62
  with the registration API, which needs a caller source location that the leaf
66
63
  may not reach for. `pytest_given.Glossary` is the subclass; everything
67
- internal annotates the base.
64
+ internal annotates the base. `LookupGlossary` sits between them and owns the
65
+ `g['Guest']` read-back both user-facing glossaries share.
68
66
  - Each package exposes the *whole* job, not its parts: `report.emit_sinks`
69
67
  (render → write → discard-on-failure, so a failure leaves no half-written
70
- report) and `lint.run_lint`. Both entry points go through them, which is what
71
- keeps `pytest-given report` behaving like the plugin.
68
+ report) and `lint.run_lint`. Both entry points reach the sinks through
69
+ `emit_sinks`, which is what keeps `pytest-given report` behaving like the
70
+ plugin; the lint is the plugin's alone, since the CLI re-renders a report
71
+ rather than running a suite.
72
72
 
73
73
  `tests/` splits `unit/` (no pytest session needed) from `integration/`, which drives the plugin end to end through `pytester` inner runs (enabled by the root `conftest.py`). Narration written inside an inner run belongs to *that* run's collector — only the outer, decorated test reaches the self-report.
74
74
 
75
- The public API is re-exported from `__init__.py` and documented in the skill's
76
- [references/api.md](src/pytest_given/skills_data/pytest-given-authoring/references/api.md).
77
-
78
- ### Step text & placeholders
79
-
80
- The authoring forms (t-string vs `Template` vs plain string, and where each is rejected) are documented in the skill's [references/api.md](src/pytest_given/skills_data/pytest-given-authoring/references/api.md); design rationale in the [design spec](docs/specs/2026-05-23-structured-step-text-design.md).
75
+ The public API, including the step-text authoring forms (t-string vs `Template` vs plain string), is re-exported from `__init__.py` and documented in the skill's [references/api.md](src/pytest_given/.agents/skills/pytest-given-authoring/references/api.md).
81
76
 
82
77
  ## Handling report output
83
78
 
84
- Outputs are opt-in; a bare `uv run pytest` writes nothing. The workflow for reading a run's narration (`--given-md`), querying the JSON report with `jq` by tag/term/status, re-rendering a saved run, and the bare-flag-order trap lives in the [navigating skill](src/pytest_given/skills_data/pytest-given-navigating/SKILL.md).
79
+ Outputs are opt-in; a bare `uv run pytest` writes nothing. The workflow for reading a run's narration (`--given-md`), querying the JSON report with `jq` by tag/term/status, re-rendering a saved run, and the bare-flag-order trap lives in the [navigating skill](src/pytest_given/.agents/skills/pytest-given-navigating/SKILL.md).
85
80
 
86
81
  ## Report testing
87
82
 
88
- Any change to `report/templates/` (Jinja, CSS, `app.js`) or the `narration` filter in `html_renderer.py` **must** be Playwright-verified before commit — Python-side regex tests on rendered HTML do not catch broken Alpine expressions, malformed `:class` bindings, or other runtime browser issues (the substring matches even when the attribute is unparseable). Open e.g. `examples/coffeeshop/coffeeshop.html` (regenerate via `uv run nox -s examples`) with the Playwright MCP server, check `browser_console_messages` for errors after init, then drive the changed surface (hover, click, URL hash). Use `browser_snapshot` (not screenshots) to read page content and interact with elements.
83
+ Any change to `report/templates/` (Jinja, CSS, `app.js`) or the `narration` filter in `html_renderer.py` **must** be Playwright-verified before commit. The project has no JS-side UI tests, and Python tests on rendered HTML cannot stand in: a substring match passes on a broken Alpine expression or a malformed `:class` binding. So:
89
84
 
90
- - **Don't write Python tests that pin frontend markup** (specific class names, wrapper structure, inline-handler shape, SVG strings). They check implementation details, not behavior, and rot the moment the renderer is refactored. The project has no JS-side UI tests; Playwright is the only verification for frontend concerns. Python tests stay on the renderer's data-shaped contract (what `data-param` value, which scenario IDs, which counts) — not on how the markup is assembled.
91
- - **Don't TDD frontend changes** for the same reason: a failing markup assertion isn't proving the bug exists in the browser, and a passing one isn't proving the fix works. Apply the change, regenerate `examples/`, drive it in Playwright, capture the result.
92
-
93
- - The report targets desktop only — assume a minimum viewport width of ~900px. No mobile/responsive layout needed.
85
+ - **No Python tests that pin frontend markup** (class names, wrapper structure, inline-handler shape, SVG strings) — they check implementation, not behavior, and rot on every renderer refactor. Python tests stay on the renderer's data-shaped contract (which `data-param` value, which scenario IDs, which counts).
86
+ - **No TDD for frontend changes**: apply the change, regenerate `examples/`, drive it in Playwright, capture the result.
87
+ - Open e.g. `examples/coffeeshop/coffeeshop.html` (regenerate via `uv run nox -s examples`) with the Playwright MCP server, check `browser_console_messages` for errors after init, then drive the changed surface (hover, click, URL hash). Use `browser_snapshot` (not screenshots) to read page content and interact with elements.
88
+ - Desktop only — assume a minimum viewport width of ~900px.
94
89
  - Traceback display and header metadata formatting are known limitations, not current priorities.
95
- - Never save Playwright screenshots into the project directory. Use `/tmp/` or omit the `filename` parameter.
90
+ - Never save Playwright screenshots into the project directory — use the session scratchpad or omit the `filename` parameter.
96
91
 
97
92
  **Setup and known traps** (`.mcp.json`, the `file://` page cache, browser installs) live in [docs/playwright-setup.md](docs/playwright-setup.md). `.mcp.json` is read at **session start**, so check that the `browser_*` tools exist before planning a task that ends in Playwright verification.
98
93
 
99
94
  ## Writing self-report scenarios
100
95
 
101
- The narration rules live in the **`pytest-given-authoring` skill** — whose canonical source is [src/pytest_given/skills_data/](src/pytest_given/skills_data/pytest-given-authoring/SKILL.md) — every link in this document points there. Contributor agents auto-discover the mirrored copy under `.claude/skills/`, and downstream projects get it via `pytest-given skills install`. After editing the canonical copy, regenerate the committed copy with `uv run pytest-given skills install` and commit both (a sync test fails otherwise).
96
+ The narration rules live in the **`pytest-given-authoring` skill**, whose canonical source is [src/pytest_given/.agents/skills/](src/pytest_given/.agents/skills/pytest-given-authoring/SKILL.md) — every link in this document points there. The copy under `.claude/skills/` is a mirror: after editing the canonical copy, regenerate it with `uv run pytest-given skills install` and commit both (a sync test fails otherwise).
97
+
98
+ **The skill is documentation with the same sync duty as the site** — downstream agents read it instead of the site, version-matched from the wheel. A change to the public API surface or its rules updates the site page under `docs/site/` *and* [references/api.md](src/pytest_given/.agents/skills/pytest-given-authoring/references/api.md); a change to narration/lint semantics updates [references/scenarios.md](src/pytest_given/.agents/skills/pytest-given-authoring/references/scenarios.md) and friends. No check catches prose drift between the site and the skill, so ask "does the skill need this too?" on every user-facing change; only the `python` blocks are covered — `tests/unit/test_skills_scripts.py` runs each against a report built from the model. The reviewing skill restates authoring rules as rubrics, so they share the same duty: its layer 2 mirrors `scenarios.md` ("Keeping it truthful", "Expected raises", "Vocabulary and tags"), and its layer 4 mirrors `glossaries.md` ("Keeping the glossary honest") and `stories.md` — change one, change both.
102
99
 
103
- **The skill is documentation with the same sync duty as the README.** A change to the public API surface or its rules updates the README *and* the skill's [references/api.md](src/pytest_given/skills_data/pytest-given-authoring/references/api.md) (which downstream agents rely on instead of the README — it ships in the wheel, version-matched); a change to narration/lint semantics updates [references/scenarios.md](src/pytest_given/skills_data/pytest-given-authoring/references/scenarios.md) and friends. No mechanical check catches content drift between README and skill — treat "does the skill need this too?" as part of every user-facing change.
100
+ Narration drifts like any other prose — the lint catches several ways step text and body can part company, not all of them — which is why [Quality gates](#quality-gates) has you read the regenerated `.md` rather than trust the text.
101
+ Before merging changes to narrated tests, run the `pytest-given-reviewing` skill: it layers the lint, a semantic audit of step text against step bodies, and a hygiene pass over glossary, tags and stories.
104
102
 
105
103
  What is specific to this repo's self-report:
106
104
 
@@ -109,7 +107,8 @@ What is specific to this repo's self-report:
109
107
 
110
108
  ## Conventions
111
109
 
112
- - Use the canonical vocabulary from [GLOSSARY.md](GLOSSARY.md) in prose as well as code — docs, skill references, and specs say the official term (`term ref`, not a paraphrase like "narrated term"). Naming and rename mechanics live in the skill's [references/glossaries.md](src/pytest_given/skills_data/pytest-given-authoring/references/glossaries.md); here, a rename lands in one commit (glossary row, `pg\[` references, implementation naming) plus a regenerated `uv run nox -s self_report`. Adding a term is safe, but still regenerate.
110
+ - Use the canonical vocabulary from [GLOSSARY.md](GLOSSARY.md) in prose as well as code — docs, skill references, and specs say the official term (`term ref`, not a paraphrase like "narrated term"). Naming and rename mechanics live in the skill's [references/glossaries.md](src/pytest_given/.agents/skills/pytest-given-authoring/references/glossaries.md); here, a rename lands in one commit (glossary row, `pg\[` references, implementation naming) plus a regenerated `uv run nox -s self_report`. Adding a term is safe, but still regenerate.
111
+ - **Docstrings only for what the code cannot say** — a non-obvious invariant, a constraint that makes the shape necessary, a trap for the next reader — and a line or two at most. No signature restatement, no implementation narration, no `X is the only caller of Y` cross-references (they rot fastest; the reader can grep). Prefer a precise name to a sentence explaining a vague one. When you change a module, delete the sentences that have stopped being true rather than repairing them.
113
112
  - Avoid `Any` — use precise types, generics, `TYPE_CHECKING` imports, or `ContextVar[T]` over untyped `threading.local`.
114
113
  - Use `NewType` for domain-specific IDs (e.g., `NodeId`) and PEP 695 `type` statements for aliases. Avoid raw complex types like `dict[str, tuple[list[str], list[Any]]]` — introduce named types instead.
115
114
  - Only module-level imports — no inline/function-level imports.
@@ -117,12 +116,12 @@ What is specific to this repo's self-report:
117
116
  - Cross-platform: plugin and tests must pass on native Windows, macOS, Linux, and WSL (Linux interpreter over a `/mnt/<drive>` Windows checkout). Never hardcode a path separator or assume POSIX semantics — go through `pathlib`, `as_posix()` for stored/serialized paths, resolve before comparing. Path-form folding is confined to `capture/source.py` (mechanics in its docstrings; the why, and how to run one checkout from both Windows and WSL without the venvs colliding, in [docs/wsl-development.md](docs/wsl-development.md)). Only tests asserting WSL `/mnt`-absolute behavior may `skipif(sys.platform == 'win32', …)` — native-Windows pathlib reads `/mnt/<drive>` as drive-relative, not absolute — everything else passes on all four targets.
118
117
  - Relative imports inside the package throughout — `from .schema import Scenario` for siblings, `from ..model import Scenario` across subpackages (always through the subpackage root, never into its submodules). Tests use absolute imports and may reach into any internal path. The dependency direction those imports must respect is under [Architecture](#architecture).
119
118
  - Prefer `assert` over `# pragma: no cover` for invariant guards. Asserts document the invariant and fail loudly if violated; pragmas hide the line and silently bail. Reserve `# pragma: no cover` for code that genuinely cannot be exercised by a test (e.g. `if __name__ == '__main__':` script entry).
119
+ - JSON reports written by older pytest-given versions are not supported: `pytest-given report` may drop or misread their fields. Don't add readers, migrations, or stale-shape errors for them.
120
120
  - Step-down rule: callers before callees, public before private. Read each file top-down from high-level API to implementation details.
121
- - TDD: write tests first
121
+ - TDD: write tests first (except frontend changes — see [Report testing](#report-testing)).
122
122
  - Commit messages: single line, no co-author trailers, no leading file/area labels like `TODO:` or `README:` — just describe the change ("note example cleanup as todo", not "TODO: note example cleanup"). Conventional-commit-style scope prefixes like `docs:` / `examples:` / `renderer:` are fine when they add information.
123
123
  - Keep commits coherent: each commit should represent one logical change. Don't split "do X", "tests for X", and "review-fixup for X" into separate commits — squash them before pushing. Don't bundle unrelated changes either.
124
- - **A user-facing change adds its `CHANGELOG.md` entry in the same commit**, under `## [Unreleased]`, in the fitting Keep a Changelog category (each category appears at most once per version — extend the existing heading rather than adding a second one). User-facing = public API, CLI flags, report output, lint rules, bundled skills; internal work (refactors, tests, CI, contributor docs) gets no entry. Release-time version bumps live under [Releasing](#releasing).
125
- - **One sentence per entry**, written for someone upgrading the package: name the symbol, flag, or surface, and say what changed. Only a breaking change earns more — the migration it needs. Cut the rest: rationale, measurements, before/after detail, and anything the reader would discover the moment they look at the thing. Visual and interaction polish is worth mentioning but not itemizing: give it one short collective bullet per release ("the sidebar and its chips are visually tidied"), never a bullet per restyled element. Accessibility fixes are the exception — they stay on their own line, since they change who can use the thing. If a change isn't worth an upgrader's attention at all, it gets none. When in doubt, the shorter entry is the right one.
124
+ - **A user-facing change adds its `CHANGELOG.md` entry in the same commit**, under `## [Unreleased]`, in the fitting Keep a Changelog category (each category appears at most once per version — extend the existing heading rather than adding a second one). User-facing = public API, CLI flags, report output, lint rules, bundled skills; internal work (refactors, tests, CI, contributor docs) gets no entry.
125
+ - **One sentence per entry**, written for someone upgrading: name the symbol, flag, or surface and say what changed — no rationale, measurements, or before/after detail. Only a breaking change earns more: the migration it needs. Visual and interaction polish gets one collective bullet per release ("the sidebar and its chips are visually tidied"), never a bullet per restyled element; accessibility fixes stay on their own line. When in doubt, the shorter entry is the right one.
126
126
  - Plan files under `docs/superpowers/plans/` are scratch artifacts — never commit them. Spec files under `docs/specs/` are committed.
127
127
  - New specs land under `docs/specs/proposed/`. When a spec's implementation lands, `git mv` it up one level into `docs/specs/` in the same commit, and fix its relative links in the same edit — a `../`-prefixed link to a sibling spec resolves into `docs/` once the file moves. `ls docs/specs/proposed` is the canonical list of outstanding design work.
128
- - Always run `uv run nox` (or at minimum `uv run nox -s format lint mypy test`) before committing
@@ -9,6 +9,53 @@ The release workflow reads the section matching the version in `pyproject.toml`
9
9
  and uses it as the GitHub Release body, so each version needs a heading of the
10
10
  form `## [x.y.z] - YYYY-MM-DD`.
11
11
 
12
+ <!-- --8<-- [start:site] -->
13
+
14
+ ## [0.3.0] - 2026-09-27
15
+
16
+ ### Added
17
+
18
+ - A documentation site at <https://nwilbert.github.io/pytest-given/> has a user guide, configuration and CLI reference, and the example reports. The bundled skills link to it, and the CLI help names the page on source-link templates.
19
+ - The HTML report has a dark theme, with a Light / Dark / System control in its header that each browser remembers.
20
+ - `--given-theme` / `given_theme` (and `--theme` on `pytest-given report`) set whether the HTML report opens light, dark, or following the viewer's system.
21
+ - Tags can be organized hierarchically: a `/` in a tag nests it in the HTML report's Tags sidebar (`ticket/ABC-123` goes under a `ticket` heading), and selecting the heading filters to every tag beneath it.
22
+ - Projects can also install the bundled skills with [library-skills](https://library-skills.io) (`uvx library-skills install --claude`), alongside the skills of their other dependencies.
23
+ - One scenario can document a flow that spans several stories: `@scenario(stories=[a, b])` lists it under each story in the Stories tab, and the Scenarios view's sentence filter names the story when a report has several.
24
+ - A sentence can be named (`sentence(..., name='checkout')`), so a pin can refer to it by name and survives reordering the story. The Stories timeline shows the name beside the sentence's coverage.
25
+
26
+ ### Changed
27
+
28
+ - **Breaking.** Story vocabulary follows Domain Storytelling: a story is made of *sentences*, and *activity* is the verb kind. `activity()` is now `sentence()`, `path()` is now `clause()`, and `Glossary.verb()` is now `Glossary.activity()`. A file glossary's kind column says `activity` instead of `verb`, the report's `#activity-filter=` link parameter is now `#sentence-filter=`, and the HTML report says *Sentence* and *Activities* where it said *Activity* and *Verbs*.
29
+ - **Breaking.** `@scenario(story=)` is now `stories=`.
30
+ - **Breaking.** Pins refer to sentences by handle under `pins=` instead of by number: `given(..., activity=3)` becomes `given(..., pins=the_story[3])`, and `@scenario(activities=[2, 3])` becomes `pins=[the_story[2], the_story[3]]`. `activity_id=` is gone, because sentences are numbered by position; to pin a sentence independently of its position, name it and pin `the_story['name']`. A pin no longer requires the scenario to bind the pinned sentence's story.
31
+ - **Breaking.** A scenario's `pins=` now sets its coverage outright: it covers exactly those sentences plus its steps' pins, with no narration matching. `activities=` only narrowed which sentences narration could cover. Pass `pins=[]` to a step or scenario to turn off narration matching without pinning anything.
32
+ - The HTML report is restyled. It embeds Source Sans 3 and Source Code Pro, so it looks the same on every machine and offline. Lists share one surface instead of a card per row, Given/When/Then sit in a gutter beside the steps, and sidebar labels and counts use sentence case. Step narration and story sentences are set larger and darker than the surrounding labels and counts, at the same line spacing, so a scenario takes no more room.
33
+ - The HTML report strips source comments from its inlined stylesheet and script, which saves about 29 KB per report and offsets part of the size the embedded fonts add.
34
+ - Grouping errors for varying attachment labels and varying `str` narration name the parametrize case that differs. The varying-`str` error also suggests `group_parametrized=False` as a way out.
35
+ - A story that no scenario covers now shows up in the report as uncovered, where before it was missing. The one exception is a story declared before the session started, such as one in a module still imported from an earlier `pytest.main()` in the same process: it is left out with a warning.
36
+ - Story coverage ignores instances: a step narrating `guest`, or any guest instance, now covers a sentence naming `guest('Alice')`. Sentences that differ only by instance can be told apart only with a pin.
37
+ - The authoring skill advises writing generic verbs (*searches for*, *adds*) as plain strings in sentences rather than glossary terms, keeping the glossary to domain vocabulary. The hotel-booking example follows it.
38
+ - The authoring skill explains how coverage matching works (instances are ignored, and two sentences whose terms nest always cover together) and how to check coverage in the JSON report. The reviewing skill checks coverage from the report instead of re-deriving it.
39
+ - The reviewing skill can list each scenario's narration beside its test's source for side-by-side review, and checks that a scenario demonstrates every rule the project's changelog announces. Both skills flag `match=` pins that use alternation, and a `when` that narrates setup while its body acts.
40
+ - The authoring, reviewing and navigating skills cover sentence handles, `pins=` and `stories=`. The reviewing skill also checks that the test body exercises each sentence its scenario pins.
41
+
42
+ ### Fixed
43
+
44
+ - When rendering a report fails unexpectedly, the previous run's report is still discarded instead of staying on disk looking current.
45
+ - A `#scenario=` deep link now opens the right scenario when two node ids differ only in a character the slug folds, and no longer breaks the URL.
46
+ - `pytest-given report` reports a non-UTF-8 input file as an error instead of crashing with a traceback.
47
+ - An unknown `--given-source-link` preset is reported under the flag the user typed, not under the `given_source_link` ini name.
48
+ - A refused scenario on a run with no `--given-*` sink no longer reports itself under a "report not written" heading.
49
+ - `when_then(...)` rejects a `Template` narration in a test body, as `given`/`when`/`then` already do.
50
+ - The Glossary view no longer lists a term's own name in another case (`guest.low`) under Instances.
51
+ - On native Windows, stories and glossary terms now record where they are declared, so the lint rules that depend on it run there too.
52
+ - `tag-shadows-term` now catches a tag that collides with a term id through a non-ASCII character that lowercases into ASCII.
53
+ - A `dead-term` finding's message states the rule's actual criterion, including that a term ref in a `@scenario` name keeps a term alive.
54
+ - The navigating skill shows the failure messages of a parametrized scenario's cases, where it printed an empty message. It also starts from a committed or CI-published report when one exists, instead of always rerunning the suite.
55
+ - The reviewing skill lints the whole suite, where a selection failed on the project's ignore entries as stale, keeps the project's own `given_lint_rules` when enabling `dead-term`, and ranks findings in one explicit order.
56
+ - A pin on an `Annotated` `given(...)` label now takes effect instead of being silently dropped, replacing the pins of the fixture step it relabels (`pins=[]` clears them). A pin on a step fixture's own `@given(...)` label also takes effect now.
57
+ - A pin on a `@given` fixture scoped wider than `function` now counts in every scenario the fixture reaches, even when a test without `@scenario` set it up first. The run used to fail with a bare `AssertionError`.
58
+
12
59
  ## [0.2.0] - 2026-09-04
13
60
 
14
61
  ### Added
@@ -305,6 +352,9 @@ First public release.
305
352
  - Bundled authoring, navigating, and reviewing skills for AI agents, shipped in
306
353
  the wheel and version-matched to the plugin.
307
354
 
308
- [Unreleased]: https://github.com/nwilbert/pytest-given/compare/v0.2.0...HEAD
355
+ [Unreleased]: https://github.com/nwilbert/pytest-given/compare/v0.3.0...HEAD
356
+ [0.3.0]: https://github.com/nwilbert/pytest-given/compare/v0.2.0...v0.3.0
309
357
  [0.2.0]: https://github.com/nwilbert/pytest-given/compare/v0.1.0...v0.2.0
310
358
  [0.1.0]: https://github.com/nwilbert/pytest-given/releases/tag/v0.1.0
359
+
360
+ <!-- --8<-- [end:site] -->
@@ -15,7 +15,7 @@ This glossary covers pytest-given's own bounded context. The terminology a *user
15
15
  | **Narration** | The human-readable text on a step or scenario name. A `Narration` bundles the flat rendered `text` with `parts` — empty for plain-string authoring, and for a t-string or `pytest_given.Template` a list of `NarrationLiteral` / `NarrationValue` / `NarrationPlaceholder` / `NarrationTermRef` pieces. The structured form lets the templatizer and renderer treat parametrize-bound values specially without regex tricks. |
16
16
  | **Phase** | The category of a step: `given`, `when`, or `then`. A step has exactly one phase. |
17
17
  | **when_then** | A step-authoring helper that emits a `when` action and its `then` outcome as two sibling steps from a single `with` block. Used mainly to narrate an expected raise (`with when_then('the action', 'the error is raised'), pytest.raises(...)`), so the action and its outcome stay distinct steps. |
18
- | **Tag** | Free-form string label attached via `@scenario(name, tags=[...])`. Used by the report's filter UI. |
18
+ | **Tag** | Free-form string label attached via `@scenario(name, tags=[...])`. Used by the report's filter UI, where a `/` nests it (`ticket/ABC-123` under `ticket`) and selecting a prefix filters to every tag beneath it. |
19
19
  | **Attachment** | A labeled blob (text or JSON) bound to the currently-active step via `attach(label, content)`. |
20
20
 
21
21
  ## Parametrization
@@ -55,6 +55,7 @@ This glossary covers pytest-given's own bounded context. The terminology a *user
55
55
  | **Parameter coloring** | Each parametrize column gets a stable highlight color; placeholders and matching values share that color wherever they appear in step text and the parameter table. |
56
56
  | **Value highlight** | A neutral highlight applied to t-string interpolation values that don't correspond to a parametrize column and are constant across every case (e.g., a computed expression like `price * 1.2`). One that varies becomes a `derived` column instead. |
57
57
  | **Source link** | A clickable file:line anchor on a scenario card, a story panel, or an expanded glossary term card, resolved from the `given_source_link` config — a preset name like `vscode` / `github`, or a raw URL template. Captured as a `SourceLocation` (POSIX relpath + 1-indexed line) from `pytest.Item.location` for a scenario, from the declaration site for a *Story* or *Term*. Disabled by default. |
58
+ | **Theme** | The HTML report's colour scheme — `light`, `dark`, or `auto` (follow the viewer's system). The `given_theme` config sets the default a report opens in; a viewer's own choice from the report's theme control, once made, wins over it in that browser. |
58
59
 
59
60
  ## Narration lint
60
61
 
@@ -69,27 +70,31 @@ This glossary covers pytest-given's own bounded context. The terminology a *user
69
70
 
70
71
  The Domain-Driven Design layer atop the core surface. All terms here are optional features: a test suite can use none, some, or all.
71
72
 
73
+ Terms follow [Domain Storytelling](https://domainstorytelling.org/quick-start-guide)'s own, where actors and work objects connected by activities form sentences.
74
+
72
75
  | Term | Meaning |
73
76
  |---|---|
74
- | **Glossary** | The Ubiquitous-Language concept — the shared vocabulary a domain speaks in — and the class that realizes it: `Glossary()`, with `.actor(...)`, `.work_object(...)`, `.verb(...)` registration methods and `g('foo')` (declare-or-get a kindless term, optional `definition=`) / `g['foo']` (get-only, raises on unknown) accessor forms. |
75
- | **File glossary** | A glossary loaded from a Markdown file, via the `FileGlossary(path)` class. It parses all GFM pipe tables in the file into the same inner `Glossary` model; terms are accessed by name (`g['Guest']`, case-insensitive). *Kind inference* fills in term kinds post-collection from activity *slot* positions when no explicit `kind_column` is configured. |
77
+ | **Glossary** | The Ubiquitous-Language concept — the shared vocabulary a domain speaks in — and the class that realizes it: `Glossary()`, with `.actor(...)`, `.work_object(...)`, `.activity(...)` registration methods and `g('foo')` (declare-or-get a kindless term, optional `definition=`) / `g['foo']` (get-only, raises on unknown) accessor forms. |
78
+ | **File glossary** | A glossary loaded from a Markdown file, via the `FileGlossary(path)` class. It parses all GFM pipe tables in the file into the same inner `Glossary` model; terms are accessed by name (`g['Guest']`, case-insensitive). *Kind inference* fills in term kinds post-collection from clause *slot* positions when no explicit `kind_column` is configured. |
76
79
  | **Deferred term** | A term handed over before its kind is settled — what `g('foo')` and `g['foo']` on a code glossary return, and every *file glossary* lookup. The handle is the same `TermHandle` the typed registrations (`g.actor(...)` and friends) hand back; `declared_kind is None` is what marks the deferral. The deferral is in the handing over, not the term: a row with an explicit `kind_column` arrives through the same handle already kinded, while the rest stay `None` until *kind inference* runs. |
77
- | **Term** | A registered glossary entry: an Actor, Work Object, Verb, or kindless term. Each carries an id (slug), a canonical name, a kind (`None` when kindless), and an optional definition (`str | None`, `None` when undefined). |
80
+ | **Term** | A registered glossary entry: an Actor, Work Object, Activity, or kindless term. Each carries an id (slug), a canonical name, a kind (`None` when kindless), and an optional definition (`str | None`, `None` when undefined). |
81
+ | **Handle** | The Python object a glossary hands back for a *term* — `TermHandle`, the same type from every accessor (`g.actor(...)`, `g('foo')`, `g['Guest']`, a captured `guest = ...`). It is what steps, scenario titles and sentences interpolate, in one of three surface forms: **bare** (`room` — the canonical display), **`.low`** (`room.low` — lowercased), or **called** (`room('Deluxe Suite')` — an *instance* on an Actor or Work Object, an inflection on an *Activity*). Each form renders as a *term ref*. A story hands out a **sentence handle** the same way: `book_a_room['cancel']` by name or `book_a_room[3]` by number, which `pins=` takes. |
78
82
  | **Actor** | A glossary term for a participant in the domain (e.g., *Guest*). Carries the actor kind color: a wash in narration, a pill in the Glossary view. |
79
83
  | **Work Object** | A glossary term for a thing acted on (e.g., *Room*, *Booking*). Carries the work-object kind color: a wash in narration, a pill in the Glossary view. |
80
- | **Verb** | A glossary term for an action (e.g., *book*, *confirm*). Verbs accept inflections — calling `book('books')` records *books* as a surface form of the canonical *book*. |
81
- | **Term ref** | An occurrence of a term inside narration. Modeled as `NarrationTermRef` in step text and as `ActivityTermRef` inside activity prose. |
84
+ | **Activity** | A glossary term for what an actor does (e.g., *book*, *confirm*), which Domain Storytelling draws as an arrow labelled with a verb. Activities accept inflections: calling `book('books')` records *books* as a surface form of the canonical *book*. |
85
+ | **Term ref** | An occurrence of a term inside narration. Modeled as `NarrationTermRef` in step text and as `ClauseTermRef` inside a clause. |
82
86
  | **Instance** | A named refinement of an Actor or Work Object (e.g., `guest('Alice')` is an instance of the *Guest* actor). Instances aggregate in the Glossary tab's refs block. |
83
- | **Inflection** | A surface form of a Verb other than its canonical name (e.g., *searches for* as an inflection of *search*). Reported under "Also used as:" in the Glossary. |
84
- | **Story** | A named flow modeled as a sequence of activities. Constructed by `story('Title', [activity(...), ...])`. Stories are first-class report tabs and the unit of coverage. |
85
- | **Activity** | One row in a story — typically `actor + verb + work_object` plus optional connective words. Constructed by `activity(...)`. |
86
- | **Activity Part** | The two-variant union making up an activity's prose (`ActivityPart` = `ActivityTermRef` or `ActivityWord`): a reference to a glossary term, whose kind resolves via the glossary, or a bare path word — a node label or edge connective that carries no kind or id, is never classified by inference, and never reaches the glossary. That last is what separates it from a kindless term, which has an id and is tracked. |
87
- | **Path** | A branching segment inside a story — `path(...)` lets alternate activity sequences share a prefix. |
88
- | **Slot** | A position role in an activity path, from its node/edge alternation: position 0 is the actor slot, odd positions are verb slots, and even positions ≥ 2 are noun slots. Slots drive both path validation and *kind inference*. |
89
- | **Scenario↔activity binding** | The link between a scenario (or step) and one or more story activities. Carried by `@scenario(story=, activities=)` and the `activity=` kwarg on `given`/`when`/`then`. |
90
- | **Coverage** | The "did this scenario touch that activity" relation. Computed by the *A_refs ⊆ S* rule: an activity is covered when its set of term references (as identities, with a canonical fallback) is a subset of a single step's term-reference identities — matching is per step, not against the union across steps. A step can also cover an activity explicitly via the `activity=` pin, regardless of its narration or its term count. |
91
- | **Kind inference** | The post-collection pass (`infer_glossary_kinds`) that assigns each undeclared term a kind from the *slot* positions it occupies across all story activities; declared kinds are verified against observed positions instead. A term used in incompatible slots (or conflicting with its declared kind) raises. A term never referenced by any activity stays *kindless*. |
92
- | **Kindless** | A term with `kind=None` — left unset when *kind inference* finds no story *slot* to infer from (a term used only in t-string steps, never in an activity). Shown in the report's *Uncategorized* bucket. |
87
+ | **Inflection** | A surface form of an Activity term other than its canonical name (e.g., *searches for* as an inflection of *search*). Reported under "Also used as:" in the Glossary. |
88
+ | **Story** | A named flow modeled as a sequence of sentences. Constructed by `story('Title', [sentence(...), ...])`. Stories are first-class report tabs and the unit of coverage. |
89
+ | **Sentence** | What Domain Storytelling calls a sentence: an actor, an activity and its work objects, plus connective words. One row in a story, numbered by its position (1..N, never set by hand) and optionally named with `sentence(..., name='cancel')`, a name that stays put when rows are inserted. Constructed by `sentence(...)`. Usually one clause, built implicitly; several when the sentence has several arrows under one number. |
90
+ | **Clause part** | The two-variant union making up a clause's prose (`ClausePart` = `ClauseTermRef` or `ClauseWord`): a reference to a glossary term, whose kind resolves via the glossary, or a bare clause word — a node label or edge connective that carries no kind or id, is never classified by inference, and never reaches the glossary. That last is what separates it from a kindless term, which has an id and is tracked. |
91
+ | **Clause** | One linear walk through a sentence's arrows, a node/edge alternation starting actor → verb → noun, constructed by `clause(...)`. A sentence with several arrow chains under one number, such as an actor handing a work object to two recipients, takes one clause per chain. Domain Storytelling names no such unit. |
92
+ | **Slot** | A position role in a clause, from its node/edge alternation: position 0 is the actor slot, odd positions are verb slots, and even positions ≥ 2 are noun slots. Slots drive both clause validation and *kind inference*. |
93
+ | **Scenario↔sentence binding** | The link between a scenario (or step) and story sentences. `@scenario(stories=...)` names the stories a scenario is narration-matched against; a *pin* (`pins=` on `@scenario` or on `given`/`when`/`then`) binds sentences explicitly, in any story, whether `stories=` names it or not. |
94
+ | **Pin** | What a *sentence handle* passed to `pins=` is recorded as, the way a term handle in narration is recorded as a *term ref*: a `Pin` naming a story and a sentence number. A pin binds explicitly and replaces narration matching — a step pin for that step, a scenario pin (`@scenario(pins=...)`) for every step of the scenario, which then covers its own pins plus its steps'. `pins=[]` pins nothing but still opts out of narration matching. A pin reaches under-anchored sentences too. |
95
+ | **Coverage** | The "did this scenario touch that sentence" relation. Computed by the *A_refs ⊆ S* rule: a sentence is covered when the set of terms it references is a subset of the terms a single step references — matching is per step, not against the union across steps, and on terms, not surface forms: an *instance* or *inflection* counts as its term. A *pin* covers its sentences directly instead, regardless of narration or term count. |
96
+ | **Kind inference** | The post-collection pass (`infer_glossary_kinds`) that assigns each undeclared term a kind from the *slot* positions it occupies across all story sentences; declared kinds are verified against observed positions instead. A term used in incompatible slots (or conflicting with its declared kind) raises. A term never referenced by any sentence stays *kindless*. |
97
+ | **Kindless** | A term with `kind=None` — left unset when *kind inference* finds no story *slot* to infer from (a term used only in t-string steps, never in a sentence). Shown in the report's *Uncategorized* bucket. |
93
98
  | **Undefined** | A term with `definition is None`; surfaced by a badge and filter in the Glossary view. Orthogonal to *kindless*. |
94
99
 
95
100
  ## Collaboration
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.5
2
+ Name: pytest-given
3
+ Version: 0.3.0
4
+ Summary: A pytest plugin that generates interactive HTML reports from Given/When/Then annotated tests.
5
+ Project-URL: Homepage, https://github.com/nwilbert/pytest-given
6
+ Project-URL: Repository, https://github.com/nwilbert/pytest-given
7
+ Project-URL: Issues, https://github.com/nwilbert/pytest-given/issues
8
+ Project-URL: Changelog, https://github.com/nwilbert/pytest-given/blob/main/CHANGELOG.md
9
+ Author-email: Niko Wilbert <mail@nikowilbert.de>
10
+ License-Expression: MIT
11
+ License-File: LICENSE.md
12
+ License-File: THIRD-PARTY-LICENSES
13
+ Keywords: bdd,domain-storytelling,given-when-then,living-documentation,pytest,report,testing,ubiquitous-language
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Framework :: Pytest
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Documentation
22
+ Classifier: Topic :: Software Development :: Testing
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.14
25
+ Requires-Dist: jinja2>=3.1
26
+ Requires-Dist: pytest>=9.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # pytest-given
30
+
31
+ A pytest plugin that turns Given/When/Then annotated tests into interactive HTML reports.
32
+
33
+ **Given** your pytest tests,<br>
34
+ **when** you narrate them with `given` / `when` / `then`,<br>
35
+ **then** documentation and behavior fuse into one report.
36
+
37
+ **What people and agents read is what the code does.**
38
+
39
+ Inspired by [JGiven](https://jgiven.org/) (Java).
40
+
41
+ **Documentation: <https://nwilbert.github.io/pytest-given/latest/>**
42
+
43
+ Live examples:
44
+ - **[Coffeeshop](https://nwilbert.github.io/pytest-given/latest/examples/coffeeshop.html)**: a tour of the core features.
45
+ - **[Hotel booking](https://nwilbert.github.io/pytest-given/latest/examples/hotel-booking.html)**: a glossary and domain stories, with story coverage.
46
+ - **[File glossary](https://nwilbert.github.io/pytest-given/latest/examples/file-glossary-booking.html)**: the same, with the glossary kept in a Markdown file.
47
+ - **[Self-report](https://nwilbert.github.io/pytest-given/latest/examples/self-report.html)**: pytest-given's own test suite.
48
+
49
+ ## Quick start
50
+
51
+ ```bash
52
+ pip install pytest-given
53
+ ```
54
+
55
+ Requires **Python ≥ 3.14** (t-strings — [PEP 750](https://peps.python.org/pep-0750/) — are part of the step-text API) and **pytest ≥ 9.0**.
56
+
57
+ If AI agents work in your repo, also install the bundled [agent skills](https://nwilbert.github.io/pytest-given/latest/ai-agents/#agent-skills) — with pytest-given's own command, or with [library-skills](https://library-skills.io) alongside the skills of your other dependencies:
58
+
59
+ ```bash
60
+ pytest-given skills install
61
+ # or
62
+ uvx library-skills install --claude
63
+ ```
64
+
65
+ Then narrate a test:
66
+
67
+ ```python
68
+ import pytest
69
+ from pytest_given import attach, given, scenario, then, when
70
+
71
+
72
+ @pytest.fixture
73
+ @given('a coffee machine')
74
+ def machine():
75
+ return {'coffees': 10, 'price': 2}
76
+
77
+
78
+ @scenario('Buy coffee', tags=['billing'])
79
+ def test_buy_coffee(machine):
80
+ with when('I insert $2'):
81
+ machine['coffees'] -= 1
82
+ with then('I get a coffee'):
83
+ assert machine['coffees'] == 9
84
+ attach('Machine state', machine)
85
+ ```
86
+
87
+ Run it:
88
+
89
+ ```bash
90
+ pytest --given-html
91
+ ```
92
+
93
+ This produces `given-report/report.html` — one file you can open directly in a browser.
94
+
95
+ ## Why pytest-given?
96
+
97
+ Classical BDD tools (Cucumber, behave, pytest-bdd) center on Gherkin, a natural-language DSL: stakeholders write the tests, and engineers maintain the glue code behind each step.
98
+
99
+ pytest-given works the other way round: **engineers or their agents write normal tests, and pytest-given turns them into readable documentation.** Stakeholders and domain experts can follow the HTML report without ever opening the test suite. Engineers get a view of the system's behavior that is easier to scan than test code, browsable by tag, glossary term, or module. The approach is the one [JGiven](https://jgiven.org/) pioneered for Java, brought to pytest.
100
+
101
+ - Plain Python: no Gherkin, no `.feature` files, no parser.
102
+ - Tests stay first-class pytest tests, and the report is a by-product.
103
+ - Self-contained HTML: open it locally or attach it to a CI run, with no server and no external assets.
104
+
105
+ ### Written by agents, reviewed by people
106
+
107
+ More and more tests aren't written by hand. A person describes a scenario, an AI agent writes the test along with the code, and people review the narrated report instead of the test code. The diagram shows this loop, and [Working with AI agents](https://nwilbert.github.io/pytest-given/latest/ai-agents/) explains how to set it up.
108
+
109
+ <p align="center">
110
+ <img src="https://raw.githubusercontent.com/nwilbert/pytest-given/main/docs/pytest-given-diagram.svg" alt="A loop between people, agents, and artifacts: developers and domain experts instruct AI agents, which write annotated tests and code. The tests verify the code and generate a report that domain experts validate and developers review, feeding back to the agents." width="640">
111
+ </p>
112
+
113
+ ### Glossary and Domain Storytelling
114
+
115
+ pytest-given goes beyond JGiven by tying tests to the domain itself. A [glossary](https://nwilbert.github.io/pytest-given/latest/guide/glossary/) defines the terms your team uses, and the report highlights them wherever a test mentions them. [Domain Storytelling](https://nwilbert.github.io/pytest-given/latest/guide/domain-storytelling/) adds the big picture: stories of how the domain works, with the report showing which scenarios cover each sentence of a story.
116
+
117
+ ## Features
118
+
119
+ - **[Step context managers](https://nwilbert.github.io/pytest-given/latest/guide/scenarios/)**: `with given(...)`, `when(...)`, `then(...)` blocks in plain pytest tests, nesting within a phase.
120
+ - **[Narrated fixtures](https://nwilbert.github.io/pytest-given/latest/guide/scenarios/)**: `@given` on a fixture, or `Annotated[..., given(...)]` on a parameter, records setup as a step.
121
+ - **[Parametrized scenarios](https://nwilbert.github.io/pytest-given/latest/guide/parametrized/)**: one narrated tree plus a parameter table per case, or one scenario per case on request.
122
+ - **[Glossary](https://nwilbert.github.io/pytest-given/latest/guide/glossary/)**: your domain's terms, declared in code or loaded from a `GLOSSARY.md`, rendered as highlighted term refs in the narration.
123
+ - **[Domain Storytelling](https://nwilbert.github.io/pytest-given/latest/guide/domain-storytelling/)**: Domain Stories as sequences of sentences, and per-sentence coverage in the report.
124
+ - **[Narration lint](https://nwilbert.github.io/pytest-given/latest/configuration/narration-lint/)**: structural checks that a step's text is honest about its body: empty steps, a `then` that checks nothing, a missing phase.
125
+ - **[Agent skills](https://nwilbert.github.io/pytest-given/latest/ai-agents/)**: bundled Agent Skills for authoring, navigating and reviewing narrated tests, installable with one command.
126
+
127
+ The full reference — pytest options, source links, the standalone CLI — is on the [documentation site](https://nwilbert.github.io/pytest-given/latest/).
128
+
129
+ ## License
130
+
131
+ [MIT](https://github.com/nwilbert/pytest-given/blob/main/LICENSE.md). The bundled Alpine.js runtime is also MIT; its notice is in [THIRD-PARTY-LICENSES](https://github.com/nwilbert/pytest-given/blob/main/THIRD-PARTY-LICENSES).
@@ -0,0 +1,103 @@
1
+ # pytest-given
2
+
3
+ A pytest plugin that turns Given/When/Then annotated tests into interactive HTML reports.
4
+
5
+ **Given** your pytest tests,<br>
6
+ **when** you narrate them with `given` / `when` / `then`,<br>
7
+ **then** documentation and behavior fuse into one report.
8
+
9
+ **What people and agents read is what the code does.**
10
+
11
+ Inspired by [JGiven](https://jgiven.org/) (Java).
12
+
13
+ **Documentation: <https://nwilbert.github.io/pytest-given/latest/>**
14
+
15
+ Live examples:
16
+ - **[Coffeeshop](https://nwilbert.github.io/pytest-given/latest/examples/coffeeshop.html)**: a tour of the core features.
17
+ - **[Hotel booking](https://nwilbert.github.io/pytest-given/latest/examples/hotel-booking.html)**: a glossary and domain stories, with story coverage.
18
+ - **[File glossary](https://nwilbert.github.io/pytest-given/latest/examples/file-glossary-booking.html)**: the same, with the glossary kept in a Markdown file.
19
+ - **[Self-report](https://nwilbert.github.io/pytest-given/latest/examples/self-report.html)**: pytest-given's own test suite.
20
+
21
+ ## Quick start
22
+
23
+ ```bash
24
+ pip install pytest-given
25
+ ```
26
+
27
+ Requires **Python ≥ 3.14** (t-strings — [PEP 750](https://peps.python.org/pep-0750/) — are part of the step-text API) and **pytest ≥ 9.0**.
28
+
29
+ If AI agents work in your repo, also install the bundled [agent skills](https://nwilbert.github.io/pytest-given/latest/ai-agents/#agent-skills) — with pytest-given's own command, or with [library-skills](https://library-skills.io) alongside the skills of your other dependencies:
30
+
31
+ ```bash
32
+ pytest-given skills install
33
+ # or
34
+ uvx library-skills install --claude
35
+ ```
36
+
37
+ Then narrate a test:
38
+
39
+ ```python
40
+ import pytest
41
+ from pytest_given import attach, given, scenario, then, when
42
+
43
+
44
+ @pytest.fixture
45
+ @given('a coffee machine')
46
+ def machine():
47
+ return {'coffees': 10, 'price': 2}
48
+
49
+
50
+ @scenario('Buy coffee', tags=['billing'])
51
+ def test_buy_coffee(machine):
52
+ with when('I insert $2'):
53
+ machine['coffees'] -= 1
54
+ with then('I get a coffee'):
55
+ assert machine['coffees'] == 9
56
+ attach('Machine state', machine)
57
+ ```
58
+
59
+ Run it:
60
+
61
+ ```bash
62
+ pytest --given-html
63
+ ```
64
+
65
+ This produces `given-report/report.html` — one file you can open directly in a browser.
66
+
67
+ ## Why pytest-given?
68
+
69
+ Classical BDD tools (Cucumber, behave, pytest-bdd) center on Gherkin, a natural-language DSL: stakeholders write the tests, and engineers maintain the glue code behind each step.
70
+
71
+ pytest-given works the other way round: **engineers or their agents write normal tests, and pytest-given turns them into readable documentation.** Stakeholders and domain experts can follow the HTML report without ever opening the test suite. Engineers get a view of the system's behavior that is easier to scan than test code, browsable by tag, glossary term, or module. The approach is the one [JGiven](https://jgiven.org/) pioneered for Java, brought to pytest.
72
+
73
+ - Plain Python: no Gherkin, no `.feature` files, no parser.
74
+ - Tests stay first-class pytest tests, and the report is a by-product.
75
+ - Self-contained HTML: open it locally or attach it to a CI run, with no server and no external assets.
76
+
77
+ ### Written by agents, reviewed by people
78
+
79
+ More and more tests aren't written by hand. A person describes a scenario, an AI agent writes the test along with the code, and people review the narrated report instead of the test code. The diagram shows this loop, and [Working with AI agents](https://nwilbert.github.io/pytest-given/latest/ai-agents/) explains how to set it up.
80
+
81
+ <p align="center">
82
+ <img src="https://raw.githubusercontent.com/nwilbert/pytest-given/main/docs/pytest-given-diagram.svg" alt="A loop between people, agents, and artifacts: developers and domain experts instruct AI agents, which write annotated tests and code. The tests verify the code and generate a report that domain experts validate and developers review, feeding back to the agents." width="640">
83
+ </p>
84
+
85
+ ### Glossary and Domain Storytelling
86
+
87
+ pytest-given goes beyond JGiven by tying tests to the domain itself. A [glossary](https://nwilbert.github.io/pytest-given/latest/guide/glossary/) defines the terms your team uses, and the report highlights them wherever a test mentions them. [Domain Storytelling](https://nwilbert.github.io/pytest-given/latest/guide/domain-storytelling/) adds the big picture: stories of how the domain works, with the report showing which scenarios cover each sentence of a story.
88
+
89
+ ## Features
90
+
91
+ - **[Step context managers](https://nwilbert.github.io/pytest-given/latest/guide/scenarios/)**: `with given(...)`, `when(...)`, `then(...)` blocks in plain pytest tests, nesting within a phase.
92
+ - **[Narrated fixtures](https://nwilbert.github.io/pytest-given/latest/guide/scenarios/)**: `@given` on a fixture, or `Annotated[..., given(...)]` on a parameter, records setup as a step.
93
+ - **[Parametrized scenarios](https://nwilbert.github.io/pytest-given/latest/guide/parametrized/)**: one narrated tree plus a parameter table per case, or one scenario per case on request.
94
+ - **[Glossary](https://nwilbert.github.io/pytest-given/latest/guide/glossary/)**: your domain's terms, declared in code or loaded from a `GLOSSARY.md`, rendered as highlighted term refs in the narration.
95
+ - **[Domain Storytelling](https://nwilbert.github.io/pytest-given/latest/guide/domain-storytelling/)**: Domain Stories as sequences of sentences, and per-sentence coverage in the report.
96
+ - **[Narration lint](https://nwilbert.github.io/pytest-given/latest/configuration/narration-lint/)**: structural checks that a step's text is honest about its body: empty steps, a `then` that checks nothing, a missing phase.
97
+ - **[Agent skills](https://nwilbert.github.io/pytest-given/latest/ai-agents/)**: bundled Agent Skills for authoring, navigating and reviewing narrated tests, installable with one command.
98
+
99
+ The full reference — pytest options, source links, the standalone CLI — is on the [documentation site](https://nwilbert.github.io/pytest-given/latest/).
100
+
101
+ ## License
102
+
103
+ [MIT](https://github.com/nwilbert/pytest-given/blob/main/LICENSE.md). The bundled Alpine.js runtime is also MIT; its notice is in [THIRD-PARTY-LICENSES](https://github.com/nwilbert/pytest-given/blob/main/THIRD-PARTY-LICENSES).