pytest-given 0.1.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 (98) hide show
  1. pytest_given-0.1.0/.gitignore +23 -0
  2. pytest_given-0.1.0/AGENTS.md +104 -0
  3. pytest_given-0.1.0/CHANGELOG.md +42 -0
  4. pytest_given-0.1.0/GLOSSARY.md +94 -0
  5. pytest_given-0.1.0/LICENSE.md +21 -0
  6. pytest_given-0.1.0/PKG-INFO +510 -0
  7. pytest_given-0.1.0/README.md +482 -0
  8. pytest_given-0.1.0/THIRD-PARTY-LICENSES +26 -0
  9. pytest_given-0.1.0/conftest.py +1 -0
  10. pytest_given-0.1.0/noxfile.py +352 -0
  11. pytest_given-0.1.0/pyproject.toml +127 -0
  12. pytest_given-0.1.0/src/pytest_given/__init__.py +32 -0
  13. pytest_given-0.1.0/src/pytest_given/capture/__init__.py +48 -0
  14. pytest_given-0.1.0/src/pytest_given/capture/collector.py +369 -0
  15. pytest_given-0.1.0/src/pytest_given/capture/decorators.py +502 -0
  16. pytest_given-0.1.0/src/pytest_given/capture/file_glossary.py +106 -0
  17. pytest_given-0.1.0/src/pytest_given/capture/glossary.py +258 -0
  18. pytest_given-0.1.0/src/pytest_given/capture/kind_inference.py +102 -0
  19. pytest_given-0.1.0/src/pytest_given/capture/markdown_glossary.py +147 -0
  20. pytest_given-0.1.0/src/pytest_given/capture/source.py +208 -0
  21. pytest_given-0.1.0/src/pytest_given/capture/story.py +303 -0
  22. pytest_given-0.1.0/src/pytest_given/capture/template.py +180 -0
  23. pytest_given-0.1.0/src/pytest_given/capture/traceback.py +157 -0
  24. pytest_given-0.1.0/src/pytest_given/cli.py +126 -0
  25. pytest_given-0.1.0/src/pytest_given/lint/__init__.py +47 -0
  26. pytest_given-0.1.0/src/pytest_given/lint/ast_rules.py +395 -0
  27. pytest_given-0.1.0/src/pytest_given/lint/base.py +77 -0
  28. pytest_given-0.1.0/src/pytest_given/lint/config.py +137 -0
  29. pytest_given-0.1.0/src/pytest_given/lint/runtime_rules.py +212 -0
  30. pytest_given-0.1.0/src/pytest_given/model/__init__.py +84 -0
  31. pytest_given-0.1.0/src/pytest_given/model/errors.py +2 -0
  32. pytest_given-0.1.0/src/pytest_given/model/ids.py +32 -0
  33. pytest_given-0.1.0/src/pytest_given/model/schema.py +326 -0
  34. pytest_given-0.1.0/src/pytest_given/model/serde.py +263 -0
  35. pytest_given-0.1.0/src/pytest_given/plugin.py +958 -0
  36. pytest_given-0.1.0/src/pytest_given/py.typed +0 -0
  37. pytest_given-0.1.0/src/pytest_given/report/__init__.py +10 -0
  38. pytest_given-0.1.0/src/pytest_given/report/aggregations.py +382 -0
  39. pytest_given-0.1.0/src/pytest_given/report/cli.py +68 -0
  40. pytest_given-0.1.0/src/pytest_given/report/coverage.py +175 -0
  41. pytest_given-0.1.0/src/pytest_given/report/html_renderer.py +370 -0
  42. pytest_given-0.1.0/src/pytest_given/report/inline_markdown.py +35 -0
  43. pytest_given-0.1.0/src/pytest_given/report/md_renderer.py +164 -0
  44. pytest_given-0.1.0/src/pytest_given/report/source_link.py +212 -0
  45. pytest_given-0.1.0/src/pytest_given/report/templates/alpine.min.js +6 -0
  46. pytest_given-0.1.0/src/pytest_given/report/templates/app.js +438 -0
  47. pytest_given-0.1.0/src/pytest_given/report/templates/report.html.j2 +606 -0
  48. pytest_given-0.1.0/src/pytest_given/report/templates/styles.css +1013 -0
  49. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-authoring/SKILL.md +43 -0
  50. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-authoring/references/api.md +62 -0
  51. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-authoring/references/domain-storytelling.md +36 -0
  52. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-authoring/references/glossaries.md +68 -0
  53. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-authoring/references/scenarios.md +29 -0
  54. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-authoring/references/stories.md +53 -0
  55. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-navigating/SKILL.md +40 -0
  56. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-navigating/references/report-json.md +69 -0
  57. pytest_given-0.1.0/src/pytest_given/skills_data/pytest-given-reviewing/SKILL.md +47 -0
  58. pytest_given-0.1.0/tests/__init__.py +0 -0
  59. pytest_given-0.1.0/tests/conftest.py +3 -0
  60. pytest_given-0.1.0/tests/integration/__init__.py +0 -0
  61. pytest_given-0.1.0/tests/integration/test_cli.py +178 -0
  62. pytest_given-0.1.0/tests/integration/test_plugin.py +1869 -0
  63. pytest_given-0.1.0/tests/integration/test_plugin_file_glossary.py +42 -0
  64. pytest_given-0.1.0/tests/integration/test_plugin_lint.py +396 -0
  65. pytest_given-0.1.0/tests/integration/test_plugin_session_isolation.py +176 -0
  66. pytest_given-0.1.0/tests/integration/test_skills_cli.py +90 -0
  67. pytest_given-0.1.0/tests/ubiquitous_language.py +89 -0
  68. pytest_given-0.1.0/tests/unit/__init__.py +0 -0
  69. pytest_given-0.1.0/tests/unit/capture/__init__.py +0 -0
  70. pytest_given-0.1.0/tests/unit/capture/test_collector.py +541 -0
  71. pytest_given-0.1.0/tests/unit/capture/test_file_glossary.py +351 -0
  72. pytest_given-0.1.0/tests/unit/capture/test_glossary.py +447 -0
  73. pytest_given-0.1.0/tests/unit/capture/test_kind_inference.py +298 -0
  74. pytest_given-0.1.0/tests/unit/capture/test_markdown_glossary.py +300 -0
  75. pytest_given-0.1.0/tests/unit/capture/test_source.py +210 -0
  76. pytest_given-0.1.0/tests/unit/capture/test_step_descriptor.py +1015 -0
  77. pytest_given-0.1.0/tests/unit/capture/test_story.py +648 -0
  78. pytest_given-0.1.0/tests/unit/capture/test_template.py +419 -0
  79. pytest_given-0.1.0/tests/unit/capture/test_traceback_parser.py +202 -0
  80. pytest_given-0.1.0/tests/unit/lint/__init__.py +0 -0
  81. pytest_given-0.1.0/tests/unit/lint/test_ast_rules.py +929 -0
  82. pytest_given-0.1.0/tests/unit/lint/test_config.py +169 -0
  83. pytest_given-0.1.0/tests/unit/lint/test_runtime_rules.py +285 -0
  84. pytest_given-0.1.0/tests/unit/model/__init__.py +0 -0
  85. pytest_given-0.1.0/tests/unit/model/test_errors.py +12 -0
  86. pytest_given-0.1.0/tests/unit/model/test_ids.py +21 -0
  87. pytest_given-0.1.0/tests/unit/model/test_schema.py +452 -0
  88. pytest_given-0.1.0/tests/unit/model/test_serde.py +692 -0
  89. pytest_given-0.1.0/tests/unit/report/__init__.py +0 -0
  90. pytest_given-0.1.0/tests/unit/report/test_aggregations.py +762 -0
  91. pytest_given-0.1.0/tests/unit/report/test_coverage.py +535 -0
  92. pytest_given-0.1.0/tests/unit/report/test_html_renderer.py +1839 -0
  93. pytest_given-0.1.0/tests/unit/report/test_inline_markdown.py +39 -0
  94. pytest_given-0.1.0/tests/unit/report/test_md_renderer.py +441 -0
  95. pytest_given-0.1.0/tests/unit/report/test_source_link.py +348 -0
  96. pytest_given-0.1.0/tests/unit/test_plugin.py +609 -0
  97. pytest_given-0.1.0/tests/unit/test_skills_data.py +55 -0
  98. pytest_given-0.1.0/uv.lock +823 -0
