pytest-selective 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/ci.yml +0 -2
  2. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/pr-title.yml +2 -1
  3. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/public-actions.yml +0 -2
  4. pytest_selective-1.3.0/.github/workflows/weekly-checkpoint.yml +21 -0
  5. pytest_selective-1.3.0/.release-please-manifest.json +3 -0
  6. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/CHANGELOG.md +14 -0
  7. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/CONTRIBUTING.md +8 -3
  8. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/PKG-INFO +1 -1
  9. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/action.yml +6 -0
  10. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/plan-lanes/action.yml +8 -0
  11. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/test-impact/action.yml +8 -0
  12. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/ci.md +8 -0
  13. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/configuration.md +8 -0
  14. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/pyproject.toml +1 -1
  15. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/skills/pytest-selective/SKILL.md +71 -18
  16. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/ci.py +24 -1
  17. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/git_operations.py +23 -0
  18. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_audit_declarations.py +13 -2
  19. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_cli.py +29 -4
  20. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_graph.py +37 -12
  21. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/local.py +3 -5
  22. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_repository_ci.py +12 -1
  23. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_audit.py +7 -1
  24. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_audit_declarations.py +10 -0
  25. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_graph.py +83 -0
  26. pytest_selective-1.3.0/tests/testing/test_pull_request_merge_base.py +83 -0
  27. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_working_tree.py +10 -0
  28. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/uv.lock +1 -1
  29. pytest_selective-1.2.0/.release-please-manifest.json +0 -3
  30. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.dry-lint/baseline.json +0 -0
  31. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.dry-lint/progress.json +0 -0
  32. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.dry-lint.toml +0 -0
  33. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.gitattributes +0 -0
  34. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  35. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  36. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/dependabot.yml +0 -0
  37. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/test-lanes.json +0 -0
  38. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/validation-inputs.json +0 -0
  39. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/docs.yml +0 -0
  40. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/dry-lint-audit.yml +0 -0
  41. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/dry-lint-judge.yml +0 -0
  42. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/dry-lint.yml +0 -0
  43. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/observe-ci.yml +0 -0
  44. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/release-please.yml +0 -0
  45. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/release.yml +0 -0
  46. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/windows-performance.yml +0 -0
  47. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.gitignore +0 -0
  48. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.pre-commit-config.yaml +0 -0
  49. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.readthedocs.yaml +0 -0
  50. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/AGENTS.md +0 -0
  51. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/CLAUDE.md +0 -0
  52. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/LICENSE +0 -0
  53. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/PROVENANCE.md +0 -0
  54. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/README.md +0 -0
  55. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/RELEASING.md +0 -0
  56. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/README.md +0 -0
  57. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/pytest-expansion/action.yml +0 -0
  58. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/validation-gate/action.yml +0 -0
  59. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/agentsync.toml +0 -0
  60. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/README.md +0 -0
  61. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/conftest.py +0 -0
  62. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/quint-connect.json +0 -0
  63. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/quint.ini +0 -0
  64. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/runtime_deps.py +0 -0
  65. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/selection.qnt +0 -0
  66. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/selection_driver.py +0 -0
  67. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/test_selection_contract.py +0 -0
  68. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/README.md +0 -0
  69. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/actions.md +0 -0
  70. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/agents.md +0 -0
  71. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/banner.png +0 -0
  72. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/banner.svg +0 -0
  73. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/brand.json +0 -0
  74. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/icon.png +0 -0
  75. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/logo.svg +0 -0
  76. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/raster-sources.json +0 -0
  77. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/social-preview.png +0 -0
  78. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/social-preview.svg +0 -0
  79. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-due-ci.json +0 -0
  80. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-due-docs-ci.json +0 -0
  81. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-due-docs-widget.json +0 -0
  82. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-due-widget.json +0 -0
  83. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-input-replays.json +0 -0
  84. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-obligation-docs.json +0 -0
  85. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-obligation.json +0 -0
  86. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-routing.md +0 -0
  87. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-selective-docs.json +0 -0
  88. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22.md +0 -0
  89. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/obligation-baseline.json +0 -0
  90. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.11.xml +0 -0
  91. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.14.xml +0 -0
  92. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-after.json +0 -0
  93. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.11.xml +0 -0
  94. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.14.xml +0 -0
  95. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-control.json +0 -0
  96. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-earlier.json +0 -0
  97. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor.md +0 -0
  98. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/windows-runtime.md +0 -0
  99. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/ci-toolkit.md +0 -0
  100. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/comparison-pytest-impacted.md +0 -0
  101. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/evidence.md +0 -0
  102. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/local-development.md +0 -0
  103. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/pytest-hygiene.md +0 -0
  104. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/selection-health.md +0 -0
  105. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/shared-plan-execution.md +0 -0
  106. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/tutorial.md +0 -0
  107. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/tutorials/adoption-checklist.md +0 -0
  108. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/tutorials/sidecar-dependencies.md +0 -0
  109. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/tutorials/skip-unrelated-ci.md +0 -0
  110. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/actions-directory-test-plan.md +0 -0
  111. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/codex-selector-correctness-performance-test-plan.md +0 -0
  112. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/codex-windows-git-performance-test-plan.md +0 -0
  113. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/graph-based-inputs-plan.md +0 -0
  114. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/impacted-improvements-test-plan.md +0 -0
  115. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/move-action-implementations-test-plan.md +0 -0
  116. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/public-action-onboarding-test-plan.md +0 -0
  117. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/ratchet-duplicates-zero-test-plan.md +0 -0
  118. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/rerun-selection-test-plan.md +0 -0
  119. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/assets/routing-comparison.svg +0 -0
  120. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/check_site.py +0 -0
  121. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/ecosystem.md +0 -0
  122. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/hooks.py +0 -0
  123. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/index.md +0 -0
  124. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/llms.txt +0 -0
  125. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/overrides/main.html +0 -0
  126. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/publishing.md +0 -0
  127. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/requirements.in +0 -0
  128. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/requirements.txt +0 -0
  129. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/mkdocs.yml +0 -0
  130. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/release-please-config.json +0 -0
  131. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/benchmark_graph.py +0 -0
  132. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/benchmark_parsing.py +0 -0
  133. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/benchmark_rust.py +0 -0
  134. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/check_docs.py +0 -0
  135. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/check_metadata.py +0 -0
  136. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/ci_file_config.py +0 -0
  137. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/compare_impacted.py +0 -0
  138. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/measure_ci_runs.py +0 -0
  139. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/public_actions.py +0 -0
  140. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/render_brand.py +0 -0
  141. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/run_ci_tests.py +0 -0
  142. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/smoke_distribution.py +0 -0
  143. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/tutorial.py +0 -0
  144. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/skills/pytest-selective/agents/openai.yaml +0 -0
  145. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/__init__.py +0 -0
  146. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/__main__.py +0 -0
  147. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/ast_imports.py +0 -0
  148. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/checkpoint.py +0 -0
  149. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/execution.py +0 -0
  150. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/gate.py +0 -0
  151. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/hygiene.py +0 -0
  152. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_audit.py +0 -0
  153. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_audit_plugin.py +0 -0
  154. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_declarations.py +0 -0
  155. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_explanation.py +0 -0
  156. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_first_party.py +0 -0
  157. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_models.py +0 -0
  158. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_name_scope.py +0 -0
  159. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_selection.py +0 -0
  160. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/input_policy.py +0 -0
  161. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/parse_batch.py +0 -0
  162. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/plugin.py +0 -0
  163. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/py.typed +0 -0
  164. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/pytest_expansion.py +0 -0
  165. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/pytest_plugins.py +0 -0
  166. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/pytest_sharding.py +0 -0
  167. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/test_inventory.py +0 -0
  168. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/typecheck_scope.py +0 -0
  169. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/working_tree.py +0 -0
  170. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/__init__.py +0 -0
  171. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_checkpoint_policy.py +0 -0
  172. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_ci_admission.py +0 -0
  173. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_ci_measurements.py +0 -0
  174. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_ci_test_execution.py +0 -0
  175. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_distribution_metadata.py +0 -0
  176. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_hygiene.py +0 -0
  177. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_public_actions.py +0 -0
  178. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_pytest_expansion.py +0 -0
  179. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_release_workflow.py +0 -0
  180. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_repository_hygiene.py +0 -0
  181. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_repository_hygiene.py.gv-dependency.yaml +0 -0
  182. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_tutorial.py +0 -0
  183. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/__init__.py +0 -0
  184. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/git_repo.py +0 -0
  185. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_ast_imports.py +0 -0
  186. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_ci.py +0 -0
  187. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_gate.py +0 -0
  188. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_git_repo.py +0 -0
  189. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_global_inputs.py +0 -0
  190. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_explanation.py +0 -0
  191. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_first_party.py +0 -0
  192. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_name_scope.py +0 -0
  193. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_parse_batch.py +0 -0
  194. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_portability.py +0 -0
  195. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_pytest_plugin.py +0 -0
  196. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_pytest_plugins.py +0 -0
  197. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_pytest_sharding.py +0 -0
  198. {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_typecheck_scope.py +0 -0
@@ -6,8 +6,6 @@ on:
6
6
  types: [opened, synchronize, reopened]
7
7
  workflow_dispatch:
8
8
  workflow_call:
9
- schedule:
10
- - cron: '13 3 * * 1'
11
9
  permissions:
12
10
  contents: read
13
11
  concurrency:
@@ -2,7 +2,8 @@ name: PR title
2
2
 
3
3
  # Squash merges use this checked title as the Conventional Commit header.
4
4
  on:
5
- pull_request_target:
5
+ # Metadata reads need no privileged target event or repository secrets.
6
+ pull_request:
6
7
  types: [opened, edited, synchronize, reopened, ready_for_review]
7
8
 
8
9
  permissions:
@@ -4,8 +4,6 @@ on:
4
4
  push:
5
5
  branches: [main]
6
6
  workflow_dispatch:
7
- schedule:
8
- - cron: '33 3 * * 1'
9
7
  permissions:
10
8
  contents: read
11
9
  jobs:
@@ -0,0 +1,21 @@
1
+ name: Weekly full checkpoint
2
+ 'on':
3
+ schedule:
4
+ - cron: 13 3 * * 1
5
+ workflow_dispatch: null
6
+ permissions:
7
+ contents: read
8
+ actions: write
9
+ concurrency:
10
+ group: weekly-full-checkpoint
11
+ cancel-in-progress: false
12
+ jobs:
13
+ dispatch:
14
+ runs-on: ubuntu-latest
15
+ timeout-minutes: 5
16
+ steps:
17
+ - uses: gigaverse-app/ci-esta/actions/weekly-checkpoint@v1
18
+ with:
19
+ workflows: '["ci.yml", "public-actions.yml"]'
20
+ ref: ''
21
+ workflow-inputs: '{}'
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.3.0"
3
+ }
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0](https://github.com/gigaverse-app/pytest-selective/compare/v1.2.0...v1.3.0) (2026-11-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * **audit:** show how many tests each written declaration selects ([#124](https://github.com/gigaverse-app/pytest-selective/issues/124)) ([b660301](https://github.com/gigaverse-app/pytest-selective/commit/b660301be82aefb04fdc79202dc83067c28d7eb1))
9
+
10
+
11
+ ### Bug fixes
12
+
13
+ * agent setup round 3, from a third adoption trial ([#127](https://github.com/gigaverse-app/pytest-selective/issues/127)) ([a82bea9](https://github.com/gigaverse-app/pytest-selective/commit/a82bea9f607abcb3ed68009571202ad8b7726c7e))
14
+ * **selection:** keep every module a pytest11 entry-point name maps to ([#122](https://github.com/gigaverse-app/pytest-selective/issues/122)) ([acb6649](https://github.com/gigaverse-app/pytest-selective/commit/acb66492c6440621387e0506c1d0071a5a48979c))
15
+ * **selection:** plan a pull request against its merge commit's actual base ([#119](https://github.com/gigaverse-app/pytest-selective/issues/119)) ([276cec8](https://github.com/gigaverse-app/pytest-selective/commit/276cec84f65c889a1820fd5a001a3a4aca8b7e0d))
16
+
3
17
  ## [1.2.0](https://github.com/gigaverse-app/pytest-selective/compare/v1.1.2...v1.2.0) (2026-11-09)
4
18
 
5
19
 
@@ -199,14 +199,19 @@ history or failed planning must execute the full affected lanes and fail the
199
199
  required planning gate. Ordinary PRs and pushes use selection; manual/release/scheduled/full overrides
200
200
  remain complete checkpoints.
201
201
 
202
- CI and public-action smoke run full checkpoints every Monday. Ordinary runs keep
202
+ The **Weekly full checkpoint** workflow dispatches CI and public-action smoke
203
+ every Monday using [CI-esta's shared action](https://github.com/gigaverse-app/ci-esta/blob/main/docs/weekly-checkpoints.md).
204
+ Its small wrapper is generated by `ci-esta checkpoint init`; the source guide
205
+ documents schedule, workflow, ref and input overrides. Ordinary runs keep
203
206
  the shared affected-file plan across all six OS/Python combinations and minimum
204
207
  pytest. `tests/test_tutorial.py` owns the end-to-end tutorial execution and follows
205
208
  that plan; there is no second tutorial invocation in each matrix job. JUnit retains
206
209
  its test result and timing, and full plans also upload whole-suite coverage.
207
210
 
208
- To run a complete checkpoint after a CI change, select **CI** in GitHub Actions
209
- and choose **Run workflow**. Retrying an ordinary PR run preserves its selection;
211
+ To run both complete checkpoints after a CI change, select **Weekly full checkpoint**
212
+ in GitHub Actions and choose **Run workflow**, then inspect the dispatched CI and
213
+ public-action results. A green dispatcher means both requests were accepted.
214
+ You can also dispatch either target directly. Retrying an ordinary PR run preserves its selection;
210
215
  use the manual checkpoint when you need full-suite coverage evidence.
211
216
 
212
217
  ## Duplication
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pytest-selective
3
- Version: 1.2.0
3
+ Version: 1.3.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/
@@ -14,6 +14,11 @@ inputs:
14
14
  head:
15
15
  description: Exact checked-out commit.
16
16
  default: HEAD
17
+ pr-head:
18
+ description: The pull request's head. When the checkout is GitHub's merge of it (refs/pull/N/merge), changes are measured
19
+ from that merge's base-branch parent, so commits that landed on the base branch after `base` are not counted as the
20
+ PR's. Empty (any other event) keeps `base`.
21
+ default: ${{ github.event.pull_request.head.sha }}
17
22
  full-roots:
18
23
  description: Space-separated complete test roots used if planning cannot prove a narrower scope.
19
24
  required: true
@@ -100,6 +105,7 @@ runs:
100
105
  config: ${{ inputs.config }}
101
106
  base: ${{ inputs.base }}
102
107
  head: ${{ inputs.head }}
108
+ pr-head: ${{ inputs.pr-head }}
103
109
  full-roots: ${{ inputs.full-roots }}
104
110
  full: ${{ inputs.full }}
105
111
  extra-files: ${{ inputs.extra-files }}
@@ -7,6 +7,12 @@ inputs:
7
7
  base:
8
8
  description: Base commit containing the complete change set.
9
9
  default: ${{ github.event.pull_request.base.sha || github.event.before }}
10
+ pr-head:
11
+ description: >-
12
+ The pull request's head. When the checkout is GitHub's merge of it (refs/pull/N/merge), changes are measured from
13
+ that merge's base-branch parent, so commits that landed on the base branch after `base` are not counted as the PR's.
14
+ Empty (any other event) keeps `base`.
15
+ default: ${{ github.event.pull_request.head.sha }}
10
16
  full:
11
17
  description: 'auto preserves selection on PR/push reruns and runs full for manual/scheduled/release checkpoints or test:full. Override with true or false.'
12
18
  default: auto
@@ -40,10 +46,12 @@ runs:
40
46
  SELECTIVE_CONFIG: ${{ inputs.config }}
41
47
  SELECTIVE_BASE: ${{ inputs.base }}
42
48
  SELECTIVE_FULL: ${{ inputs.full }}
49
+ SELECTIVE_PR_HEAD: ${{ inputs.pr-head }}
43
50
  run: |
44
51
  export PYTHONPATH="$SELECTIVE_ACTION/../../src${PYTHONPATH:+:$PYTHONPATH}"
45
52
  args=(--config "$SELECTIVE_CONFIG" --base "$SELECTIVE_BASE" --github-output "$GITHUB_OUTPUT")
46
53
  args+=(--full "$SELECTIVE_FULL")
54
+ if [ -n "$SELECTIVE_PR_HEAD" ]; then args+=(--pr-head "$SELECTIVE_PR_HEAD"); fi
47
55
  uv run --no-project --python 3.14 --with pydantic==2.13.4 --with pyyaml==6.0.3 \
48
56
  python -m pytest_selective.ci "${args[@]}" | tee test-lanes.json
49
57
  {
@@ -13,6 +13,12 @@ inputs:
13
13
  head:
14
14
  description: Exact checked-out commit.
15
15
  default: HEAD
16
+ pr-head:
17
+ description: >-
18
+ The pull request's head. When the checkout is GitHub's merge of it (refs/pull/N/merge), changes are measured from
19
+ that merge's base-branch parent, so commits that landed on the base branch after `base` are not counted as the PR's.
20
+ Empty (any other event) keeps `base`.
21
+ default: ${{ github.event.pull_request.head.sha }}
16
22
  full-roots:
17
23
  description: Space-separated complete test roots used if planning cannot prove a narrower scope.
18
24
  required: true
@@ -114,6 +120,7 @@ runs:
114
120
  IMPACT_CONFIG: ${{ inputs.config }}
115
121
  IMPACT_BASE: ${{ inputs.base }}
116
122
  IMPACT_HEAD: ${{ inputs.head }}
123
+ IMPACT_PR_HEAD: ${{ inputs.pr-head }}
117
124
  IMPACT_FULL: ${{ inputs.full }}
118
125
  IMPACT_EXTRA: ${{ inputs.extra-files }}
119
126
  IMPACT_ALTERNATE_BASE: ${{ inputs.alternate-base }}
@@ -147,6 +154,7 @@ runs:
147
154
  # A reused workspace must not upload a previous plan's explanation.
148
155
  rm -f test-impact-explanation.json
149
156
  args+=(--full "$IMPACT_FULL")
157
+ if [ -n "${IMPACT_PR_HEAD:-}" ]; then args+=(--pr-head "$IMPACT_PR_HEAD"); fi
150
158
  if [ -n "$IMPACT_EXTRA" ]; then args+=(--extra-files "$IMPACT_EXTRA"); fi
151
159
  if [ -n "${IMPACT_GRAPH_BASE:-}" ]; then args+=(--graph-base "$IMPACT_GRAPH_BASE"); fi
152
160
  # Optional: an unfetchable alternate only forgoes a smaller selection.
@@ -64,6 +64,14 @@ controls the test interpreter. `full-roots` must contain the complete roots, not
64
64
  the previous selection. `base` defaults to the PR base or the push's previous
65
65
  commit. Override it only when you have validated a different comparison base.
66
66
 
67
+ On a pull request, GitHub checks out a merge of the PR head into the base
68
+ branch's current tip, which can be newer than the PR's recorded base. Both
69
+ actions pass the PR head (`pr-head`) so the planner measures changes from that
70
+ merge's base-branch parent. Commits that landed on the base branch after the PR
71
+ was opened, such as a release's version bump, are then not counted as the PR's
72
+ own. A shallow checkout or any other event keeps `base`. Check out with
73
+ `fetch-depth: 0` to get the narrower plan.
74
+
67
75
  Both file and lane actions default to `full: auto`:
68
76
 
69
77
  | Trigger | Default |
@@ -22,6 +22,11 @@ select. The default filenames are `test_*.py`, `*_test.py`, `*_tests.py`, and
22
22
  `tests.py`; configure
23
23
  `test_patterns` for a different collection policy.
24
24
 
25
+ Every `--config` (`affected`, `explain`, `fingerprints`, `audit`, `run` and the rest) also accepts a
26
+ [lane config](ci.md) such as `.github/test-lanes.json`: it uses the lane config's `graph`, with its
27
+ top-level `global_inputs` when declared. Pass the lane config itself rather than a copy of its graph,
28
+ so a later `graph.pytest_policy` refresh is always the one in use.
29
+
25
30
  ## Review and record collection policy
26
31
 
27
32
  If your repository has `pyproject.toml`, `pytest.ini`, a `tox.ini` with a `[pytest]`
@@ -61,6 +66,9 @@ fixture example, directory manifests, dependency comments, and dynamic Python in
61
66
 
62
67
  A read that names a tracked file by literal path, such as `open("data/prices.json")` or
63
68
  `(Path(__file__).parent / "prices.json").read_text()`, is followed automatically as a content read.
69
+ A string that only names a tracked file, such as a routing test's `("bench.py", ...)` row, adds no
70
+ edge: a dotted string is followed as a possible module name (a patch target, a plugin) only when it
71
+ names no tracked file, from the repository root or beside its reader.
64
72
  For a path that is computed, or built from several parts, add a reader-owned comment:
65
73
 
66
74
  ```python
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-selective"
7
- version = "1.2.0"
7
+ version = "1.3.0"
8
8
  description = "Explainable Python test impact analysis: select affected pytest files from Git changes"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -56,7 +56,10 @@ A fresh fork has no runs yet. Read the typical jobs and durations from UPSTREAM
56
56
 
57
57
  Record:
58
58
 
59
- - **Conventions**: commit and PR title style, changelog rules, AI policy, pre-commit hooks.
59
+ - **Conventions**: commit and PR title style, changelog rules, AI policy, pre-commit hooks. When the pull request
60
+ template names a CONTRIBUTING, CODE_OF_CONDUCT or security file the repository lacks, read the owner's organization
61
+ default from its `.github` repository (in a fork, the upstream owner's):
62
+ `gh api "repos/OWNER/.github/contents/CONTRIBUTING.md" --jq .content | base64 -d`.
60
63
  - **Packaging tool**: uv project, Poetry, PDM, Hatch with tox or nox, or plain pip requirements/extras. It decides how
61
64
  you run the planner locally and how routing tests install (step 6). CI planning itself installs nothing in the
62
65
  project: the actions bring their own Python 3.14 planner.
@@ -66,7 +69,11 @@ Record:
66
69
  its matrix size, services, coverage floors and typical duration (`gh run view RUN --repo "$REPO" --json jobs`).
67
70
  - **Publish paths**: jobs that publish or deploy (PyPI, Test PyPI, Pages, containers, releases) and every job they
68
71
  `needs:`, directly or transitively. Step 4.4 keeps these paths unchanged.
69
- - **Triggers**: whether `push` runs for every branch, and any `branches:`/`paths:` filters.
72
+ - **Test workflows**: the file name of each workflow that runs tests (`test.yml`, `tests.yaml`, `ci.yml`, ...).
73
+ Below, `test.yml` stands for `<the repo's test workflow>`: write the real names in the config, the routing test and
74
+ the report.
75
+ - **Triggers**: whether `push` runs for every branch, and any `branches:`, `paths:` or `paths-ignore:` filters (4.3
76
+ removes path filters from gated workflows).
70
77
  - **Existing gate**: an aggregate job (`all-green`, `alls-green`, `check`) and the required check names.
71
78
 
72
79
  If CI already finishes in a couple of minutes, or nearly every change touches source code, say so in your report:
@@ -84,7 +91,9 @@ VERSION=${TAG#v}
84
91
  ```
85
92
 
86
93
  Local commands use `pytest-selective==$VERSION`; the routing-test job (step 6) installs `pytest-selective>=$VERSION,<2`,
87
- so it follows the same major version as `@v1`.
94
+ so it follows the same major version as `@v1`. uv caches the package index, so a release published minutes ago can
95
+ be missing (`No solution found`, `pytest-selective==X.Y.Z` unsatisfiable): the local commands below pass
96
+ `--refresh-package pytest-selective`. With pip, add `--no-cache-dir`.
88
97
 
89
98
  Only when the repository's tooling rejects tags (zizmor's `unpinned-uses`, a `pinact` hook, or an all-SHA policy its
90
99
  CI enforces) pin the release instead: `@$SHA # $TAG` on every action, with
@@ -122,7 +131,7 @@ line or a nox `session.install`). CI does not need this.
122
131
  "global_inputs": ["pyproject.toml", "tox.ini", "requirements/tests.txt"]
123
132
  },
124
133
  "lanes": {
125
- "tests": {"roots": ["tests"]},
134
+ "tests": {"roots": ["tests"], "inputs": ["src/toy/*.c", "src/toy/*.pyi"]},
126
135
  "bench": {"roots": ["tests/benchmarks"]},
127
136
  "package": {"inputs": ["src/**", "pyproject.toml", "README.md", "LICENSE"]},
128
137
  "routing": {"roots": ["ci_tests"]}
@@ -143,6 +152,11 @@ Adapt every path; this is a shape, not an inventory. The rules:
143
152
  `dir/**/*.py` matches `dir/x.py` and `dir/a/x.py`. Name execution files; never a directory that also holds prose
144
153
  (`examples/**` runs the lane for an example's README; use `examples/**/*.py`). A lane without roots runs only on its
145
154
  inputs, on global inputs, or when a change reaches every test.
155
+ - **Non-Python sources are not imports.** A C extension (`_speedups.c`), Cython (`.pyx`, `.pxd`), type stubs
156
+ (`.pyi`), templates or data files the tests read: the graph does not see them unless a test opens them by literal
157
+ path. Add each as an input of the lanes that build or test it, as exact globs
158
+ (`"tests": {"roots": ["tests"], "inputs": ["src/pkg/*.c", "src/pkg/*.pyi"]}`) or the package directory (`src/**`)
159
+ when every file there matters, and give each a routing row (step 6).
146
160
  - **`graph.global_inputs`** are the environment files whose change can affect every test, and the only place to narrow
147
161
  them. Omitted, the built-in list applies: `pyproject.toml`, `setup.cfg`, `setup.py`, `tox.ini`, `pytest.ini`,
148
162
  `pytest.toml`, `uv.lock`, `poetry.lock`, `pdm.lock`, `pixi.lock`, `Pipfile`, `Pipfile.lock`, `*requirements*.txt`,
@@ -166,30 +180,39 @@ Add `.test-impact/` to `.gitignore`: the planner keeps its graph cache there.
166
180
 
167
181
  **Collection policy.** If pytest configuration exists (`pyproject.toml`, `pytest.ini`, `pytest.toml`, a `tox.ini`
168
182
  with `[pytest]`, a `setup.cfg` with `[tool:pytest]`), the planner runs every lane until you record its fingerprints
169
- (`full-suite fallback: pytest configuration changed; review collection policy and refresh its fingerprints`). Review
183
+ (`full-suite fallback: pytest configuration changed (pyproject.toml); review collection policy and refresh its
184
+ fingerprints: ...`). Review
170
185
  the pytest settings and collection hooks, commit the config, then:
171
186
 
172
187
  ```bash
173
- mkdir -p .test-impact
174
- python -c "import json; print(json.dumps(json.load(open('.github/test-lanes.json'))['graph']))" > .test-impact/graph.json
175
- uv run --no-project --with "pytest-selective==$VERSION" \
176
- pytest-selective fingerprints --config .test-impact/graph.json --head HEAD
188
+ uv run --refresh-package pytest-selective --no-project --with "pytest-selective==$VERSION" \
189
+ pytest-selective fingerprints --config .github/test-lanes.json --head HEAD
177
190
  ```
178
191
 
179
- Copy the printed object into `graph.pytest_policy` and commit. Releases up to 1.1.1 also print a `tox.ini` or
192
+ Copy the printed object into `graph.pytest_policy` and commit. Every `pytest-selective` command (`fingerprints`,
193
+ `explain`, `affected`, `run`) takes the lane config itself as `--config`, so the policy you record is the one in use.
194
+ Releases up to 1.2.0 need a derived file instead: `pytest-selective file-config --lanes .github/test-lanes.json
195
+ --output .test-impact/graph.json` (it needs top-level `global_inputs`), passed as `--config .test-impact/graph.json`.
196
+ Re-run it after every lane-config change, recording `pytest_policy` included, or `explain` keeps failing with
197
+ `pytest configuration changed`. Releases up to 1.1.1 also print a `tox.ini` or
180
198
  `setup.cfg` that has no pytest section (pytest ignores it); keeping that entry is harmless. Without uv, run the same
181
199
  commands in a virtual environment after `python -m pip install "pytest-selective==$VERSION"`. When pytest settings
182
200
  change later, the planner falls back to every lane until someone reviews and refreshes the fingerprints.
183
201
 
184
202
  ### Prove the config locally
185
203
 
186
- On a throwaway branch, commit one probe edit at a time and plan it against the default branch:
204
+ On a throwaway branch made from the adoption branch, after committing the lane config, commit one probe edit at a
205
+ time and plan it against its parent:
187
206
 
188
207
  ```bash
189
- uv run --no-project --with "pytest-selective==$VERSION" \
190
- python -m pytest_selective.ci --config .github/test-lanes.json --base origin/DEFAULT --head HEAD
208
+ uv run --refresh-package pytest-selective --no-project --with "pytest-selective==$VERSION" \
209
+ python -m pytest_selective.ci --config .github/test-lanes.json --base HEAD^ --head HEAD
191
210
  ```
192
211
 
212
+ Do not plan against `origin/DEFAULT` before the merge: DEFAULT lacks `.github/test-lanes.json`, a global input, so
213
+ every probe would report `global input: .github/test-lanes.json` and select every lane. The parent commit already has
214
+ the config, so the plan sees only the probe edit.
215
+
193
216
  Expected: JSON with `lanes` (one boolean per lane), `reason` and `lane_reasons`. A README edit gives every lane
194
217
  `false` (or only `package`, when the build embeds the README) with `no affected tests or changed inputs`; a source
195
218
  edit selects the lanes of the tests that import it (`affected test: tests/test_x.py`); a lockfile or workflow edit
@@ -241,6 +264,10 @@ The action also uploads its plan as a `test-lane-selection` artifact, which
241
264
  fromJSON(needs.select-tests.outputs.lanes).tests) }}
242
265
  ```
243
266
 
267
+ A skipped matrix job is listed under its unevaluated name (for example `${{ matrix.name || matrix.python }}`),
268
+ because the matrix never expands. Mention it in the report, or give the job a name that also reads well unexpanded,
269
+ such as `name: tests (${{ matrix.python }})`.
270
+
244
271
  `always()` lets the condition run when the planner failed, and the failed-planner branch then runs the job: a broken
245
272
  plan must never become a green skip. The condition applies before the matrix expands, so the whole matrix skips
246
273
  together. A job with other needs must check them itself, because `always()` also ignores their failure:
@@ -259,6 +286,10 @@ together. A job with other needs must check them itself, because `always()` also
259
286
  control at the probe head (step 8), so keep them; cancel the source probe's push runs as soon as they start
260
287
  (`gh run cancel RUN --repo "$REPO"`) and judge that probe by its `pull_request` runs. The cost ends once the
261
288
  adoption merges.
289
+ - **`paths:` and `paths-ignore:` filters.** Remove them from every workflow that gets a gate; the lanes replace
290
+ them. A required gate in a workflow that a filter skipped never reports, so a docs-only pull request would wait on
291
+ it forever. This is a behavior change: a change the filter skipped (say `docs/**`) now starts the planner, the
292
+ unconditional jobs and the gate. Say so in the report, with its cost (step 10, net effect).
262
293
  - **`branches:` filters.** A probe pull request opened against your adoption branch does not start workflows filtered
263
294
  to `pull_request: branches: [main]`. Prove those workflows with routing tests and local plans, and say in the report
264
295
  that their hosted probe waits for the merge.
@@ -365,6 +396,7 @@ ROOT = Path(__file__).resolve().parents[1]
365
396
  CONFIG = LaneConfig.model_validate_json((ROOT / ".github/test-lanes.json").read_text(encoding="utf-8"))
366
397
  EVERY = dict.fromkeys(CONFIG.lanes, True)
367
398
  NONE = dict.fromkeys(CONFIG.lanes, False)
399
+ # <the repo's test workflow>: tests.yaml, ci.yml, ...
368
400
  WORKFLOW = ROOT / ".github/workflows/test.yml"
369
401
  PLANNER = "select-tests"
370
402
  # The gate job's id. Keep an existing aggregate job's id, for example "success".
@@ -406,6 +438,8 @@ def repo(tmp_path: Path) -> Path:
406
438
  ("requirements-mypy.txt", NONE),
407
439
  ("tests/test_other.py", {**NONE, "tests": True}),
408
440
  ("src/toy/other.py", {**NONE, "tests": True, "package": True}),
441
+ # Non-Python sources the tests exercise (a C extension, stubs), declared as lane inputs.
442
+ ("src/toy/_speedups.c", {**NONE, "tests": True, "package": True}),
409
443
  ("tests/benchmarks/test_bench.py", {**NONE, "tests": True, "bench": True}),
410
444
  ("tox.ini", EVERY),
411
445
  (".github/test-lanes.json", EVERY),
@@ -446,12 +480,12 @@ def test_the_gate_guards_every_job_and_lane_jobs_run_when_planning_fails() -> No
446
480
  assert all(f"fromJSON(needs.{PLANNER}.outputs.lanes).{lane}" in condition for lane in lanes), job
447
481
  ```
448
482
 
449
- Add a row for every directory you narrowed, every requirements file, each workflow and each lane, using real tracked
450
- paths. A row appends a comment; a `<workflow>#<job>` input ignores comments, so edit the job itself in that row. With
483
+ Add a row for every directory you narrowed, every requirements file, each non-Python source input, each workflow and
484
+ each lane, using real tracked paths. A row only names its path; it does not make this test depend on that file. A row appends a comment; a `<workflow>#<job>` input ignores comments, so edit the job itself in that row. With
451
485
  several workflows, repeat the gate test per workflow with its own `GATE`. Make the file pass the repository's own
452
486
  linters, formatter and type checker before committing: for example, ruff's `required-imports` may demand
453
487
  `from __future__ import annotations` (the template has it), and black or `ruff format` with a shorter line length
454
- reflows it. Run it locally with `uv run --no-project --with "pytest-selective==$VERSION" pytest ci_tests -q` and
488
+ reflows it. Run it locally with `uv run --refresh-package pytest-selective --no-project --with "pytest-selective==$VERSION" pytest ci_tests -q` and
455
489
  commit it.
456
490
 
457
491
  ## 7. Prove it on hosted CI
@@ -487,8 +521,12 @@ Compare a selected run with a full control **at the same head**:
487
521
  gh workflow run WORKFLOW --repo "$REPO" --ref PROBE_BRANCH # once per workflow
488
522
  ```
489
523
 
490
- GitHub dispatches only workflows whose file exists on DEFAULT, and may read the trigger from there: if a workflow
491
- gains `workflow_dispatch:` only in your adoption and the dispatch is refused, measure after the merge. A rerun is
524
+ Dispatch only the workflows this adoption gates. A workflow without `workflow_dispatch:` returns HTTP 422
525
+ (`Workflow does not have 'workflow_dispatch' trigger`); do not add the trigger to workflows you otherwise leave
526
+ alone. Leave such workflows out of both sides and say the comparison covers only the gated workflows. GitHub
527
+ dispatches only workflows whose file exists on DEFAULT, and may read the trigger from DEFAULT's copy rather than the
528
+ branch's: if a workflow gains `workflow_dispatch:` only in your adoption and the dispatch is refused, measure after
529
+ the merge. A rerun is
492
530
  not a control: it keeps the selection. Do not wait on a `test:full` label; creating one is the human's step 9.
493
531
 
494
532
  Both sides then share a head SHA: a `pull_request` run records the branch head as its `head_sha`, although it checks
@@ -541,6 +579,15 @@ pytest-selective vX.Y.Z: actions at `@v1` (or `@<sha> # vX.Y.Z` where tags are r
541
579
  | Elapsed seconds | | |
542
580
 
543
581
  Runs: [full](https://github.com/OWNER/NAME/actions/runs/RUN_ID), [selected](https://github.com/OWNER/NAME/actions/runs/RUN_ID). One pair, not an average or a bill.
582
+ Workflows compared: ... (and any left out, such as one without `workflow_dispatch:`).
583
+
584
+ ### Net effect
585
+ - Docs-only pull requests: before ... runner-seconds (0 where a removed `paths-ignore` skipped them), after ....
586
+ - Code changes: lane jobs now wait for the planner, about N s.
587
+ - Changes that now skip tests: ... (prose and tooling the old filters did not cover).
588
+
589
+ ### Trigger changes (behavior change)
590
+ - Removed `paths-ignore: [...]` from `<the repo's test workflow>`; added `workflow_dispatch:` ... (or "none").
544
591
 
545
592
  ### Evidence
546
593
  - N routing tests: docs, requirements, source, tests, benchmarks, workflows, missing history, full checkpoint.
@@ -551,9 +598,15 @@ Runs: [full](https://github.com/OWNER/NAME/actions/runs/RUN_ID), [selected](http
551
598
  - Label `test:full`; trigger change; scheduled full run.
552
599
 
553
600
  ### Not done or not verified
601
+ - Skipped matrix jobs show their unevaluated name (`matrix.name || matrix.python`), if kept.
554
602
  - ...
555
603
  ```
556
604
 
605
+ The net effect and trigger changes are required. Write them honestly: when CI was already cheap, or a `paths-ignore`
606
+ already skipped docs, say the adoption saves little, what it costs (planner latency on code changes; docs-only runs
607
+ going from 0 to about N runner-seconds), and what it buys (a gate that can be required, and skips for changes the old
608
+ filters missed).
609
+
557
610
  For depth: [configuration reference](https://pytest-selective.readthedocs.io/en/latest/docs/configuration/),
558
611
  [CI guide](https://pytest-selective.readthedocs.io/en/latest/docs/ci/),
559
612
  [documentation-only audit](https://pytest-selective.readthedocs.io/en/latest/docs/tutorials/skip-unrelated-ci/),
@@ -12,6 +12,7 @@ import yaml
12
12
  from pydantic import Field, field_validator, model_validator
13
13
 
14
14
  from pytest_selective.checkpoint import full_checkpoint_reason
15
+ from pytest_selective.git_operations import pull_request_merge_base
15
16
  from pytest_selective.impact_graph import DependencyDeclarationError, GraphCache, git, matches, within
16
17
  from pytest_selective.impact_models import ImpactConfig, ImpactModel
17
18
  from pytest_selective.impact_selection import affected
@@ -63,6 +64,20 @@ class LaneConfig(ImpactModel):
63
64
  update={"global_inputs": sorted(set(self.graph.global_inputs) | set(self.global_inputs))}
64
65
  )
65
66
 
67
+ @classmethod
68
+ def file_config_at(cls, path: Path) -> ImpactConfig:
69
+ """The file-selection config at `path`, which may also be a lane config such as `.github/test-lanes.json`.
70
+
71
+ A lane config gives its graph, with its explicit global inputs when it declares them. Reading
72
+ it directly means no derived copy can miss a later `graph.pytest_policy` refresh.
73
+ """
74
+ text = path.read_text(encoding="utf-8")
75
+ data = json.loads(text)
76
+ if not isinstance(data, dict) or "lanes" not in data:
77
+ return ImpactConfig.model_validate_json(text)
78
+ lanes = cls.model_validate(data)
79
+ return lanes.graph if lanes.global_inputs is None else lanes.file_config()
80
+
66
81
  def is_global_input(self, path: str) -> bool:
67
82
  if self.global_inputs is not None:
68
83
  return matches(path, self.global_inputs)
@@ -254,6 +269,8 @@ def main(argv: list[str] | None = None) -> int:
254
269
  parser.add_argument("--repo", type=Path, default=Path("."))
255
270
  parser.add_argument("--base", required=True)
256
271
  parser.add_argument("--head", default="HEAD")
272
+ # A pull request's head: when `--head` is GitHub's merge of it, plan against the merge's actual base.
273
+ parser.add_argument("--pr-head", default="")
257
274
  parser.add_argument(
258
275
  "--full", nargs="?", const="true", default="false", type=full_checkpoint_reason, metavar="auto|true|false"
259
276
  )
@@ -261,7 +278,13 @@ def main(argv: list[str] | None = None) -> int:
261
278
  args = parser.parse_args(argv)
262
279
  try:
263
280
  config = LaneConfig.model_validate_json(args.config.read_text(encoding="utf-8"))
264
- result = plan(args.repo, config, args.base, args.head, full=args.full is not None)
281
+ base = pull_request_merge_base(args.repo, args.base, args.head, args.pr_head)
282
+ if base != args.base:
283
+ print(
284
+ f"Planning against the tested merge's base {base}; the event's base {args.base} is behind it.",
285
+ file=sys.stderr,
286
+ )
287
+ result = plan(args.repo, config, base, args.head, full=args.full is not None)
265
288
  if args.full is not None:
266
289
  result.reason = args.full
267
290
  except (ValueError, OSError, DependencyDeclarationError) as error:
@@ -47,3 +47,26 @@ def git(repo: Path, *args: str, data: bytes | None = None, index: Path | None =
47
47
  def git_text(repo: Path, *args: str, index: Path | None = None) -> str:
48
48
  """UTF-8 text with universal newlines, independent of the Windows locale."""
49
49
  return git(repo, *args, index=index).decode("utf-8").replace("\r\n", "\n").replace("\r", "\n")
50
+
51
+
52
+ def pull_request_merge_base(repo: Path, base: str, head: str, pr_head: str) -> str:
53
+ """
54
+ The base a pull request's merge commit was built on, or `base` when that is not provable.
55
+
56
+ GitHub checks out `refs/pull/N/merge`: the PR head merged into the base branch's current tip.
57
+ The event's `pull_request.base.sha` can be older, so commits that landed on the base branch
58
+ since would read as the PR's own changes: a release's version bump then selects every test.
59
+ When `head` is that merge (two parents, the second the PR head) and `base` is an ancestor of
60
+ the first parent, the first parent is the tested base. A push, a shallow history or any other
61
+ shape keeps `base`, which is never narrower than the truth.
62
+ """
63
+ if not pr_head:
64
+ return base
65
+ try:
66
+ parents = git_text(repo, "rev-list", "--parents", "-n", "1", head).split()[1:]
67
+ if len(parents) != 2 or parents[1] != git_text(repo, "rev-parse", f"{pr_head}^{{commit}}").strip():
68
+ return base
69
+ git(repo, "merge-base", "--is-ancestor", base, parents[0])
70
+ except subprocess.CalledProcessError:
71
+ return base
72
+ return parents[0]
@@ -8,6 +8,10 @@ input becomes an ``import`` rule, because a test that loads or runs a script out
8
8
  ``content`` rule. Reads under one directory collapse into ``<directory>/**`` only when the test read
9
9
  every tracked file under it, so the glob claims no input the run did not see. Every pattern is
10
10
  checked against the analysed tree before anything is written.
11
+
12
+ A declaration selects every test that depends on its reader, not only the reader. Each sidecar
13
+ records how many tests that is, so a reader that every test reaches (a test module another test
14
+ imports, or one that registers session plugins) is visible before the declaration is committed.
11
15
  """
12
16
 
13
17
  import json
@@ -21,6 +25,7 @@ from pytest_selective.impact_declarations import (
21
25
  match_inputs,
22
26
  parse_manifest,
23
27
  )
28
+ from pytest_selective.impact_explanation import dependent_tests
24
29
  from pytest_selective.impact_models import (
25
30
  AuditRecord,
26
31
  AuditReport,
@@ -40,6 +45,8 @@ class SidecarDeclarations(ImpactModel):
40
45
  rules: list[ManifestDependencyRule]
41
46
  #: Pattern (as written, ``//``-rooted) to the undeclared reads it declares.
42
47
  covers: dict[str, list[str]]
48
+ #: Test files a change to any declared input selects: the reader and every test depending on it.
49
+ selects: int
43
50
 
44
51
  @property
45
52
  def sidecar(self) -> str:
@@ -48,6 +55,8 @@ class SidecarDeclarations(ImpactModel):
48
55
 
49
56
  class DeclarationPlan(ImpactModel):
50
57
  sidecars: list[SidecarDeclarations]
58
+ #: Test files in the analysed tree, the scale ``SidecarDeclarations.selects`` is read against.
59
+ total_tests: int
51
60
  #: Reads no valid pattern can name (the path itself is not a valid declaration), with the reason.
52
61
  skipped: list[str]
53
62
 
@@ -82,6 +91,7 @@ def plan_declarations(report: AuditReport, snapshot: Snapshot, records: list[Aud
82
91
  """The sidecar rules that would declare every undeclared read in ``report``."""
83
92
  files = set(snapshot.files)
84
93
  reads = tracked_reads(snapshot, records)
94
+ reach = dependent_tests(snapshot)
85
95
  undeclared: dict[str, set[str]] = {}
86
96
  for read in report.undeclared:
87
97
  undeclared.setdefault(read.test, set()).add(read.input)
@@ -106,8 +116,9 @@ def plan_declarations(report: AuditReport, snapshot: Snapshot, records: list[Aud
106
116
  if (patterns := inputs[kind])
107
117
  ]
108
118
  if rules:
109
- sidecars.append(SidecarDeclarations(test=test, rules=rules, covers=covers))
110
- return DeclarationPlan(sidecars=sidecars, skipped=skipped)
119
+ selects = max(reach.get(test, 0).bit_count(), 1)
120
+ sidecars.append(SidecarDeclarations(test=test, rules=rules, covers=covers, selects=selects))
121
+ return DeclarationPlan(sidecars=sidecars, skipped=skipped, total_tests=len(snapshot.tests))
111
122
 
112
123
 
113
124
  def render_rules(rules: list[ManifestDependencyRule]) -> str: