pytest-selective 1.3.0__tar.gz → 1.5.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 (207) hide show
  1. pytest_selective-1.3.0/.github/test-lanes.json → pytest_selective-1.5.0/.github/plan-lanes.json +1 -21
  2. pytest_selective-1.5.0/.github/test-lanes.json +22 -0
  3. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/ci.yml +19 -18
  4. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/public-actions.yml +6 -4
  5. pytest_selective-1.5.0/.release-please-manifest.json +3 -0
  6. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/AGENTS.md +4 -3
  7. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/CHANGELOG.md +14 -0
  8. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/CONTRIBUTING.md +41 -17
  9. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/PKG-INFO +1 -1
  10. pytest_selective-1.5.0/contracts/selection/conftest.py.gv-dependency.yaml +5 -0
  11. pytest_selective-1.5.0/contracts/selection/test_selection_contract.py.gv-dependency.yaml +6 -0
  12. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/ci-toolkit.md +8 -1
  13. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/ci.md +24 -0
  14. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/configuration.md +6 -0
  15. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/selection-health.md +5 -2
  16. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/tutorials/adoption-checklist.md +3 -0
  17. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/tutorials/sidecar-dependencies.md +42 -2
  18. pytest_selective-1.5.0/documentation/case-study.md +293 -0
  19. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/mkdocs.yml +14 -0
  20. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/pyproject.toml +1 -1
  21. pytest_selective-1.5.0/scripts/check_lane_routing.py +145 -0
  22. pytest_selective-1.5.0/scripts/ci_file_config.py +18 -0
  23. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/smoke_distribution.py +2 -0
  24. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/skills/pytest-selective/SKILL.md +10 -0
  25. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/ci.py +15 -11
  26. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_declarations.py +16 -5
  27. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_graph.py +11 -3
  28. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_models.py +2 -0
  29. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_ci_admission.py +2 -1
  30. pytest_selective-1.5.0/tests/test_ci_admission.py.gv-dependency.yaml +5 -0
  31. pytest_selective-1.5.0/tests/test_release_workflow.py.gv-dependency.yaml +5 -0
  32. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_repository_ci.py +76 -71
  33. pytest_selective-1.5.0/tests/test_repository_ci.py.gv-dependency.yaml +7 -0
  34. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_ci.py +76 -0
  35. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_impact_audit_declarations.py +16 -0
  36. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_impact_graph.py +137 -0
  37. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/uv.lock +1 -1
  38. pytest_selective-1.3.0/.release-please-manifest.json +0 -3
  39. pytest_selective-1.3.0/scripts/ci_file_config.py +0 -13
  40. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.dry-lint/baseline.json +0 -0
  41. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.dry-lint/progress.json +0 -0
  42. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.dry-lint.toml +0 -0
  43. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.gitattributes +0 -0
  44. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  45. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  46. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/dependabot.yml +0 -0
  47. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/validation-inputs.json +0 -0
  48. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/docs.yml +0 -0
  49. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/dry-lint-audit.yml +0 -0
  50. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/dry-lint-judge.yml +0 -0
  51. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/dry-lint.yml +0 -0
  52. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/observe-ci.yml +0 -0
  53. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/pr-title.yml +0 -0
  54. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/release-please.yml +0 -0
  55. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/release.yml +0 -0
  56. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/weekly-checkpoint.yml +0 -0
  57. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.github/workflows/windows-performance.yml +0 -0
  58. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.gitignore +0 -0
  59. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.pre-commit-config.yaml +0 -0
  60. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/.readthedocs.yaml +0 -0
  61. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/CLAUDE.md +0 -0
  62. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/LICENSE +0 -0
  63. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/PROVENANCE.md +0 -0
  64. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/README.md +0 -0
  65. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/RELEASING.md +0 -0
  66. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/action.yml +0 -0
  67. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/actions/README.md +0 -0
  68. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/actions/plan-lanes/action.yml +0 -0
  69. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/actions/pytest-expansion/action.yml +0 -0
  70. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/actions/test-impact/action.yml +0 -0
  71. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/actions/validation-gate/action.yml +0 -0
  72. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/agentsync.toml +0 -0
  73. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/contracts/selection/README.md +0 -0
  74. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/contracts/selection/conftest.py +0 -0
  75. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/contracts/selection/quint-connect.json +0 -0
  76. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/contracts/selection/quint.ini +0 -0
  77. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/contracts/selection/runtime_deps.py +0 -0
  78. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/contracts/selection/selection.qnt +0 -0
  79. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/contracts/selection/selection_driver.py +0 -0
  80. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/contracts/selection/test_selection_contract.py +0 -0
  81. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/README.md +0 -0
  82. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/actions.md +0 -0
  83. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/agents.md +0 -0
  84. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/assets/banner.png +0 -0
  85. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/assets/banner.svg +0 -0
  86. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/assets/brand.json +0 -0
  87. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/assets/icon.png +0 -0
  88. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/assets/logo.svg +0 -0
  89. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/assets/raster-sources.json +0 -0
  90. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/assets/social-preview.png +0 -0
  91. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/assets/social-preview.svg +0 -0
  92. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22-due-ci.json +0 -0
  93. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22-due-docs-ci.json +0 -0
  94. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22-due-docs-widget.json +0 -0
  95. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22-due-widget.json +0 -0
  96. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22-input-replays.json +0 -0
  97. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22-obligation-docs.json +0 -0
  98. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22-obligation.json +0 -0
  99. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22-routing.md +0 -0
  100. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22-selective-docs.json +0 -0
  101. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-22.md +0 -0
  102. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-23-windows-saleor/obligation-baseline.json +0 -0
  103. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.11.xml +0 -0
  104. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.14.xml +0 -0
  105. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-after.json +0 -0
  106. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.11.xml +0 -0
  107. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.14.xml +0 -0
  108. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-control.json +0 -0
  109. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-earlier.json +0 -0
  110. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/2026-10-23-windows-saleor.md +0 -0
  111. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/benchmarks/windows-runtime.md +0 -0
  112. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/comparison-pytest-impacted.md +0 -0
  113. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/evidence.md +0 -0
  114. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/local-development.md +0 -0
  115. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/pytest-hygiene.md +0 -0
  116. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/shared-plan-execution.md +0 -0
  117. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/tutorial.md +0 -0
  118. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/tutorials/skip-unrelated-ci.md +0 -0
  119. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/work-in-progress/tickets/actions-directory-test-plan.md +0 -0
  120. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/work-in-progress/tickets/codex-selector-correctness-performance-test-plan.md +0 -0
  121. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/work-in-progress/tickets/codex-windows-git-performance-test-plan.md +0 -0
  122. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/work-in-progress/tickets/graph-based-inputs-plan.md +0 -0
  123. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/work-in-progress/tickets/impacted-improvements-test-plan.md +0 -0
  124. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/work-in-progress/tickets/move-action-implementations-test-plan.md +0 -0
  125. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/work-in-progress/tickets/public-action-onboarding-test-plan.md +0 -0
  126. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/work-in-progress/tickets/ratchet-duplicates-zero-test-plan.md +0 -0
  127. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/docs/work-in-progress/tickets/rerun-selection-test-plan.md +0 -0
  128. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/assets/routing-comparison.svg +0 -0
  129. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/check_site.py +0 -0
  130. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/ecosystem.md +0 -0
  131. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/hooks.py +0 -0
  132. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/index.md +0 -0
  133. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/llms.txt +0 -0
  134. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/overrides/main.html +0 -0
  135. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/publishing.md +0 -0
  136. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/requirements.in +0 -0
  137. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/documentation/requirements.txt +0 -0
  138. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/release-please-config.json +0 -0
  139. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/benchmark_graph.py +0 -0
  140. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/benchmark_parsing.py +0 -0
  141. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/benchmark_rust.py +0 -0
  142. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/check_docs.py +0 -0
  143. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/check_metadata.py +0 -0
  144. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/compare_impacted.py +0 -0
  145. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/measure_ci_runs.py +0 -0
  146. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/public_actions.py +0 -0
  147. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/render_brand.py +0 -0
  148. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/run_ci_tests.py +0 -0
  149. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/scripts/tutorial.py +0 -0
  150. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/skills/pytest-selective/agents/openai.yaml +0 -0
  151. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/__init__.py +0 -0
  152. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/__main__.py +0 -0
  153. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/ast_imports.py +0 -0
  154. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/checkpoint.py +0 -0
  155. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/execution.py +0 -0
  156. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/gate.py +0 -0
  157. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/git_operations.py +0 -0
  158. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/hygiene.py +0 -0
  159. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_audit.py +0 -0
  160. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_audit_declarations.py +0 -0
  161. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_audit_plugin.py +0 -0
  162. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_cli.py +0 -0
  163. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_explanation.py +0 -0
  164. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_first_party.py +0 -0
  165. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_name_scope.py +0 -0
  166. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/impact_selection.py +0 -0
  167. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/input_policy.py +0 -0
  168. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/local.py +0 -0
  169. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/parse_batch.py +0 -0
  170. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/plugin.py +0 -0
  171. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/py.typed +0 -0
  172. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/pytest_expansion.py +0 -0
  173. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/pytest_plugins.py +0 -0
  174. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/pytest_sharding.py +0 -0
  175. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/test_inventory.py +0 -0
  176. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/typecheck_scope.py +0 -0
  177. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/src/pytest_selective/working_tree.py +0 -0
  178. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/__init__.py +0 -0
  179. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_checkpoint_policy.py +0 -0
  180. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_ci_measurements.py +0 -0
  181. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_ci_test_execution.py +0 -0
  182. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_distribution_metadata.py +0 -0
  183. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_hygiene.py +0 -0
  184. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_public_actions.py +0 -0
  185. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_pytest_expansion.py +0 -0
  186. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_release_workflow.py +0 -0
  187. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_repository_hygiene.py +0 -0
  188. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_repository_hygiene.py.gv-dependency.yaml +0 -0
  189. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/test_tutorial.py +0 -0
  190. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/__init__.py +0 -0
  191. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/git_repo.py +0 -0
  192. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_ast_imports.py +0 -0
  193. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_gate.py +0 -0
  194. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_git_repo.py +0 -0
  195. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_global_inputs.py +0 -0
  196. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_impact_audit.py +0 -0
  197. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_impact_explanation.py +0 -0
  198. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_impact_first_party.py +0 -0
  199. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_impact_name_scope.py +0 -0
  200. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_parse_batch.py +0 -0
  201. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_portability.py +0 -0
  202. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_pull_request_merge_base.py +0 -0
  203. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_pytest_plugin.py +0 -0
  204. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_pytest_plugins.py +0 -0
  205. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_pytest_sharding.py +0 -0
  206. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_typecheck_scope.py +0 -0
  207. {pytest_selective-1.3.0 → pytest_selective-1.5.0}/tests/testing/test_working_tree.py +0 -0
@@ -25,27 +25,6 @@
25
25
  "src/**",
26
26
  "tests/**"
27
27
  ]
28
- },
29
- "contracts": {
30
- "roots": [
31
- "contracts"
32
- ],
33
- "inputs": [
34
- "src/**",
35
- "contracts/**",
36
- "tests/testing/git_repo.py"
37
- ]
38
- },
39
- "package": {
40
- "inputs": [
41
- "src/**",
42
- "pyproject.toml",
43
- "uv.lock",
44
- "README.md",
45
- "LICENSE",
46
- "scripts/check_metadata.py",
47
- "scripts/smoke_distribution.py"
48
- ]
49
28
  }
50
29
  },
51
30
  "global_inputs": [
@@ -56,6 +35,7 @@
56
35
  "pyproject.toml",
57
36
  "**/uv.lock",
58
37
  "uv.lock",
38
+ ".github/plan-lanes.json",
59
39
  ".github/test-lanes.json",
60
40
  ".github/workflows/ci.yml",
61
41
  ".github/workflows/release.yml",
@@ -0,0 +1,22 @@
1
+ {
2
+ "extends": "workflow",
3
+ "lanes": {
4
+ "tests": {
5
+ "roots": ["tests"]
6
+ }
7
+ },
8
+ "rules": [
9
+ {
10
+ "name": "python-environment",
11
+ "when": ["pyproject.toml", "**/pyproject.toml", "uv.lock", "**/uv.lock", ".python-version"],
12
+ "select": ["all"],
13
+ "reason": "the locked test environment; documentation/requirements*.txt belongs to Read the Docs"
14
+ },
15
+ {
16
+ "name": "distribution-smoke",
17
+ "when": ["scripts/smoke_distribution.py"],
18
+ "select": ["lane:tests"],
19
+ "reason": "the distributions job runs the smoke only when the tests lane is planned"
20
+ }
21
+ ]
22
+ }
@@ -13,6 +13,9 @@ concurrency:
13
13
  group: ci-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.run_attempt == 1 && github.event.pull_request.number || github.run_id }}
