pytest-given 0.1.0__tar.gz → 0.2.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 (170) hide show
  1. pytest_given-0.2.0/AGENTS.md +128 -0
  2. pytest_given-0.2.0/CHANGELOG.md +310 -0
  3. {pytest_given-0.1.0 → pytest_given-0.2.0}/GLOSSARY.md +27 -18
  4. {pytest_given-0.1.0 → pytest_given-0.2.0}/PKG-INFO +172 -140
  5. {pytest_given-0.1.0 → pytest_given-0.2.0}/README.md +170 -138
  6. {pytest_given-0.1.0 → pytest_given-0.2.0}/noxfile.py +47 -24
  7. {pytest_given-0.1.0 → pytest_given-0.2.0}/pyproject.toml +52 -5
  8. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/__init__.py +3 -1
  9. pytest_given-0.2.0/src/pytest_given/capture/__init__.py +87 -0
  10. pytest_given-0.2.0/src/pytest_given/capture/collector.py +503 -0
  11. pytest_given-0.2.0/src/pytest_given/capture/discovery.py +71 -0
  12. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/capture/file_glossary.py +31 -34
  13. pytest_given-0.2.0/src/pytest_given/capture/glossary.py +263 -0
  14. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/capture/kind_inference.py +46 -35
  15. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/capture/markdown_glossary.py +57 -27
  16. pytest_given-0.2.0/src/pytest_given/capture/process_state.py +71 -0
  17. pytest_given-0.2.0/src/pytest_given/capture/scenario.py +166 -0
  18. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/capture/source.py +93 -107
  19. pytest_given-0.2.0/src/pytest_given/capture/steps.py +444 -0
  20. pytest_given-0.2.0/src/pytest_given/capture/story.py +306 -0
  21. pytest_given-0.2.0/src/pytest_given/capture/template.py +222 -0
  22. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/capture/traceback.py +33 -46
  23. pytest_given-0.2.0/src/pytest_given/cli/__init__.py +37 -0
  24. pytest_given-0.2.0/src/pytest_given/cli/report.py +121 -0
  25. pytest_given-0.1.0/src/pytest_given/cli.py → pytest_given-0.2.0/src/pytest_given/cli/skills.py +41 -54
  26. pytest_given-0.2.0/src/pytest_given/grouping/__init__.py +10 -0
  27. pytest_given-0.2.0/src/pytest_given/grouping/attachments.py +141 -0
  28. pytest_given-0.2.0/src/pytest_given/grouping/checks.py +396 -0
  29. pytest_given-0.2.0/src/pytest_given/grouping/columns.py +277 -0
  30. pytest_given-0.2.0/src/pytest_given/grouping/context.py +104 -0
  31. pytest_given-0.2.0/src/pytest_given/grouping/group.py +102 -0
  32. pytest_given-0.2.0/src/pytest_given/grouping/percase.py +91 -0
  33. pytest_given-0.2.0/src/pytest_given/grouping/templatize.py +264 -0
  34. pytest_given-0.2.0/src/pytest_given/lint/__init__.py +31 -0
  35. pytest_given-0.2.0/src/pytest_given/lint/ast_rules.py +472 -0
  36. pytest_given-0.2.0/src/pytest_given/lint/base.py +66 -0
  37. pytest_given-0.2.0/src/pytest_given/lint/config.py +178 -0
  38. pytest_given-0.2.0/src/pytest_given/lint/runner.py +31 -0
  39. pytest_given-0.2.0/src/pytest_given/lint/runtime_rules.py +167 -0
  40. pytest_given-0.2.0/src/pytest_given/lint/summary.py +45 -0
  41. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/model/__init__.py +62 -8
  42. pytest_given-0.2.0/src/pytest_given/model/errors.py +35 -0
  43. pytest_given-0.2.0/src/pytest_given/model/narration.py +124 -0
  44. pytest_given-0.2.0/src/pytest_given/model/runtime.py +38 -0
  45. pytest_given-0.2.0/src/pytest_given/model/schema.py +411 -0
  46. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/model/serde.py +121 -31
  47. pytest_given-0.2.0/src/pytest_given/model/steps.py +43 -0
  48. pytest_given-0.2.0/src/pytest_given/model/text.py +54 -0
  49. pytest_given-0.2.0/src/pytest_given/plugin/__init__.py +43 -0
  50. pytest_given-0.2.0/src/pytest_given/plugin/collection.py +79 -0
  51. pytest_given-0.2.0/src/pytest_given/plugin/fixtures.py +234 -0
  52. pytest_given-0.2.0/src/pytest_given/plugin/options.py +216 -0
  53. pytest_given-0.2.0/src/pytest_given/plugin/runtest.py +266 -0
  54. pytest_given-0.2.0/src/pytest_given/plugin/session.py +274 -0
  55. pytest_given-0.2.0/src/pytest_given/plugin/state.py +136 -0
  56. pytest_given-0.2.0/src/pytest_given/report/__init__.py +28 -0
  57. pytest_given-0.2.0/src/pytest_given/report/coverage.py +222 -0
  58. pytest_given-0.2.0/src/pytest_given/report/glossary_view.py +367 -0
  59. pytest_given-0.2.0/src/pytest_given/report/html_renderer.py +461 -0
  60. pytest_given-0.2.0/src/pytest_given/report/inline_markdown.py +52 -0
  61. pytest_given-0.2.0/src/pytest_given/report/md_renderer.py +266 -0
  62. pytest_given-0.2.0/src/pytest_given/report/palette.py +184 -0
  63. pytest_given-0.2.0/src/pytest_given/report/sinks.py +171 -0
  64. pytest_given-0.2.0/src/pytest_given/report/slugs.py +92 -0
  65. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/report/source_link.py +49 -41
  66. pytest_given-0.2.0/src/pytest_given/report/story_view.py +137 -0
  67. pytest_given-0.2.0/src/pytest_given/report/templates/_macros.html.j2 +155 -0
  68. pytest_given-0.2.0/src/pytest_given/report/templates/app.js +888 -0
  69. pytest_given-0.2.0/src/pytest_given/report/templates/report.html.j2 +584 -0
  70. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/report/templates/styles.css +425 -138
  71. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/skills_data/pytest-given-authoring/SKILL.md +16 -7
  72. pytest_given-0.2.0/src/pytest_given/skills_data/pytest-given-authoring/references/api.md +64 -0
  73. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/skills_data/pytest-given-authoring/references/domain-storytelling.md +1 -1
  74. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/skills_data/pytest-given-authoring/references/glossaries.md +19 -7
  75. pytest_given-0.2.0/src/pytest_given/skills_data/pytest-given-authoring/references/scenarios.md +81 -0
  76. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/skills_data/pytest-given-authoring/references/stories.md +14 -3
  77. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/skills_data/pytest-given-navigating/SKILL.md +9 -7
  78. pytest_given-0.2.0/src/pytest_given/skills_data/pytest-given-navigating/references/report-json.md +92 -0
  79. pytest_given-0.2.0/src/pytest_given/skills_data/pytest-given-reviewing/SKILL.md +64 -0
  80. pytest_given-0.2.0/src/pytest_given/skills_data/pytest-given-reviewing/references/story-coverage.md +45 -0
  81. pytest_given-0.2.0/tests/integration/test_cli.py +354 -0
  82. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/integration/test_plugin.py +842 -118
  83. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/integration/test_plugin_lint.py +129 -76
  84. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/integration/test_plugin_session_isolation.py +89 -2
  85. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/integration/test_skills_cli.py +19 -2
  86. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/ubiquitous_language.py +7 -3
  87. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_collector.py +183 -111
  88. pytest_given-0.2.0/tests/unit/capture/test_discovery.py +124 -0
  89. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_file_glossary.py +19 -36
  90. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_glossary.py +54 -51
  91. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_kind_inference.py +8 -15
  92. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_markdown_glossary.py +53 -16
  93. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_source.py +18 -11
  94. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_step_descriptor.py +355 -113
  95. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_story.py +179 -52
  96. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_template.py +66 -48
  97. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/test_traceback_parser.py +22 -46
  98. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/lint/test_ast_rules.py +311 -145
  99. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/lint/test_config.py +82 -26
  100. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/lint/test_runtime_rules.py +96 -91
  101. pytest_given-0.2.0/tests/unit/lint/test_summary.py +79 -0
  102. pytest_given-0.2.0/tests/unit/model/test_narration.py +15 -0
  103. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/model/test_schema.py +93 -40
  104. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/model/test_serde.py +295 -28
  105. pytest_given-0.2.0/tests/unit/model/test_steps.py +34 -0
  106. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/report/test_coverage.py +77 -33
  107. pytest_given-0.1.0/tests/unit/report/test_aggregations.py → pytest_given-0.2.0/tests/unit/report/test_glossary_view.py +60 -340
  108. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/report/test_html_renderer.py +475 -116
  109. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/report/test_md_renderer.py +296 -41
  110. pytest_given-0.2.0/tests/unit/report/test_palette.py +105 -0
  111. pytest_given-0.2.0/tests/unit/report/test_sinks.py +70 -0
  112. pytest_given-0.2.0/tests/unit/report/test_slugs.py +110 -0
  113. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/report/test_source_link.py +177 -80
  114. pytest_given-0.2.0/tests/unit/report/test_story_view.py +366 -0
  115. pytest_given-0.2.0/tests/unit/test_grouping.py +2843 -0
  116. pytest_given-0.2.0/tests/unit/test_percase.py +221 -0
  117. pytest_given-0.2.0/tests/unit/test_plugin.py +603 -0
  118. pytest_given-0.2.0/tests/unit/test_plugin_traceback.py +45 -0
  119. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/test_skills_data.py +7 -1
  120. {pytest_given-0.1.0 → pytest_given-0.2.0}/uv.lock +13 -5
  121. pytest_given-0.1.0/AGENTS.md +0 -104
  122. pytest_given-0.1.0/CHANGELOG.md +0 -42
  123. pytest_given-0.1.0/src/pytest_given/capture/__init__.py +0 -48
  124. pytest_given-0.1.0/src/pytest_given/capture/collector.py +0 -369
  125. pytest_given-0.1.0/src/pytest_given/capture/decorators.py +0 -502
  126. pytest_given-0.1.0/src/pytest_given/capture/glossary.py +0 -258
  127. pytest_given-0.1.0/src/pytest_given/capture/story.py +0 -303
  128. pytest_given-0.1.0/src/pytest_given/capture/template.py +0 -180
  129. pytest_given-0.1.0/src/pytest_given/lint/__init__.py +0 -47
  130. pytest_given-0.1.0/src/pytest_given/lint/ast_rules.py +0 -395
  131. pytest_given-0.1.0/src/pytest_given/lint/base.py +0 -77
  132. pytest_given-0.1.0/src/pytest_given/lint/config.py +0 -137
  133. pytest_given-0.1.0/src/pytest_given/lint/runtime_rules.py +0 -212
  134. pytest_given-0.1.0/src/pytest_given/model/errors.py +0 -2
  135. pytest_given-0.1.0/src/pytest_given/model/ids.py +0 -32
  136. pytest_given-0.1.0/src/pytest_given/model/schema.py +0 -326
  137. pytest_given-0.1.0/src/pytest_given/plugin.py +0 -958
  138. pytest_given-0.1.0/src/pytest_given/report/__init__.py +0 -10
  139. pytest_given-0.1.0/src/pytest_given/report/aggregations.py +0 -382
  140. pytest_given-0.1.0/src/pytest_given/report/cli.py +0 -68
  141. pytest_given-0.1.0/src/pytest_given/report/coverage.py +0 -175
  142. pytest_given-0.1.0/src/pytest_given/report/html_renderer.py +0 -370
  143. pytest_given-0.1.0/src/pytest_given/report/inline_markdown.py +0 -35
  144. pytest_given-0.1.0/src/pytest_given/report/md_renderer.py +0 -164
  145. pytest_given-0.1.0/src/pytest_given/report/templates/app.js +0 -438
  146. pytest_given-0.1.0/src/pytest_given/report/templates/report.html.j2 +0 -606
  147. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-authoring/references/api.md +0 -62
  148. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-authoring/references/scenarios.md +0 -29
  149. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-navigating/references/report-json.md +0 -69
  150. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-reviewing/SKILL.md +0 -47
  151. pytest_given-0.1.0/tests/integration/test_cli.py +0 -178
  152. pytest_given-0.1.0/tests/unit/model/test_ids.py +0 -21
  153. pytest_given-0.1.0/tests/unit/test_plugin.py +0 -609
  154. {pytest_given-0.1.0 → pytest_given-0.2.0}/.gitignore +0 -0
  155. {pytest_given-0.1.0 → pytest_given-0.2.0}/LICENSE.md +0 -0
  156. {pytest_given-0.1.0 → pytest_given-0.2.0}/THIRD-PARTY-LICENSES +0 -0
  157. {pytest_given-0.1.0 → pytest_given-0.2.0}/conftest.py +0 -0
  158. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/py.typed +0 -0
  159. {pytest_given-0.1.0 → pytest_given-0.2.0}/src/pytest_given/report/templates/alpine.min.js +0 -0
  160. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/__init__.py +0 -0
  161. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/conftest.py +0 -0
  162. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/integration/__init__.py +0 -0
  163. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/integration/test_plugin_file_glossary.py +0 -0
  164. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/__init__.py +0 -0
  165. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/capture/__init__.py +0 -0
  166. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/lint/__init__.py +0 -0
  167. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/model/__init__.py +0 -0
  168. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/model/test_errors.py +0 -0
  169. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/report/__init__.py +0 -0
  170. {pytest_given-0.1.0 → pytest_given-0.2.0}/tests/unit/report/test_inline_markdown.py +0 -0