@@ -0,0 +1,23 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .nox/
5
+ .nox-tmp/
6
+ .venv/
7
+ .wsl-venv/
8
+ .coverage
9
+ /htmlcov
10
+ .mypy_cache/
11
+ /dist/
12
+ *.egg-info/
13
+ .DS_Store
14
+ .idea/
15
+ .superpowers/
16
+ docs/superpowers/plans/
17
+ /given-report/
18
+ /benchmarks/test_large_scenarios.py
19
+ /benchmarks/large-scenarios-data.json
20
+ /benchmarks/large-scenarios.html
21
+ .claude/settings.local.json
22
+ .playwright-mcp/
23
+ .mcp.json
@@ -0,0 +1,104 @@
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
+ Run all checks: `uv run nox`. List individual sessions 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.** 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 whose real content is untouched by your change will still show a diff — `git checkout` those files rather than committing the noise. The Markdown report carries none of these fields: it is deterministic, so **read the `.md` diff first** — it is the behavioural delta of your change in prose. (An unchanged `.md` doesn't by itself prove the JSON/HTML are noise-only: source-line shifts and glossary/story data don't surface in the Markdown.) Regenerate only the reports a change can affect: the `examples` reports narrate the `examples/**` test files, and the `self_report` narrates the backend tests under `tests/**` (so a shifted line number in a decorated backend test — e.g. from adding or removing code above it — is a real self-report change worth committing, even when no example changed).
24
+ - Both regeneration sessions run the narration lint (`--given-lint=true`; 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 honest-two-phase ignore convention live in the [authoring skill](.claude/skills/pytest-given-authoring/references/scenarios.md) under "Mechanical counterparts".
25
+ - `uv run nox -s coverage` enforces a 100% coverage target.
26
+ - `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.
27
+
28
+ ## Releasing
29
+
30
+ 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).
31
+
32
+ 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`.
33
+
34
+ `check_release` installs pytest-given from an index into a throwaway environment and runs a real scenario through it — the same smoke test `nox -s build` runs against a locally built wheel, pointed at what an index actually serves.
35
+
36
+ ## Architecture
37
+
38
+ - `src/pytest_given/__init__.py` — Public API re-exports; the full surface is documented in the skill's [references/api.md](src/pytest_given/skills_data/pytest-given-authoring/references/api.md)
39
+ - `src/pytest_given/plugin.py` — pytest hooks, parametrized test grouping, structural templatize, scenario-source capture from `item.location`. Top-level orchestrator.
40
+ - `src/pytest_given/cli.py` — Console entry point (`pytest-given`): argparse root, `skills install` subcommand (mirrors `src/pytest_given/skills_data/` into a project's `.claude/skills/`, `--check` for drift). Top-level like `plugin.py`; delegates `report` to `report/cli.py`.
41
+ - `src/pytest_given/capture/decorators.py` — `StepDescriptor` + `ScenarioDecorator`: dual context-manager/decorator, cross-phase nesting detection, thread-local state
42
+ - `src/pytest_given/capture/collector.py` — Step stack, collects scenario data during test execution
43
+ - `src/pytest_given/capture/template.py` — `Template` (deferred brace substitution for `@scenario(...)`) + `narration_from(...)` (dispatches `str` / `Template` / t-string into a `Narration`) + `parse_tstring(...)`
44
+ - `src/pytest_given/capture/source.py` — Rootdir-aware `capture_caller_source(skip=...)` helper using `inspect.stack`; used by `story()` and glossary registration to record their construction site as a `SourceLocation`
45
+ - `src/pytest_given/model/schema.py` — Frozen / mutable dataclasses for the report tree, the `Narration` part union, and the `NodeId` / `Phase` aliases
46
+ - `src/pytest_given/model/serde.py` — `report_to_dict` / `report_from_dict` boundary between JSON and the dataclass model; discriminates the three `NarrationPart` variants by key
47
+ - `src/pytest_given/model/errors.py` — `PytestGivenError`
48
+ - `src/pytest_given/lint/` — Narration lint: `base.py` (Finding model + rule catalog as data), `config.py` (`given_lint_rules` / `given_lint_ignore` parsing, `apply_config`), `runtime_rules.py` (rules over the recorded model), `ast_rules.py` (rules over step bodies anchored to their `with` / helper `FunctionDef`). Pure — imports only from `model/`; findings surface via the terminal summary and exit code in `plugin.py`, never in report artifacts
49
+ - `src/pytest_given/report/html_renderer.py` — Reads JSON via `report_from_dict` and walks typed dataclasses; emits self-contained HTML (Jinja2 + Alpine.js); single structural `narration` filter dispatching on `NarrationPart` variants via `match`/`case`
50
+ - `src/pytest_given/report/md_renderer.py` — Plain-text Markdown renderer over the same typed model; `«term»` markers, no browser needed
51
+ - `src/pytest_given/report/source_link.py` — Preset resolution (`vscode` / `cursor` / `zed` / `pycharm` / `github`), template variable substitution, GitHub org/repo + commit-SHA detection for `--given-source-link`
52
+ - `src/pytest_given/report/cli.py` — The `pytest-given report` subcommand (mirrors `--given-source-link` as `--source-link`); registered on the root parser owned by `cli.py`
53
+ - `src/pytest_given/report/templates/` — Jinja2 template, CSS, bundled Alpine.js
54
+
55
+ ### Step text & placeholders
56
+
57
+ 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).
58
+
59
+ ## Handling report output
60
+
61
+ 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](.claude/skills/pytest-given-navigating/SKILL.md).
62
+
63
+ ## Report testing
64
+
65
+ 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.
66
+
67
+ - **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.
68
+ - **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.
69
+
70
+ - The report targets desktop only — assume a minimum viewport width of ~900px. No mobile/responsive layout needed.
71
+ - Traceback display and header metadata formatting are known limitations, not current priorities.
72
+ - Never save Playwright screenshots into the project directory. Use `/tmp/` or omit the `filename` parameter.
73
+ - If the Playwright MCP browser install hangs after the download reaches 100% (microsoft/playwright#40998 in alpha builds), switch `.mcp.json` from `--browser chromium` to `--browser chrome` to use system Chrome.
74
+
75
+ **Setup:** copy `.mcp.json.example` to `.mcp.json` (gitignored) to enable the server. It is deliberately not committed: opening a report needs `--allow-unrestricted-file-access`, because Playwright MCP blocks `file://` navigation entirely by default and offers no narrower scope. That flag also lets the browser read any file the user can, so it stays opt-in per developer rather than arriving with a clone. Keep the version pinned — `@latest` would resolve fresh from npm on every launch.
76
+
77
+ ## Writing self-report scenarios
78
+
79
+ The narration rules live in the **`pytest-given-authoring` skill** — auto-discovered by contributor agents from [.claude/skills/pytest-given-authoring/](.claude/skills/pytest-given-authoring/SKILL.md) and shipped to downstream projects via `pytest-given skills install`. The canonical source is [src/pytest_given/skills_data/](src/pytest_given/skills_data/pytest-given-authoring/SKILL.md); after editing it, regenerate the committed copy with `uv run pytest-given skills install` and commit both (a sync test fails otherwise). The subsection below covers only what is specific to this repo's self-report.
80
+
81
+ **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.
82
+
83
+ ### Self-report mechanics (this repo)
84
+
85
+ - 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).
86
+ - Regeneration (`uv run nox -s self_report`), narration-lint gating, and the commit-noise / `.md`-diff-review rules live under [Quality gates](#quality-gates).
87
+
88
+ ## Conventions
89
+
90
+ - Use the project's canonical vocabulary — see [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"). Term-naming and rename mechanics live in the skill's [references/glossaries.md](src/pytest_given/skills_data/pytest-given-authoring/references/glossaries.md) — a rename lands everywhere at once: glossary row, `pg\[` references, and the implementation naming. Repo specifics: all of it in the same commit, then `uv run nox -s self_report` and commit the regenerated report (adding a term is safe, but still regenerate).
91
+ - Avoid `Any` — use precise types, generics, `TYPE_CHECKING` imports, or `ContextVar[T]` over untyped `threading.local`.
92
+ - 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.
93
+ - Only module-level imports — no inline/function-level imports.
94
+ - **pytest config lives in `[tool.pytest]` (native TOML mode).** Since pytest 8.4/9.0, `pyproject.toml`'s `[tool.pytest]` table is parsed with native TOML types — lists are real arrays, not newline-separated strings — and this is the table the project uses (`testpaths = ["tests"]`). `[tool.pytest.ini_options]` is the legacy string-based INI-compat mode; **don't add it alongside `[tool.pytest]`** — pytest raises `UsageError` if both are present. New `addini` options are configured here as native types (a `type='linelist'` ini takes a TOML array).
95
+ - Cross-platform: the plugin and its tests must run 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`, normalize with `as_posix()` for stored/serialized paths, and resolve before comparing. The one platform seam is `capture/source.py`, which folds Windows-style and `/mnt/<drive>` path forms into the running platform's native one (mechanics in its docstrings). Tests that assert WSL/`/mnt`-absolute behavior must `skipif(sys.platform == 'win32', …)` — native-Windows pathlib treats `/mnt/<drive>` as drive-relative, not absolute — but every other test must pass on all four targets.
96
+ - Subpackage boundaries (convention, not lint-enforced): `src/pytest_given/` is split into four subpackages with a strict dependency direction. `model/` is the leaf; `capture/`, `lint/`, and `report/` all depend on `model/`; they do not depend on each other. `plugin.py` sits at the top level as the orchestrator and is allowed to import from all four. Inside the package, use relative imports throughout — `from .schema import Scenario` for siblings, `from ..model import Scenario` for cross-subpackage (always through the subpackage root, not into its submodules). The top-level `__init__.py` and `plugin.py` also use relative imports (`from .capture import …`). Tests use absolute imports and may reach into any internal path.
97
+ - 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).
98
+ - Step-down rule: callers before callees, public before private. Read each file top-down from high-level API to implementation details.
99
+ - TDD: write tests first
100
+ - 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.
101
+ - 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.
102
+ - Plan files under `docs/superpowers/plans/` are scratch artifacts — never commit them. Spec files under `docs/specs/` are committed.
103
+ - 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. `ls docs/specs/proposed` is the canonical list of outstanding design work.
104
+ - Always run `uv run nox` (or at minimum `uv run nox -s format lint mypy test`) before committing
@@ -0,0 +1,42 @@
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
+ ## [Unreleased]
13
+
14
+ ## [0.1.0] - 2026-08-08
15
+
16
+ First public release.
17
+
18
+ ### Added
19
+
20
+ - `@scenario` decorator plus `given` / `when` / `then` step blocks, usable as
21
+ both context managers and decorators, including on fixtures.
22
+ - Self-contained interactive HTML report (`--given-html`), Markdown report
23
+ (`--given-md`), and JSON report (`--given-json`). The HTML bundles Alpine.js
24
+ and needs no server or external assets.
25
+ - Structured step text: plain strings, `Template` objects, and t-strings
26
+ ([PEP 750](https://peps.python.org/pep-0750/)), with parameter interpolation
27
+ rendered as highlighted values.
28
+ - `attach()` for text and JSON attachments on a step.
29
+ - Domain Storytelling support: ubiquitous-language glossaries (inline or
30
+ Markdown-backed via `FileGlossary`), Domain Stories, and story coverage.
31
+ - Narration lint (`--given-lint`) with a configurable rule catalog via
32
+ `given_lint_rules` and `given_lint_ignore`.
33
+ - `--given-source-link` with `vscode`, `cursor`, `zed`, `pycharm`, and `github`
34
+ presets for jumping from a report step to its source.
35
+ - `pytest-given` console script: `report` to re-render a saved JSON report, and
36
+ `skills install` to mirror the bundled agent skills into a project's
37
+ `.claude/skills/`.
38
+ - Bundled authoring, navigating, and reviewing skills for AI agents, shipped in
39
+ the wheel and version-matched to the plugin.
40
+
41
+ [Unreleased]: https://github.com/nwilbert/pytest-given/compare/v0.1.0...HEAD
42
+ [0.1.0]: https://github.com/nwilbert/pytest-given/releases/tag/v0.1.0
@@ -0,0 +1,94 @@
1
+ # Glossary
2
+
3
+ Canonical vocabulary for pytest-given. Use these terms in code, docs, commit messages, and conversation; flag inconsistencies in review.
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.
6
+
7
+ **Update rule:** rename or repurpose a term → update this file in the same commit.
8
+
9
+ ## Core test model
10
+
11
+ | Term | Meaning |
12
+ |---|---|
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. |
16
+ | **Phase** | The category of a step: `given`, `when`, or `then`. A step has exactly one phase. |
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. |
19
+ | **Attachment** | A labeled blob (text or JSON) bound to the currently-active step via `attach(label, content)`. |
20
+
21
+ ## Parametrization
22
+
23
+ | Term | Meaning |
24
+ |---|---|
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
+ | **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. |
30
+
31
+ ## Fixtures and recording
32
+
33
+ | Term | Meaning |
34
+ |---|---|
35
+ | **Step fixture** | A pytest fixture whose function is wrapped with `@given(text)`. Only `@given` is allowed on fixtures; `@when` / `@then` are rejected. |
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. |
38
+ | **Graft** | Attaching a fixture recording into the active scenario's step tree at the moment its host test starts. |
39
+
40
+ ## Collector state
41
+
42
+ | Term | Meaning |
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. |
45
+ | **Active scenario** | The scenario currently being recorded into; tracked by node ID. |
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
+ | **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. |
48
+
49
+ ## Report
50
+
51
+ | Term | Meaning |
52
+ |---|---|
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. |
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. |
58
+
59
+ ## Domain Storytelling
60
+
61
+ The Domain-Driven Design layer atop the core surface. All terms here are optional features: a test suite can use none, some, or all.
62
+
63
+ | Term | Meaning |
64
+ |---|---|
65
+ | **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
+ | **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. |
68
+ | **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. |
71
+ | **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. |
73
+ | **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
+ | **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. |
76
+ | **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). |
78
+ | **Path** | A branching segment inside a story — `path(...)` lets alternate activity sequences share a prefix. |
79
+ | **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
+ | **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. |
82
+ | **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
+ | **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
+ | **Undefined** | A term with `definition is None`; surfaced by a badge and filter in the Glossary view. Orthogonal to *kindless*. |
85
+
86
+ ## Collaboration
87
+
88
+ The people and machines collaborating on a pytest-given test suite — the cast of the project's own domain story (defined in `tests/ubiquitous_language.py`).
89
+
90
+ | Term | Meaning |
91
+ |---|---|
92
+ | **Developer** | Person who writes the application code and — together with the *Agent* — the scenarios; curates the glossary with the *Domain Expert*. |
93
+ | **Domain Expert** | Person who owns the domain knowledge and the ubiquitous language; source of domain stories and reviewer of scenarios and reports. A stakeholder in the broad sense. |
94
+ | **Agent** | AI coding agent that authors and maintains scenarios alongside the *Developer*, guided by the pytest-given skills. |
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Niko Wilbert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.