14
14
  cancel-in-progress: ${{ github.event_name == 'pull_request' && github.run_attempt == 1 }}
15
15
  jobs:
16
+ # Plans whole lanes before any runner starts: each lane is the name a job's condition reads, and a change selects it
17
+ # through the tests, scripts and actions that job runs. `ci-esta lanes` shows the lanes and why; .github/test-lanes.json
18
+ # holds only what the workflow cannot show.
16
19
  select-tests:
17
20
  runs-on: ubuntu-latest
18
21
  permissions:
@@ -31,7 +34,7 @@ jobs:
31
34
  CONTRACT_LANE_JOBS: '{"selection":"Selection contract (Quint)"}'
32
35
  outputs:
33
36
  # Planned lanes after reuse: a reusing lane is false only when every one of its jobs is proven by a receipt.
34
- lanes: ${{ steps.admit.outputs.lanes }}
37
+ lanes: ${{ steps.contract-evidence.outputs.admitted-lanes || steps.plan.outputs.lanes }}
35
38
  # The plan before reuse, for steps that follow changed runtime inputs rather than test execution.
36
39
  planned: ${{ steps.plan.outputs.lanes }}
37
40
  mode: ${{ steps.files.outputs.mode }}
@@ -49,9 +52,10 @@ jobs:
49
52
  if: github.event_name == 'pull_request'