@@ -0,0 +1,128 @@
1
+ # Agents
2
+
3
+ ## Project overview
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.
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ uv sync --group dev
11
+ ```
12
+
13
+ **All Python invocations go through `uv run`** — `uv run pytest …`, `uv run python -m …`, `uv run nox …`. There is no system `python` on PATH; bare `python` / `pytest` calls will fail. This applies to one-off commands (running a single test file, REPL exploration) too, not just nox sessions.
14
+
15
+ **Never prepend `cd <path> &&` to commands.** The working directory is already set to the project root; the `cd` is redundant and triggers a permission prompt.
16
+
17
+ ## Quality gates
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`.
20
+
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
+ - `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.
23
+ - **Only commit a regenerated report when its *content* actually changed.**
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
+ - **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
+ - 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.
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.
30
+
31
+ ## Releasing
32
+
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`.
36
+
37
+ ## Architecture
38
+
39
+ `src/pytest_given/` is five library subpackages plus two entry points, with a
40
+ strict dependency direction (convention, not lint-enforced):
41
+
42
+ ```
43
+ model/ the leaf — schema, serde, errors, shared text rules
44
+ capture/ lint/ report/ each on model/ only, never on each other
45
+ grouping/ on model/ + capture/
46
+ plugin/ cli/ the entry points; may import all five, and hold
47
+ nothing the five could
48
+ ```
49
+
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:
53
+
54
+ - `grouping/` — the parametrize pass: a scenario's cases collapsed into one
55
+ narrated tree plus a parameter table, refusing the authoring forms that would
56
+ make that tree lie. Runs at session finish *before* the sinks are written, so
57
+ a bare `pytest` failing on one is the point, not a side effect.
58
+ - `plugin/__init__.py` is the hook surface pluggy registers — the `pytest11`
59
+ entry point is the package, and pluggy scans a module's attributes for
60
+ `pytest_*` names, so the re-exports there *are* the registration.
61
+ - `capture/` imports pytest nowhere, so all of it is unit-testable without a
62
+ session; a stray step warns with `model.PytestGivenWarning`, not pytest's.
63
+ - `Glossary` exists twice on purpose: `model/schema.py` holds the storage the
64
+ report carries and serde rebuilds, and `capture/glossary.py` subclasses it
65
+ with the registration API, which needs a caller source location that the leaf
66
+ may not reach for. `pytest_given.Glossary` is the subclass; everything
67
+ internal annotates the base.
68
+ - Each package exposes the *whole* job, not its parts: `report.emit_sinks`
69
+ (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.
72
+
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
+
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).
81
+
82
+ ## Handling report output
83
+
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).
85
+
86
+ ## Report testing
87
+
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.
89
+
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.
94
+ - 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.
96
+
97
+ **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
+
99
+ ## Writing self-report scenarios
100
+
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).
102
+
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.
104
+
105
+ What is specific to this repo's self-report:
106
+
107
+ - The glossary handle is `pg` — `GLOSSARY.md` loaded as a `FileGlossary` in `tests/conftest.py` via `tests/ubiquitous_language.py`. Term-rename mechanics live under [Conventions](#conventions); regeneration and lint gating under [Quality gates](#quality-gates).
108
+ - **New or changed user-facing behavior needs a scenario, not just a test** — otherwise the behavior is invisible in the report. Decorate the test that best *states* the rule, one per rule, not per branch; the edge cases around it stay plain. Two gaps to check for: a rule the [CHANGELOG](CHANGELOG.md) announces that no scenario names, and a [GLOSSARY.md](GLOSSARY.md) row *asserting* behavior (`Templatize`, `Parameter table`) that no scenario demonstrates.
109
+
110
+ ## Conventions
111
+
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.
113
+ - Avoid `Any` — use precise types, generics, `TYPE_CHECKING` imports, or `ContextVar[T]` over untyped `threading.local`.
114
+ - 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
+ - Only module-level imports — no inline/function-level imports.
116
+ - **pytest config lives in `[tool.pytest]`** — native TOML mode since pytest 8.4/9.0, where lists are real arrays rather than newline-separated strings. `[tool.pytest.ini_options]` is the legacy INI-compat table; **never add it alongside** — pytest raises `UsageError` if both are present. New `addini` options take native types (a `type='linelist'` ini takes a TOML array).
117
+ - 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
+ - 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
+ - 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).
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
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
+ - 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.
126
+ - Plan files under `docs/superpowers/plans/` are scratch artifacts — never commit them. Spec files under `docs/specs/` are committed.
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
@@ -0,0 +1,310 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ The release workflow reads the section matching the version in `pyproject.toml`
9
+ and uses it as the GitHub Release body, so each version needs a heading of the
10
+ form `## [x.y.z] - YYYY-MM-DD`.
11
+
12
+ ## [0.2.0] - 2026-09-04
13
+
14
+ ### Added
15
+
16
+ - `pytest_given.PytestGivenWarning` is a top-level export, and a step or
17
+ `attach()` recorded in a test without `@scenario` now warns with it instead of
18
+ `pytest.PytestWarning`.
19
+ - `--given-title=TEXT` (or the `given_title` ini) names the report, replacing the
20
+ rootdir name.
21
+ - A parametrized scenario's parameter table now carries a typed column per varying
22
+ value — `param`, `derived`, or `attachment` for a varying attachment payload —
23
+ rather than one column per parametrize name.
24
+ - `@scenario(group_parametrized=False)` declines the grouping and emits each case
25
+ as its own scenario, titled by its parametrize id.
26
+ - The HTML report's sidebar gains **Terms** as a third browse axis, and all three
27
+ axes — Tags, Terms, Modules — now filter the Scenarios view the same way, with
28
+ each active filter carried in the URL.
29
+ - The sidebar can be ordered by group size as well as by name, and resized by
30
+ dragging its seam or with the arrow keys.
31
+ - A selected activity in the Stories view offers **Open in Scenarios**, filtering
32
+ the Scenarios view down to the scenarios covering it.
33
+
34
+ ### Changed
35
+
36
+ #### CLI
37
+
38
+ - **Breaking.** `--given-lint` is a plain boolean flag: write `--given-lint` and
39
+ `--no-given-lint` instead of `--given-lint=true` / `--given-lint=false`. Either
40
+ form still overrides the `given_lint` ini for one run.
41
+
42
+ #### Authoring API
43
+
44
+ - **Breaking.** Step narration must now be uniform across parametrize cases;
45
+ these fail the run with `PytestGivenError`, writing no report, instead of
46
+ quietly reporting case 1:
47
+
48
+ - a plain `str` (usually an f-string) that renders differently per case;
49
+ - a varying interpolation that is not a bare name (`t"{cup_size * 0.01}"`,
50
+ `t"{m.balance}"`);
51
+ - a t-string narrating a parametrize name that no longer holds the case's
52
+ value — either a local rebound it, or the body mutated it in place;
53
+ - a step whose set of `attach` labels differs between cases;
54
+ - a glossary term ref that names a different term or reads differently between
55
+ cases, including one bound to a parametrize column;
56
+ - passed cases that narrate different templates altogether.
57
+
58
+ Every one but the last is fixed by binding the varying part to a local and
59
+ narrating it with a t-string, keeping labels and term refs constant; varying
60
+ content belongs in the new `attachment` column. The last needs
61
+ `@scenario(..., group_parametrized=False)`, giving each case its own scenario.
62
+ - **Breaking.** `attach()` now takes a plain `str` label; a t-string label raises
63
+ `PytestGivenError` — use an f-string.
64
+ - **Breaking.** `attach()` called with no step open now raises `PytestGivenError`
65
+ instead of silently discarding the payload; move the call inside the step it
66
+ belongs to.
67
+ - **Breaking.** `activity(..., id=N)` is now `activity(..., activity_id=N)`; the
68
+ `Activity.id` field itself is unchanged.
69
+ - **Breaking.** `FileGlossary` is now a `Glossary` subclass rather than a wrapper
70
+ around one, so its `.glossary` attribute is gone — use the `FileGlossary`
71
+ itself wherever that attribute was passed.
72
+ - A glossary term placed in an activity slot its declared kind forbids now raises
73
+ `PytestGivenError` when `activity(...)` is built rather than at session finish.
74
+ - A glossary file whose table has a header and separator but no data rows now
75
+ says so, instead of reporting that no table was found.
76
+ - A `FileGlossary` whose columns are all named now skips a Markdown table that
77
+ carries none of those names, so a glossary file may hold prose tables beside
78
+ the glossary; a table carrying some of them still raises, as does any table
79
+ under an index-based column spec.
80
+ - `@scenario(activities=...)` now rejects a `str` and non-`int` members with a
81
+ `TypeError`.
82
+ - `@scenario` now returns the test function itself rather than a wrapper, so the
83
+ test keeps its own signature.
84
+
85
+ #### Plugin and run behavior
86
+
87
+ - An unknown `given_source_link` preset is now a `UsageError` raised before the
88
+ suite runs.
89
+ - The collection-time `@scenario` checks now report as a `UsageError` instead of
90
+ an `INTERNALERROR` traceback.
91
+ - The narration lint summary prints each finding's location in its own column
92
+ rather than appended to the message.
93
+ - `pytest-given` with no subcommand, and `pytest-given skills` with no
94
+ subcommand, now print that parser's usage and exit 2 instead of the root help
95
+ and exit 1.
96
+
97
+ #### Report content (all formats)
98
+
99
+ - **Breaking (JSON report).** `parameters.names` becomes `parameters.columns`
100
+ (`{id, name, kind}`), cells may hold an attachment object, placeholder parts
101
+ gain `column_id`, term-ref parts lose `param_column`, and a grouped step's
102
+ `narration.text` is the template rather than case 1's rendering.
103
+ - **Breaking (JSON report).** A step no longer carries `status` or `error`;
104
+ failure lives on the scenario and on the parameter table's cases. A consumer
105
+ reading `step.status` should read `scenario.status` instead.
106
+ - The Markdown report now shows a scenario's failure reason — the message and the
107
+ failing frame — under the scenario, and under the parameter table for each
108
+ failed case.
109
+
110
+ #### HTML report
111
+
112
+ - The browse sidebar leads with **Modules** and renders them as a collapsible
113
+ package tree whose nodes filter by path prefix; it no longer lists individual
114
+ scenarios under each group.
115
+ - The report's colors are retuned into one system — a term ref in a step or a
116
+ scenario title reads as a word under a light wash rather than a bordered pill
117
+ (the Glossary view keeps its pills), and column colors are generated per
118
+ column — and the sidebar, its filter chips and the attachment badges are
119
+ tidied along with it.
120
+ - The report opens and filters substantially faster on large suites, and its file
121
+ is smaller — a term reference now points at its glossary entry instead of
122
+ repeating the entry's definition, which takes about 18% off a term-heavy
123
+ report.
124
+ - A glossary term referenced only in a `@scenario` title now contributes an
125
+ instance to the Glossary view, where it previously counted toward the term's
126
+ scenario tally while showing no instance.
127
+
128
+ #### Bundled skills
129
+
130
+ - The authoring and reviewing skills gain the report mechanics their rules depend
131
+ on, a symptom index, a completeness audit, the full lint rule catalog, and
132
+ guidance for sparser tagging.
133
+
134
+ ### Removed
135
+
136
+ - **Breaking.** The `divergent-case-structure` lint rule; delete any
137
+ `given_lint_rules` or `given_lint_ignore` entry naming it, which would
138
+ otherwise fail config parsing.
139
+
140
+ ### Fixed
141
+
142
+ #### Authoring API
143
+
144
+ - Parametrize cases that claim different step activities now say so, instead of
145
+ reporting the more drastic "a different step structure".
146
+
147
+ - A `@given` fixture scoped wider than `function` no longer loses its step when
148
+ the first test to use it has no `@scenario`.
149
+ - `@given`/`@when`/`@then` are signature-preserving, so a decorated helper stays
150
+ callable to a type checker (was: `"StepDecorated" not callable`).
151
+ - `@given(...)` above `@pytest.fixture` now raises and names the fix, instead of
152
+ surfacing as `fixture '<name>' not found`.
153
+ - `@scenario(activities=...)` is now typed `int | Sequence[int] | None`, so a
154
+ bare `activities=2` type-checks.
155
+ - `@scenario(activities=...)` with an unknown id, or without `story=`, is
156
+ rejected at the decorator rather than at collection.
157
+ - A `-k`- or `--deselect`-narrowed run no longer fails on an authoring error in
158
+ a scenario it did not select.
159
+ - Glossary term handles are now hashable, and equal for the same term whichever
160
+ accessor produced them.
161
+ - A `when_then` step in a test without `@scenario` now points its warning at the
162
+ test rather than at pytest-given's own module.
163
+
164
+ #### Narration lint
165
+
166
+ - A `given_lint_ignore` entry beginning with a Windows drive letter
167
+ (`c:/repo/tests/t.py::test_x`) is no longer rejected as an unknown rule prefix.
168
+
169
+ - A rule configured `off` no longer runs; levels were applied only after every
170
+ rule had already produced its findings.
171
+ - A scenario tag with no ASCII alphanumerics (`tags=['日本語']`) no longer takes
172
+ the run down with a traceback from `tag-shadows-term`.
173
+
174
+ #### Report
175
+
176
+ - `pytest-given report` reports an unreadable input as an error instead of a
177
+ traceback (a directory raised `IsADirectoryError` through the console script).
178
+ - A JSON report with an out-of-range scenario `status` or term `kind` is
179
+ rejected by name, instead of crashing a renderer with a bare `KeyError`.
180
+ - A `<br>` inside an inline code span in a glossary definition renders as text
181
+ rather than as a line break.
182
+ - The Terms browse axis no longer lists a term the selected glossary does not
183
+ hold.
184
+
185
+ #### Plugin and run behavior
186
+
187
+ - A `--given-json`/`--given-html`/`--given-md` path that could not be a report
188
+ file is now refused before the suite runs, instead of a bare flag swallowing a
189
+ following test path and overwriting — or, on a failed run, deleting — it.
190
+ - `pytest-given report` now discards a stale report when the *render* fails, not
191
+ only when the write does.
192
+ - `pytest-given report --source-link` is now validated on a `--format md` run
193
+ instead of being accepted and ignored, and an unknown preset is reported under
194
+ the name the user typed.
195
+ - A `git` on PATH that cannot be executed no longer fails the run.
196
+ - A nested in-process pytest run that dies while parsing its arguments no longer
197
+ strands the outer session's captured rootdir, which silently dropped every
198
+ later step's source anchor.
199
+ - `@given`/`@when`/`@then` on an `async def` helper now records around the
200
+ awaited body, and async generator fixtures are handled too.
201
+ - The narration lint now inspects `async def` step helpers, whose bodies were
202
+ invisible to every AST rule.
203
+ - An explicit `--given-source-link=` now disables source links instead of falling
204
+ through to the `given_source_link` ini.
205
+ - A finished scenario no longer leaves its collector — and every scenario and
206
+ step it recorded — reachable from a process-global for the rest of the process.
207
+ - An error-level lint finding no longer overwrites a more specific exit code, so
208
+ an interrupted or nothing-collected run keeps reporting as one.
209
+ - A report that cannot be written into an unwritable directory now reports
210
+ through the terminal summary instead of escaping as a bare traceback.
211
+ - `pytest-given report` and `pytest-given skills install` now report a failed
212
+ write as a CLI error, and a failed `report` write discards the previous run's
213
+ report rather than leaving it to read as current.
214
+ - `pytest-given report` now reports a bad input file — missing, unparsable, or
215
+ JSON that is not a pytest-given report — as a CLI error instead of a traceback,
216
+ as does an unknown `--source-link` preset.
217
+ - A fixture that raises after its `yield` now fails the scenario it tore down,
218
+ instead of leaving it green in a report pytest counted as an error.
219
+ - An error-level narration-lint finding now counts as an error in the run's
220
+ summary line.
221
+ - Every failure building or writing a report — a suite reaching two glossaries, a
222
+ term used in incompatible slots, an unusable source-link template, an
223
+ unwritable output path — now surfaces as a terminal summary and a failing exit
224
+ code, where only grouping errors did.
225
+ - The sinks are now rendered in full before any is written, and a failure on
226
+ either side discards all of them.
227
+
228
+ #### Report content (all formats)
229
+
230
+ - A parametrized scenario whose cases all skipped now keeps its skip reason
231
+ instead of reporting `skipped` with none.
232
+ - A parameter row whose cell count disagreed with the table's columns silently
233
+ truncated in the HTML report; it now fails the same way it already did in
234
+ Markdown.
235
+
236
+ - A parametrized scenario now keeps its place in source order instead of moving
237
+ below every unparametrized one.
238
+ - A glossary term written as a code span keeps the markup inside it, so
239
+ `` `a*b*c` `` canonicalizes to `a*b*c`.
240
+ - A parameter-table cell now reads the way the step pointing at it read, carrying
241
+ the interpolation's own format spec and, under `indirect=True`, the bound test
242
+ argument.
243
+ - A `Template` narration's `text` is now what its parts render, so the report's
244
+ search box and `jq` queries match what the page displays.
245
+ - The grouped step tree now comes from the first case that *passed*, where a
246
+ skipped case 1 used to render an empty tree.
247
+ - A parametrize value that is a glossary term instance now narrates as its
248
+ display rather than the whole `Glossary` dataclass repr — in a step's
249
+ `Template` slot, in a scenario name, and in an
250
+ `Annotated[..., given(Template(...))]` parameter label.
251
+
252
+ #### HTML report
253
+
254
+ - Two test files sharing a basename across directories no longer abort the HTML
255
+ report; the scenarios' `#scenario=` slugs gain directory components instead.
256
+ - A `#view=stories`, `#view=glossary` or `#term=` link opened against a report
257
+ that has no such tab now falls back to the Scenarios view.
258
+ - The Glossary view's kind headings and their term counts now follow the search
259
+ and definition filters, and a filter matching nothing says so.
260
+ - Content reaching past a scenario card's right edge is no longer clipped: a wide
261
+ parameter table and an attachment payload scroll, a source path and a
262
+ traceback's frame location wrap, and the source link no longer overlaps the
263
+ card's last element.
264
+ - Jumping to a scenario from a story activity, or to a term's scenarios from the
265
+ Glossary tab, now clears filters that would hide the target; the filters in a
266
+ `#scenario=` deep link still win.
267
+ - Accent-colored text and the parametrize column colors now meet WCAG AA, and
268
+ term kinds stay distinguishable for red-green color blindness.
269
+ - The report is operable from the keyboard: status pills, browse-axis and
270
+ browse-tree rows, tag pills, story sidebar entries, activity and attachment
271
+ badges, and every expand/collapse chevron are now real buttons, and the view
272
+ tabs report which one is selected.
273
+ - A step pinned with `given(..., activity=N)` now covers an activity regardless
274
+ of its term count; an under-anchored activity previously still rendered as
275
+ "not coverage-tracked".
276
+
277
+ #### Bundled skills
278
+
279
+ - The bundled skills are corrected against the shipped behavior.
280
+
281
+ ## [0.1.0] - 2026-08-08
282
+
283
+ First public release.
284
+
285
+ ### Added
286
+
287
+ - `@scenario` decorator plus `given` / `when` / `then` step blocks, usable as
288
+ both context managers and decorators, including on fixtures.
289
+ - Self-contained interactive HTML report (`--given-html`), Markdown report
290
+ (`--given-md`), and JSON report (`--given-json`). The HTML bundles Alpine.js
291
+ and needs no server or external assets.
292
+ - Structured step text: plain strings, `Template` objects, and t-strings
293
+ ([PEP 750](https://peps.python.org/pep-0750/)), with parameter interpolation
294
+ rendered as highlighted values.
295
+ - `attach()` for text and JSON attachments on a step.
296
+ - Domain Storytelling support: ubiquitous-language glossaries (inline or
297
+ Markdown-backed via `FileGlossary`), Domain Stories, and story coverage.
298
+ - Narration lint (`--given-lint`) with a configurable rule catalog via
299
+ `given_lint_rules` and `given_lint_ignore`.
300
+ - `--given-source-link` with `vscode`, `cursor`, `zed`, `pycharm`, and `github`
301
+ presets for jumping from a report step to its source.
302
+ - `pytest-given` console script: `report` to re-render a saved JSON report, and
303
+ `skills install` to mirror the bundled agent skills into a project's
304
+ `.claude/skills/`.
305
+ - Bundled authoring, navigating, and reviewing skills for AI agents, shipped in
306
+ the wheel and version-matched to the plugin.
307
+
308
+ [Unreleased]: https://github.com/nwilbert/pytest-given/compare/v0.2.0...HEAD
309
+ [0.2.0]: https://github.com/nwilbert/pytest-given/compare/v0.1.0...v0.2.0
310
+ [0.1.0]: https://github.com/nwilbert/pytest-given/releases/tag/v0.1.0
@@ -2,7 +2,7 @@
2
2
 
3
3
  Canonical vocabulary for pytest-given. Use these terms in code, docs, commit messages, and conversation; flag inconsistencies in review.
4
4
 
5
- This glossary covers pytest-given's own bounded context. The terminology a *user's* test suite adopts (the domain the user is testing — e.g., the coffee domain in `examples/test_examples.py`) is a separate concern.
5
+ This glossary covers pytest-given's own bounded context. The terminology a *user's* test suite adopts (the domain the user is testing — e.g., the coffee domain in `examples/coffeeshop/test_coffeeshop.py`) is a separate concern.
6
6
 
7
7
  **Update rule:** rename or repurpose a term → update this file in the same commit.
8
8
 
@@ -11,8 +11,8 @@ This glossary covers pytest-given's own bounded context. The terminology a *user
11
11
  | Term | Meaning |
12
12
  |---|---|
13
13
  | **Scenario** | A test function decorated with `@scenario(...)`. Not every pytest test is a scenario — undecorated tests are tolerated but not collected. |
14
- | **Step** | A unit of narration: a `with given(...)` / `when(...)` / `then(...)` block, or the root recording from a step fixture. Steps nest. Each carries a phase, text, status (`'passed'` by default; set to `'failed'` on the step where a scenario fails, for highlighting in the report), optional error, attachments, and children. |
15
- | **Narration** | The human-readable text on a step or scenario name. Modeled as a `Narration` dataclass bundling a flat rendered `text: str` with a `parts: list[NarrationPart]` (empty for plain-string authoring; populated when the source was a t-string or `pytest_given.Template`, with `NarrationLiteral` / `NarrationValue` / `NarrationPlaceholder` pieces). The structured form lets the templatizer and renderer treat parametrize-bound values specially without regex tricks. |
14
+ | **Step** | A unit of narration: a `with given(...)` / `when(...)` / `then(...)` block, or the root recording from a step fixture. Steps nest, and each carries a phase, narration, attachments and children — but no status or error of its own: a failure is recorded on the *Scenario*, and per *Case* on the *Parameter table*, which is where both renderers read it. |
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
18
  | **Tag** | Free-form string label attached via `@scenario(name, tags=[...])`. Used by the report's filter UI. |
@@ -24,9 +24,9 @@ This glossary covers pytest-given's own bounded context. The terminology a *user
24
24
  |---|---|
25
25
  | **Parametrized scenario** | A `@scenario`-decorated test that also carries `@pytest.mark.parametrize(...)`. Produces multiple scenario records during a run; pytest-given groups them. |
26
26
  | **Case** | One row of a parametrized scenario — a single tuple of parameter values, its status, and any error. |
27
- | **Parameter table** | The per-scenario grouping of column names + cases. Appears in the report below the grouped-template steps. |
28
- | **Group** | Collapsing the N scenario records of a parametrized scenario into one logical scenario carrying a parameter table. Scenarios group when they share the same name and module. |
29
- | **Templatize** | Derive the grouped-template step text from the first case, so the report shows a single set of steps with `{name}` placeholders that the parameter table fills in per case. Non-first cases' structured text is discarded. |
27
+ | **Parameter table** | The per-scenario grouping of typed columns + cases. A column is a `param` (a `@pytest.mark.parametrize` input), a `derived` (a narrated value that varies across cases) or an `attachment` (an attachment whose payload varies). Appears in the report below the grouped-template steps. |
28
+ | **Group** | Collapsing the N scenario records of a parametrized scenario into one logical scenario carrying a *Parameter table*. Cases group when they share both the same name and the same *Node ID* without its parametrize tail — one test function; two same-named scenarios on different test functions stay separate. `@scenario(group_parametrized=False)` declines the merge, so each case lives on as its own scenario. |
29
+ | **Templatize** | Derive the grouped-template step text by comparing every comparable case: what all of them share stays inline, and anything that varies becomes a `{name}` placeholder or attachment badge pointing at a parameter-table column. The baseline tree comes from the first passed case, and every other passed case must narrate that same template. |
30
30
 
31
31
  ## Fixtures and recording
32
32
 
@@ -34,14 +34,14 @@ This glossary covers pytest-given's own bounded context. The terminology a *user
34
34
  |---|---|
35
35
  | **Step fixture** | A pytest fixture whose function is wrapped with `@given(text)`. Only `@given` is allowed on fixtures; `@when` / `@then` are rejected. |
36
36
  | **Plain fixture** | A pytest fixture without a pytest-given decorator. Used by tests but produces no step in the report. |
37
- | **Fixture recording** | A captured subtree of steps + attachments produced while a step fixture is being set up (and, for generator fixtures, torn down). Stored keyed by fixture-instance identity. |
37
+ | **Fixture recording** | A captured subtree of steps + attachments produced while a step fixture is being set up. Stored keyed by fixture-instance identity. A generator fixture's teardown records nothing — it refuses steps and attachments. |
38
38
  | **Graft** | Attaching a fixture recording into the active scenario's step tree at the moment its host test starts. |
39
39
 
40
40
  ## Collector state
41
41
 
42
42
  | Term | Meaning |
43
43
  |---|---|
44
- | **Collector** | The module-level singleton that accumulates scenarios, fixture recordings, and parameter info during a pytest session. Reset at the start of each session. |
44
+ | **Collector** | The per-session object accumulating scenarios, fixture recordings, and parameter info, published through a `ContextVar` so a nested run can displace and restore it. |
45
45
  | **Active scenario** | The scenario currently being recorded into; tracked by node ID. |
46
46
  | **Node ID** | A pytest test identifier (e.g., `tests/test_x.py::test_y[a-b]`). Used as a key throughout the collector. |
47
47
  | **Step stack** | The chain of currently-open steps; entered by `with given(...)`, popped on exit. Mirrored inside a fixture recording while a fixture body is running. |
@@ -51,10 +51,19 @@ This glossary covers pytest-given's own bounded context. The terminology a *user
51
51
  | Term | Meaning |
52
52
  |---|---|
53
53
  | **Report** | The output artifact: a JSON data file and optional self-contained HTML and Markdown renderings derived from it. The JSON is the source of truth. |
54
- | **Renderer** | Converts a JSON report into a self-contained HTML page. |
54
+ | **Renderer** | Converts a JSON report into a rendering — a self-contained HTML page or a Markdown document. |
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
- | **Value highlight** | A neutral highlight applied to t-string interpolation values that don't correspond to a parametrize column (e.g., a computed expression like `price * 1.2`). |
57
- | **Source link** | A clickable file:line anchor on a scenario card. Resolved from the `given_source_link` config (preset name like `vscode` / `github`, or a raw URL template). Captured per-scenario as a `SourceLocation` (POSIX relpath + 1-indexed line) from `pytest.Item.location`. Disabled by default. |
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
+ | **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
+
59
+ ## Narration lint
60
+
61
+ | Term | Meaning |
62
+ |---|---|
63
+ | **Narration lint** | The `--given-lint` pass that checks recorded scenarios against the *Lint rule* catalog once the *Report* model is built. It enforces the structural subset of the narration conventions — it cannot check whether a *Step*'s text is semantically true. |
64
+ | **Lint rule** | One named check, carrying a surface and a default *Severity*. A `runtime` rule reads the recorded scenarios; an `ast` rule parses the step bodies' source. The catalog in `lint/base.py` is data, so severities, config validation and docs stay in sync with one table. |
65
+ | **Finding** | One *Lint rule* firing on one subject: the rule id, the *Severity* it fired at, the subject, a *Source link* location and a message naming the offender. |
66
+ | **Severity** | A *Lint rule*'s level — `off`, `warn` or `error` — defaulted by the catalog and overridable per rule via `given_lint_rules`. Only `error` fails the run; `warn` reports in the terminal summary; an `off` rule is not evaluated at all. |
58
67
 
59
68
  ## Domain Storytelling
60
69
 
@@ -64,21 +73,21 @@ The Domain-Driven Design layer atop the core surface. All terms here are optiona
64
73
  |---|---|
65
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. |
66
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. |
67
- | **Deferred term** | A term obtained before its kind is known — the `DeferredTermHandle` returned by the code glossary (`g('foo')` declare-or-get, `g['foo']` get-only) and by a *file glossary* (`g['Guest']`). One handle type serves all kinds (unlike the eager Actor/Work Object/Verb handles); the kind stays `None` until *kind inference* runs. |
76
+ | **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. |
68
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). |
69
- | **Actor** | A glossary term for a participant in the domain (e.g., *Guest*). Renders with the actor pill style. |
70
- | **Work Object** | A glossary term for a thing acted on (e.g., *Room*, *Booking*). Renders with the work-object pill style. |
78
+ | **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
+ | **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. |
71
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*. |
72
- | **Term ref** | An occurrence of a term inside narration. Modelled as `NarrationTermRef` in step text and as `ActivityTermRef` inside activity prose. |
81
+ | **Term ref** | An occurrence of a term inside narration. Modeled as `NarrationTermRef` in step text and as `ActivityTermRef` inside activity prose. |
73
82
  | **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. |
74
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. |
75
- | **Story** | A named flow modelled as a sequence of activities. Constructed by `story('Title', [activity(...), ...])`. Stories are first-class report tabs and the unit of coverage. |
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. |
76
85
  | **Activity** | One row in a story — typically `actor + verb + work_object` plus optional connective words. Constructed by `activity(...)`. |
77
- | **Activity Part** | The two-variant union making up an activity's prose (`ActivityPart` = `ActivityTermRef | ActivityWord`): `ActivityTermRef` (a reference to a glossary term — actor, work object, or verb; kind resolved via the glossary) and `ActivityWord` (a bare path word — a node label or an edge connective; carries no kind or id, is never classified by inference, and never appears in the glossary — distinct from a kindless/undefined term, which has an id and is tracked). |
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. |
78
87
  | **Path** | A branching segment inside a story — `path(...)` lets alternate activity sequences share a prefix. |
79
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*. |
80
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`. |
81
- | **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. |
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. |
82
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*. |
83
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. |
84
93
  | **Undefined** | A term with `definition is None`; surfaced by a badge and filter in the Glossary view. Orthogonal to *kindless*. |