50
53
  run: 'true'
51
54
  - id: plan
52
- uses: ./actions/plan-lanes
55
+ uses: gigaverse-app/ci-esta/actions/plan-work@v1
53
56
  with:
54
57
  config: .github/test-lanes.json
58
+ # File selection has no workflow graph, so it reads the complete lane file public-actions.yml plans with.
55
59
  - name: Share the repository graph and execution inputs with file selection
56
60
  env:
57
61
  PYTHONPATH: src
@@ -82,6 +86,8 @@ jobs:
82
86
  input-paths: ${{ steps.inputs.outputs.tests }}
83
87
  reuse-after-merge: 'true'
84
88
  reuse-version-only-release: 'true'
89
+ planned-lanes: ${{ steps.plan.outputs.lanes }}
90
+ lane-keys: '{"tests":["py311-ubuntu","py311-windows","py311-macos","py314-ubuntu","py314-windows","py314-macos","minimum"]}'
85
91
  - id: contract-evidence
86
92
  if: >-
87
93
  ${{ (github.event_name == 'pull_request' || github.event_name == 'push') &&
@@ -93,21 +99,9 @@ jobs:
93
99
  input-paths: ${{ steps.inputs.outputs.contracts }}
94
100
  reuse-after-merge: 'true'
95
101
  reuse-version-only-release: 'true'
96
- - id: admit
97
- name: Admit each reusing lane unless every job is proven
98
- env:
99
- PLANNED_LANES: ${{ steps.plan.outputs.lanes }}
100
- REUSED: ${{ steps.evidence.outputs.reused || '{}' }}
101
- CONTRACT_REUSED: ${{ steps.contract-evidence.outputs.reused || '{}' }}
102
- run: |
103
- admitted=$(jq -c \
104
- --argjson tests "$TEST_LANE_JOBS" --argjson reused "$REUSED" \
105
- --argjson contracts "$CONTRACT_LANE_JOBS" --argjson contract_reused "$CONTRACT_REUSED" '
106
- def proven($jobs; $proof): [$jobs | keys[] | $proof[.] == true] | all;
107
- (if proven($tests; $reused) then .tests = false else . end)
108
- | (if proven($contracts; $contract_reused) then .contracts = false else . end)' <<<"$PLANNED_LANES")
109
- echo "reused=$REUSED contracts=$CONTRACT_REUSED"
110
- echo "lanes=$admitted" | tee -a "$GITHUB_OUTPUT"
102
+ # Chained: the tests step already admitted its lane; this step admits the contracts lane on top.
103
+ planned-lanes: ${{ steps.evidence.outputs.admitted-lanes }}
104
+ lane-keys: '{"contracts":["selection"]}'
111
105
  quality:
112
106
  runs-on: ubuntu-latest
113
107
  steps:
@@ -119,11 +113,18 @@ jobs:
119
113
  - run: uv run --locked pre-commit run --all-files
120
114
  env:
121
115
  SKIP: dry-lint
116
+ # Each lane-gated job's condition, the gate's lane-jobs and the lanes the workflow defines must agree.
122
117
  - name: Check aggregate gate against the execution lanes
123
118
  run: >-
124
119
  uv run --no-project --python 3.14
125
- --with "ci-esta>=1.1.0,<2.0.0"
120
+ --with "ci-esta[selection]>=1.13.0,<2.0.0"
126
121
  ci-esta check-workflow --workflow .github/workflows/ci.yml --lanes .github/test-lanes.json
122
+ # Plans one-file changes with CI-esta against real commits and fails on any lane a change should not run.
123
+ - name: Check the lane routing
124
+ run: >-
125
+ uv run --no-project --python 3.14
126
+ --with "ci-esta[selection]>=1.13.0,<2.0.0"
127
+ python scripts/check_lane_routing.py
127
128
  - name: Validate GitHub Actions
128
129
  run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.7 -color
129
130
  tests:
@@ -15,10 +15,12 @@ jobs:
15
15
  - uses: actions/checkout@v7
16
16
  with:
17
17
  fetch-depth: 0
18
+ # This workflow smokes the repository's own actions, so its planner is plan-lanes itself, run selectively on every
19
+ # change against .github/plan-lanes.json; CI's lanes come from CI-esta's plan-work.
18
20
  - id: select
19
21
  uses: ./actions/plan-lanes
20
22
  with:
21
- config: .github/test-lanes.json
23
+ config: .github/plan-lanes.json
22
24
  artifact-name: public-action-selection
23
25
  public-actions:
24
26
  needs: select-tests
@@ -35,7 +37,7 @@ jobs:
35
37
  python - <<'PY'
36
38
  import json
37
39
  from pathlib import Path
38
- graph = json.loads(Path('.github/test-lanes.json').read_text())['graph']
40
+ graph = json.loads(Path('.github/plan-lanes.json').read_text())['graph']
39
41
  Path('smoke-impact.json').write_text(json.dumps(graph))
40
42
  PY
41
43
  - uses: ./
@@ -57,7 +59,7 @@ jobs:
57
59
  - uses: ./actions/plan-lanes
58
60
  id: lanes
59
61
  with:
60
- config: .github/test-lanes.json
62
+ config: .github/plan-lanes.json
61
63
  base: ${{ github.sha }}
62
64
  full: 'true'
63
65
  artifact-name: ''
@@ -68,7 +70,7 @@ jobs:
68
70
  python - <<'PY'
69
71
  import json, os
70
72
  # A full checkpoint admits every configured lane.
71
- configured = json.load(open('.github/test-lanes.json', encoding='utf-8'))['lanes']
73
+ configured = json.load(open('.github/plan-lanes.json', encoding='utf-8'))['lanes']
72
74
  assert json.loads(os.environ['LANES']) == dict.fromkeys(configured, True)
73
75
  PY
74
76
  - name: Require successful planning after fallback execution
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.5.0"
3
+ }
@@ -19,9 +19,10 @@ on pytest-selective itself.
19
19
  - Planning supports committed revisions and explicit working-tree snapshots. Preserve full execution on
20
20
  operational uncertainty and diagnostics for invalid declarations. An empty
21
21
  selection is not an empty pytest argument list.
22
- - Select consumer routing regressions by their declared execution/configuration inputs. Update
23
- `.github/test-lanes.json` when adding execution inputs; preserve explicit full/manual
24
- checkpoints and the `test:full` override.
22
+ - Select consumer routing regressions by their declared execution/configuration inputs. Declare a
23
+ file a test or CI script reads by path next to its reader; `.github/test-lanes.json` holds only
24
+ what CI-esta cannot see in the workflow, and `scripts/check_lane_routing.py` pins the lanes. Preserve
25
+ explicit full/manual checkpoints and the `test:full` override.
25
26
  - The README's performance numbers are scenario-specific observations. Keep
26
27
  claims linked to [raw evidence](docs/benchmarks/2026-10-22-routing.md); distinguish
27
28
  runner-seconds, elapsed time and billing. Skill/example edits need usable links
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.0](https://github.com/gigaverse-app/pytest-selective/compare/v1.4.0...v1.5.0) (2026-11-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * **declarations:** share reader-relative dependency rules ([#139](https://github.com/gigaverse-app/pytest-selective/issues/139)) ([7eea4ba](https://github.com/gigaverse-app/pytest-selective/commit/7eea4ba3ec3fd4607acf1e52b171e6eabd0c96fc))
9
+
10
+ ## [1.4.0](https://github.com/gigaverse-app/pytest-selective/compare/v1.3.0...v1.4.0) (2026-11-09)
11
+
12
+
13
+ ### Features
14
+
15
+ * **lanes:** partition test roots by filename patterns ([27c10ec](https://github.com/gigaverse-app/pytest-selective/commit/27c10ec608f052a36f58d9b10730048ab9d38598))
16
+
3
17
  ## [1.3.0](https://github.com/gigaverse-app/pytest-selective/compare/v1.2.0...v1.3.0) (2026-11-09)
4
18
 
5
19
 
@@ -65,6 +65,7 @@ uv run --group quint pytest-quint-connect report '**/.artifacts/quint-connect/re
65
65
  | Private working-tree snapshots, staged inputs and index timestamp preservation | `working_tree.py` |
66
66
  | Python module names, Unicode matching and dotted import relationships | `ast_imports.py` |
67
67
  | Test-file discovery and cache exclusions | `test_inventory.py` |
68
+ | Reader-owned declaration grammar, path expansion and dependency kinds | `impact_declarations.py`, with the canonical models in `impact_models.py` |
68
69
  | Repository-relative path containment, including the dot root | `impact_graph.within` |
69
70
 
70
71
  The expansion guard imports the first three owners directly when run as a script;
@@ -98,24 +99,47 @@ limitations. The pull request template provides a short checklist. See
98
99
 
99
100
  ## CI lane selection
100
101
 
101
- This repository uses its own `plan-lanes` action. Documentation-only PRs keep
102
+ CI plans whole lanes with [CI-esta](https://github.com/gigaverse-app/ci-esta)'s
103
+ `plan-work` before any runner starts: `tests` (the Python matrix and
104
+ `pytest-minimum`), `contracts` (the Quint selection contract) and `package` (the
105
+ distribution build), the names the jobs' conditions read. A change selects a lane
106
+ through what its jobs run: the tests that import it, the files tests declare they
107
+ read, and the scripts and actions each job executes. Documentation-only PRs keep
102
108
  quality checks (including Markdown links) and the documentation build, while
103
- skipping every test lane and the distribution build. A README or LICENSE change
104
- also runs the `package` lane, because both distributions embed them. Within an
105
- admitted lane, the repository's public file-selection action selects affected
106
- test files once; the platform matrix and minimum-pytest job execute that same
107
- plan. CI-esta receives both lane and file explanations, learns JUnit timings,
108
- and validates the aggregate gate. Source and test edits follow dependencies;
109
- scripts, dependency/pytest policy and execution workflow changes retain full
110
- validation. Manual/called/release runs and the `test:full` label run it in full; a
111
- failed planner also runs all tests. Installed-distribution smoke checks use the
112
- same runtime-input decision; README-only changes skip those checks while still
113
- verifying the README embedded in both release archives. Repository routing
114
- controls exercise the committed config against real Git trees, including full and missing-history
115
- controls. Add new non-import execution inputs to `.github/test-lanes.json`.
116
-
117
- File planning derives its graph from that same lane config and merges its declared
118
- global execution inputs with the graph's environment inputs. Declare subprocess
109
+ skipping every lane. A README or LICENSE change runs the `package` lane, because
110
+ both distributions embed them; a README change also runs `contracts`, whose job
111
+ checks the README's contract report, and the one test whose metadata check reads it. `pyproject.toml` and `uv.lock` run every
112
+ lane. `ci-esta lanes --base origin/main` shows what a change would run and why.
113
+ [.github/test-lanes.json](.github/test-lanes.json) extends the workflow and holds
114
+ only what it cannot show: the `tests` lane's roots (its jobs run `pytest-selective
115
+ execute-plan`, which CI-esta does not read as a pytest command), a
116
+ `python-environment` rule that leaves the documentation site's requirements out,
117
+ and a rule that plans the tests lane when the installed-distribution smoke script
118
+ changes, because `distributions` smokes only when that lane is planned. When a test
119
+ or a script starts reading a file by path, declare it next to the reader (a
120
+ `# gv:test-dependency: PATH` comment for simple inputs, or a directory
121
+ `.gv-dependencies.yaml` with `relative_to: reader` for repeated local layouts).
122
+ Keep per-reader sidecars for exceptions needing separate declarations. Review
123
+ `pytest-selective audit --write-declarations` output for shared conventions before
124
+ retaining its individual sidecars; preserve `content` versus `import` semantics.
125
+ `quality` runs [scripts/check_lane_routing.py](scripts/check_lane_routing.py),
126
+ which plans one-file changes with CI-esta against real Git commits, including a
127
+ full checkpoint, and `ci-esta check-workflow`, which keeps the job conditions, the
128
+ gate and the lane file in agreement. Manual/called/release runs and the
129
+ `test:full` label run everything; a failed planner also runs all tests.
130
+ Installed-distribution smoke checks follow the tests lane's plan; README-only
131
+ changes skip them while still verifying the README embedded in both release
132
+ archives.
133
+
134
+ Within an admitted lane, the repository's public file-selection action selects
135
+ affected test files once; the platform matrix and minimum-pytest job execute that
136
+ same plan. CI-esta receives both lane and file explanations, learns JUnit timings,
137
+ and validates the aggregate gate. File selection has no workflow graph, so it reads
138
+ [.github/plan-lanes.json](.github/plan-lanes.json), a complete lane file: its graph
139
+ plus the execution inputs that run every test file (scripts, workflows, actions,
140
+ dependency and pytest policy). `public-actions.yml` plans with the same file through
141
+ this repository's own `plan-lanes` action, selectively on every change, and smokes
142
+ its full checkpoint. Declare subprocess
119
143
  and non-import dependencies at their readers. Full plans collect whole-suite
120
144
  coverage; scoped plans produce JUnit timing evidence without presenting partial
121
145
  coverage as a whole-suite result. The public [shared-plan executor](docs/shared-plan-execution.md)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pytest-selective
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: Explainable Python test impact analysis: select affected pytest files from Git changes
5
5
  Project-URL: Homepage, https://github.com/gigaverse-app/pytest-selective
6
6
  Project-URL: Documentation, https://pytest-selective.readthedocs.io/en/latest/
@@ -0,0 +1,5 @@
1
+ dependencies:
2
+ - inputs:
3
+ - "//contracts/selection/runtime_deps.py"
4
+ kind: import
5
+ reason: "The runtime referee, run by path in a subprocess (python -I -B runtime_deps.py) for every history."
@@ -0,0 +1,6 @@
1
+ dependencies:
2
+ - inputs:
3
+ - "//contracts/selection/quint-connect.json"
4
+ - "//contracts/selection/selection.qnt"
5
+ kind: content
6
+ reason: "quint_contract() reads the contract file, which names the Quint model it runs."
@@ -36,7 +36,14 @@ PR, on a version-only release PR, and on a PR push that changes nothing the
36
36
  tests read. A `PR evidence #N head SHA` step ties each PR run to its pull
37
37
  request, because GitHub often leaves that link out. The distribution smoke test
38
38
  still follows the plan before reuse (`planned`), and partial proof runs the
39
- whole lane.
39
+ whole lane. The contract lane works the same way with its own receipt and inputs
40
+ (including `README.md`, which its README check reads).
41
+
42
+ CI-esta decides admission: each evidence step gets the planned lanes and
43
+ `lane-keys`, the keys of every job a lane gates, and reports `admitted-lanes`.
44
+ The contract step chains on the tests step's result. `ci-esta check-workflow`
45
+ then proves `lane-keys` name every job each lane gates, matrix cells included,
46
+ so a newly added cell can never be skipped as proven.
40
47
 
41
48
  The unconditional CI job runs `ci-esta check-workflow` against this workflow and
42
49
  lane configuration, so renamed lanes or omitted metadata prerequisites cannot
@@ -285,3 +285,27 @@ The action also uploads the report as the `test-lane-selection` artifact; set
285
285
  pull-request sticky's lane section: which lanes ran and why, and the runner time
286
286
  the skipped jobs took on the default branch's last full run. See
287
287
  [how the two fit together in a Python project](https://github.com/gigaverse-app/ci-esta/blob/main/docs/integrations.md#getting-the-most-from-both-in-a-python-project).
288
+
289
+ ## Split whole lanes by test filename
290
+
291
+ Lanes sharing a directory can partition its tests with `test_patterns` and
292
+ `exclude_patterns`. Both match repository-relative POSIX paths using the same
293
+ globs as lane inputs; `*` also matches path separators. Roots still limit the
294
+ search. The defaults include every test under the roots and exclude none.
295
+
296
+ ```json
297
+ {
298
+ "unit": {"roots": ["tests"], "exclude_patterns": ["*_integration.py"]},
299
+ "integration": {"roots": ["tests"], "test_patterns": ["*_integration.py"]}
300
+ }
301
+ ```
302
+
303
+ Put these entries under `lanes` in the lane configuration. A source dependency
304
+ still selects every lane containing an affected test. Deleted tests use their
305
+ base-revision membership; an affected test claimed by no lane causes a full
306
+ fallback. Explicit inputs and full checkpoints are unaffected by these filters.
307
+
308
+ Same-named modules in independent module roots are unrelated unless an import
309
+ connects them. An actual ambiguous bare import conservatively retains every
310
+ possible target. Use qualified imports to make independent demo modules
311
+ unambiguous; selection does not guess a runtime import order.
@@ -89,6 +89,12 @@ root. `content` follows the file's bytes; `import` also follows its dependencies
89
89
  The default is `import`. Inputs must match tracked files. Invalid patterns,
90
90
  missing matches, duplicate YAML keys, and aliases are errors to repair.
91
91
 
92
+ For repeated conventions, a directory `.gv-dependencies.yaml` can match `readers`
93
+ and set `relative_to: reader`: each unanchored input then resolves beside each
94
+ matched reader. The default remains `relative_to: manifest`, and `//` always
95
+ anchors at the repository root. This keeps sibling readers' inputs separate;
96
+ see [shared conventions](tutorials/sidecar-dependencies.md#share-a-convention-without-sharing-every-input).
97
+
92
98
  Declare lock files, environment manifests, and other suite-wide inputs explicitly:
93
99
 
94
100
  ```json
@@ -29,8 +29,11 @@ uv run pytest-selective audit --config .github/test-impact.json --records impact
29
29
 
30
30
  The recorder observes real file reads in the tests you execute. The audit can
31
31
  expose a JSON fixture, template or schema that the graph did not declare. Add an
32
- edge at the reader using a dependency comment or YAML sidecar, then recheck the
33
- route. `audit --write-declarations` writes those sidecars for you; see
32
+ edge at the reader using a dependency comment, shared directory manifest or YAML
33
+ sidecar, then recheck the route. Prefer a shared `relative_to: reader` convention
34
+ for repeated layouts, preserving each reader's own input scope and dependency kind.
35
+ `audit --write-declarations` writes individual sidecars as a starting point;
36
+ review repeated patterns before keeping them. Existing shared rules satisfy the audit. See
34
37
  [write the missing declarations](configuration.md#write-the-missing-declarations). [Work through the sidecar tutorial](tutorials/sidecar-dependencies.md), or consult
35
38
  [declaration syntax](configuration.md#declare-inputs-that-imports-cannot-describe).
36
39
  The audit covers observed paths; keep representative full runs for paths it has
@@ -100,6 +100,9 @@ dependencies:
100
100
  - [ ] Use `content` for bytes read as data, and `import` when the reader executes
101
101
  Python and needs that module's transitive dependencies.
102
102
  - [ ] Keep declarations beside readers; use central rules for actual shared policy.
103
+ - [ ] Replace repeated per-reader layouts with directory manifest rules using
104
+ `relative_to: reader`; compare affected readers and lanes before and after,
105
+ including sibling inputs, new matching files and deletions.
103
106
  - [ ] Review locks, environment configuration, external checkout pins and plugin startup.
104
107
  - [ ] Retain default global inputs until a narrower environment contract is verified.
105
108
  - [ ] Review nested/custom pytest configuration before refreshing fingerprints.
@@ -20,7 +20,7 @@ cd selective-sidecar-demo
20
20
  git init
21
21
  python -m venv .venv
22
22
  source .venv/bin/activate
23
- python -m pip install "pytest-selective>=0.3,<0.4"
23
+ python -m pip install "pytest-selective>=1,<2"
24
24
  mkdir -p tests/data
25
25
  ```
26
26
 
@@ -142,7 +142,7 @@ change should select its reader. There is no universal documentation exemption.
142
142
  | --- | --- | --- |
143
143
  | Dependency comment | In the reading Python file | One or two inputs fit naturally beside the code |
144
144
  | File sidecar | `reader.py.gv-dependency.yaml` | You need an explicit dependency kind or want declarations outside Python |
145
- | Directory manifest | `.gv-dependencies.yaml` | Several related readers share inputs |
145
+ | Directory manifest | `.gv-dependencies.yaml` | Several related readers share inputs or a per-reader layout convention |
146
146
 
147
147
  Instead of the sidecar above, a comment in `tests/test_prices.py` can declare:
148
148
 
@@ -172,6 +172,46 @@ are relative to its directory. `//` anchors either pattern at the repository
172
172
  root, for example `//schemas/receipt.json`. Do not use `../` to reach another
173
173
  directory.
174
174
 
175
+ ### Share a convention without sharing every input
176
+
177
+ When each reader consumes files beside itself, use `relative_to: reader` in a
178
+ directory manifest. For example, `contracts/.gv-dependencies.yaml` can replace
179
+ one sidecar per contract:
180
+
181
+ ```yaml
182
+ dependencies:
183
+ - readers: ["**/test_*_contract.py"]
184
+ relative_to: reader
185
+ inputs: ["model.qnt", "quint-connect.json", "history/**", "//contracts/quint.ini"]
186
+ kind: content
187
+ reason: Each contract reads its own model, manifest and history under the shared pytest configuration.
188
+ ```
189
+
190
+ The `readers` pattern is still relative to the manifest. Each unanchored input
191
+ is resolved separately beside each matched reader; `//` still means the repository
192
+ root. Changing one contract's model selects its readers, without connecting sibling
193
+ contracts to that model. New matching readers inherit the same convention. Every
194
+ input pattern must match for **each** reader; a missing history directory in one
195
+ contract is an error even if a sibling has one. Omit inputs your convention does
196
+ not require, or put distinct conventions in separate rules.
197
+
198
+ The default, `relative_to: manifest`, preserves existing behavior. Both modes
199
+ support `content` and `import`; a file sidecar has the same directory as its reader,
200
+ so either mode means the same thing there. Expansion is reused for readers in the
201
+ same directory within a rule and rebuilt against each Git tree, including added
202
+ files and deleted inputs. Editing the shared manifest selects all its readers;
203
+ keep unrelated conventions in separate manifests.
204
+
205
+ For a source scanner, the same mechanism can declare `readers: ["*/test_inventory.py"]`
206
+ with `inputs: ["**/*.py"]`, `relative_to: reader` and `kind: content`. Each inventory
207
+ then follows only the Python source under its own directory. Prefer this to a broad
208
+ rule joining every scanner to every project's source.
209
+
210
+ Keep the convention where its readers live. A project-specific layout belongs in
211
+ its manifest; it does not need a framework-specific plugin in the selector. Review
212
+ `audit --write-declarations` output for repeated layouts before retaining individual
213
+ sidecars. An existing shared rule already satisfies the audit for covered reads.
214
+
175
215
  ### Reading content versus executing Python
176
216
 
177
217
  | Input use | Kind | What changes can select the reader |