sarj-python-lint 0.77.0__tar.gz → 0.78.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 (123) hide show
  1. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/PKG-INFO +6 -6
  2. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/README.md +2 -2
  3. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/pyproject.toml +4 -4
  4. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/__main__.py +5 -1
  5. sarj_python_lint-0.78.0/src/sarj_python_lint/_analysis_session.py +11 -0
  6. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rule_base.py +7 -2
  7. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_first_party.py +80 -47
  8. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_paths.py +9 -13
  9. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_project_index.py +11 -5
  10. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +1 -1
  11. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/get_delegates_to_get_many.py +1 -1
  12. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +1 -1
  13. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +9 -7
  14. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +87 -24
  15. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +1 -1
  16. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +1 -1
  17. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +1 -1
  18. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +1 -1
  19. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +7 -9
  20. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +1 -1
  21. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +1 -1
  22. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +1 -1
  23. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/preserve_enum_types.py +1 -1
  24. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +1 -1
  25. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +1 -1
  26. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +1 -1
  27. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +1 -1
  28. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +1 -1
  29. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +1 -1
  30. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/.gitignore +0 -0
  31. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/LICENSE +0 -0
  32. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/__init__.py +0 -0
  33. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/_filesystem.py +0 -0
  34. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  35. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/_secret_names.py +0 -0
  36. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/_version.py +0 -0
  37. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/py.typed +0 -0
  38. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/ratchet.py +0 -0
  39. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  40. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  41. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  42. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  43. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  44. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  45. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  46. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  47. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  48. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  49. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  50. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  51. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  52. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
  53. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  54. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  55. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  56. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
  57. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
  58. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  59. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  60. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  61. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  62. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  63. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  64. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  65. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  66. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  67. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  68. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  69. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  70. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  71. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  72. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  73. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  74. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
  75. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  76. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  77. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  78. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  79. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  80. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  81. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  82. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  83. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  84. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  85. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  86. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  87. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  88. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  89. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  90. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  91. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  92. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  93. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  94. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  95. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  96. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  97. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  98. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  99. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  100. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  101. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  102. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  103. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  104. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  105. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  106. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  107. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  108. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  109. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  110. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  111. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  112. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  113. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  114. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  115. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  116. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  117. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
  118. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  119. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  120. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  121. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  122. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  123. {sarj_python_lint-0.77.0 → sarj_python_lint-0.78.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.77.0
3
+ Version: 0.78.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
7
- Project-URL: Changelog, https://github.com/sarj-ai/standards/releases
8
- Project-URL: Repository, https://github.com/sarj-ai/standards
9
- Project-URL: Issues, https://github.com/sarj-ai/standards/issues
7
+ Project-URL: Changelog, https://github.com/sarj-ai/code-standards/releases
8
+ Project-URL: Repository, https://github.com/sarj-ai/code-standards
9
+ Project-URL: Issues, https://github.com/sarj-ai/code-standards/issues
10
10
  Author: sarj-ai
11
11
  License-Expression: MIT
12
12
  License-File: LICENSE
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Description-Content-Type: text/markdown
21
21
 
22
- <!-- Generated by `sarj-standards maintain docs sync`; do not edit. -->
22
+ <!-- Generated by `code-standards maintain docs sync`; do not edit. -->
23
23
 
24
24
  # sarj-python-lint
25
25
 
@@ -33,4 +33,4 @@ uv tool install sarj-python-lint
33
33
  sarj-python-lint --help
34
34
  ```
35
35
 
36
- [Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/standards)
36
+ [Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/code-standards)
@@ -1,4 +1,4 @@
1
- <!-- Generated by `sarj-standards maintain docs sync`; do not edit. -->
1
+ <!-- Generated by `code-standards maintain docs sync`; do not edit. -->
2
2
 
3
3
  # sarj-python-lint
4
4
 
@@ -12,4 +12,4 @@ uv tool install sarj-python-lint
12
12
  sarj-python-lint --help
13
13
  ```
14
14
 
15
- [Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/standards)
15
+ [Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/code-standards)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.77.0"
3
+ version = "0.78.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -24,9 +24,9 @@ sarj-ratchet = "sarj_python_lint._ratchet_cli:main"
24
24
  [project.urls]
25
25
  Homepage = "https://code-standards.sarj.ai/rules/python/"
26
26
  Documentation = "https://code-standards.sarj.ai/rules/python/"
27
- Changelog = "https://github.com/sarj-ai/standards/releases"
28
- Repository = "https://github.com/sarj-ai/standards"
29
- Issues = "https://github.com/sarj-ai/standards/issues"
27
+ Changelog = "https://github.com/sarj-ai/code-standards/releases"
28
+ Repository = "https://github.com/sarj-ai/code-standards"
29
+ Issues = "https://github.com/sarj-ai/code-standards/issues"
30
30
 
31
31
  [dependency-groups]
32
32
  dev = [
@@ -10,6 +10,7 @@ from types import MappingProxyType
10
10
  from typing import NamedTuple
11
11
 
12
12
  from sarj_python_lint import __version__
13
+ from sarj_python_lint._analysis_session import AnalysisSession
13
14
  from sarj_python_lint._filesystem import atomic_write_text
14
15
  from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Severity, is_suppressed
15
16
  from sarj_python_lint.rules import REGISTRY
@@ -77,6 +78,9 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
77
78
  sys.stderr.write(f"available: {', '.join(sorted(REGISTRY))}\n")
78
79
  raise SystemExit(2)
79
80
  rules = [REGISTRY[rid]() for rid in rule_ids]
81
+ session = AnalysisSession()
82
+ for rule in rules:
83
+ rule.prepare_session(session)
80
84
  clear_path_caches()
81
85
  expanded = _expand_paths(paths)
82
86
  loaded: dict[Path, str] = {}
@@ -87,7 +91,7 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
87
91
  continue
88
92
  project_rules = [rule for rule in rules if isinstance(rule, ProjectRule)]
89
93
  if project_rules:
90
- indexes = ProjectIndexSet.build(expanded, loaded)
94
+ indexes = ProjectIndexSet.build(expanded, loaded, facts=session.first_party)
91
95
  for rule in project_rules:
92
96
  rule.prepare(indexes)
93
97
  diags: list[Diagnostic] = []
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import final
4
+
5
+ from sarj_python_lint.rules._first_party import FirstPartyFacts
6
+
7
+
8
+ @final
9
+ class AnalysisSession:
10
+ def __init__(self) -> None:
11
+ self.first_party = FirstPartyFacts()
@@ -12,11 +12,12 @@ from typing import TYPE_CHECKING, ClassVar, Final, Self
12
12
  if TYPE_CHECKING:
13
13
  from collections.abc import Sequence
14
14
 
15
+ from sarj_python_lint._analysis_session import AnalysisSession
15
16
  from sarj_python_lint.rules._project_index import ProjectIndexSet
16
17
 
17
18
 
18
19
  # Each rule points directly to its executable examples.
19
- REPO_BLOB: Final = "https://github.com/sarj-ai/standards/blob/main"
20
+ REPO_BLOB: Final = "https://github.com/sarj-ai/code-standards/blob/main"
20
21
  TESTS_DIR: Final = "packages/python/tests/rules"
21
22
 
22
23
 
@@ -237,6 +238,10 @@ class Rule(ABC):
237
238
  code: str
238
239
  description: str
239
240
  documentation: ClassVar[RuleDocumentation | None] = None
241
+ _analysis_session: AnalysisSession | None = None
242
+
243
+ def prepare_session(self, session: AnalysisSession) -> None:
244
+ self._analysis_session = session
240
245
 
241
246
  @abstractmethod
242
247
  def check(self, path: Path, source: str) -> list[Diagnostic]:
@@ -281,7 +286,7 @@ class Rule(ABC):
281
286
  return () if spec is None else spec.public_examples
282
287
 
283
288
 
284
- class ProjectRule(Rule):
289
+ class ProjectRule(Rule, ABC):
285
290
  _project_indexes: ProjectIndexSet | None = None
286
291
 
287
292
  def prepare(self, indexes: ProjectIndexSet) -> None:
@@ -2,9 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from functools import lru_cache
6
5
  import sys
7
- from typing import TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, final
8
7
 
9
8
 
10
9
  if TYPE_CHECKING:
@@ -53,22 +52,78 @@ _MAX_DIRS_SCANNED = 3000
53
52
  _MAX_ANCESTORS = 24
54
53
 
55
54
 
56
- def is_first_party_module(module: str, path: Path) -> bool:
55
+ @final
56
+ class FirstPartyFacts:
57
+ def __init__(self) -> None:
58
+ self._project_roots: dict[Path, Path | None] = {}
59
+ self._roots: dict[Path, frozenset[str]] = {}
60
+ self._packages: dict[Path, tuple[tuple[str, Path], ...]] = {}
61
+
62
+ def project_root(self, path: Path) -> Path | None:
63
+ if path in self._project_roots:
64
+ return self._project_roots[path]
65
+ resolved = _resolved(path)
66
+ if resolved is None:
67
+ self._project_roots[path] = None
68
+ return None
69
+ ancestors = list(resolved.parents)[:_MAX_ANCESTORS]
70
+ for ancestor in ancestors:
71
+ if (ancestor / _GIT_MARKER).exists():
72
+ self._project_roots[path] = ancestor
73
+ return ancestor
74
+ # No VCS boundary: take the OUTERMOST directory of the unbroken run of
75
+ # pyproject.toml ancestors, which is the workspace root in a uv workspace.
76
+ outermost: Path | None = None
77
+ for ancestor in ancestors:
78
+ if not (ancestor / _PROJECT_MARKER).exists():
79
+ if outermost is not None:
80
+ break
81
+ continue
82
+ outermost = ancestor
83
+ self._project_roots[path] = outermost
84
+ return outermost
85
+
86
+ def first_party_roots(self, root: Path) -> frozenset[str]:
87
+ cached = self._roots.get(root)
88
+ if cached is not None:
89
+ return cached
90
+ roots = frozenset(name for name, _ in self.first_party_packages(root))
91
+ self._roots[root] = roots
92
+ return roots
93
+
94
+ def first_party_packages(self, root: Path) -> tuple[tuple[str, Path], ...]:
95
+ cached = self._packages.get(root)
96
+ if cached is not None:
97
+ return cached
98
+ packages = _scan_first_party_packages(root)
99
+ self._packages[root] = packages
100
+ return packages
101
+
102
+
103
+ def _fresh_facts(facts: FirstPartyFacts | None) -> FirstPartyFacts:
104
+ # Compatibility helper calls are independent requests. The CLI supplies one
105
+ # explicit instance so scans are still shared within a full analysis.
106
+ return facts if facts is not None else FirstPartyFacts()
107
+
108
+
109
+ def is_first_party_module(module: str, path: Path, *, facts: FirstPartyFacts | None = None) -> bool:
110
+ facts = _fresh_facts(facts)
57
111
  top = module.partition(".")[0]
58
112
  if not top or top in sys.stdlib_module_names:
59
113
  return False
60
- root = _project_root(path)
114
+ root = facts.project_root(path)
61
115
  if root is None:
62
116
  # Unresolved ownership biases to third-party to avoid unfixable findings.
63
117
  return False
64
- return top in _first_party_roots(root)
118
+ return top in facts.first_party_roots(root)
65
119
 
66
120
 
67
- def own_top_package(path: Path) -> str | None:
121
+ def own_top_package(path: Path, *, facts: FirstPartyFacts | None = None) -> str | None:
122
+ facts = _fresh_facts(facts)
68
123
  resolved = _resolved(path)
69
124
  if resolved is None:
70
125
  return None
71
- root = _project_root(resolved)
126
+ root = facts.project_root(resolved)
72
127
  top: str | None = None
73
128
  for ancestor in list(resolved.parents)[:_MAX_ANCESTORS]:
74
129
  if (ancestor / "__init__.py").exists():
@@ -76,30 +131,33 @@ def own_top_package(path: Path) -> str | None:
76
131
  if ancestor == root:
77
132
  break
78
133
  if top is None and root is not None:
79
- roots = _first_party_roots(root)
134
+ roots = facts.first_party_roots(root)
80
135
  if len(roots) == 1:
81
136
  return next(iter(roots))
82
137
  return top
83
138
 
84
139
 
85
- def same_distribution(module: str, path: Path) -> bool:
86
- ours = distribution_root(path)
140
+ def same_distribution(module: str, path: Path, *, facts: FirstPartyFacts | None = None) -> bool:
141
+ facts = _fresh_facts(facts)
142
+ ours = distribution_root(path, facts=facts)
87
143
  if ours is None:
88
144
  return False
89
- root = _project_root(path)
145
+ root = facts.project_root(path)
90
146
  if root is None:
91
147
  return False
92
148
  top = module.partition(".")[0]
93
149
  return any(
94
- name == top and distribution_root(package_dir) == ours for name, package_dir in _first_party_packages(root)
150
+ name == top and distribution_root(package_dir, facts=facts) == ours
151
+ for name, package_dir in facts.first_party_packages(root)
95
152
  )
96
153
 
97
154
 
98
- def distribution_root(path: Path) -> Path | None:
155
+ def distribution_root(path: Path, *, facts: FirstPartyFacts | None = None) -> Path | None:
156
+ facts = _fresh_facts(facts)
99
157
  resolved = _resolved(path)
100
158
  if resolved is None:
101
159
  return None
102
- root = _project_root(resolved)
160
+ root = facts.project_root(resolved)
103
161
  for ancestor in list(resolved.parents)[:_MAX_ANCESTORS]:
104
162
  if any((ancestor / marker).exists() for marker in _DISTRIBUTION_MARKERS):
105
163
  return ancestor
@@ -108,19 +166,21 @@ def distribution_root(path: Path) -> Path | None:
108
166
  return None
109
167
 
110
168
 
111
- def has_first_party_source(module: str, path: Path) -> bool:
169
+ def has_first_party_source(module: str, path: Path, *, facts: FirstPartyFacts | None = None) -> bool:
170
+ facts = _fresh_facts(facts)
112
171
  top, _, rest = module.partition(".")
113
- root = _project_root(path)
172
+ root = facts.project_root(path)
114
173
  if root is None:
115
174
  return False
116
175
  segments = rest.split(".") if rest else []
117
176
  return any(
118
- name == top and _declares_module(package_dir, segments) for name, package_dir in _first_party_packages(root)
177
+ name == top and _declares_module(package_dir, segments)
178
+ for name, package_dir in facts.first_party_packages(root)
119
179
  )
120
180
 
121
181
 
122
- def project_root(path: Path) -> Path | None:
123
- return _project_root(path)
182
+ def project_root(path: Path, *, facts: FirstPartyFacts | None = None) -> Path | None:
183
+ return _fresh_facts(facts).project_root(path)
124
184
 
125
185
 
126
186
  def _declares_module(package_dir: Path, segments: list[str]) -> bool:
@@ -138,34 +198,7 @@ def _resolved(path: Path) -> Path | None:
138
198
  return None
139
199
 
140
200
 
141
- @lru_cache(maxsize=256)
142
- def _project_root(path: Path) -> Path | None:
143
- resolved = _resolved(path)
144
- if resolved is None:
145
- return None
146
- ancestors = list(resolved.parents)[:_MAX_ANCESTORS]
147
- for ancestor in ancestors:
148
- if (ancestor / _GIT_MARKER).exists():
149
- return ancestor
150
- # No VCS boundary: take the OUTERMOST directory of the unbroken run of
151
- # pyproject.toml ancestors, which is the workspace root in a uv workspace.
152
- outermost: Path | None = None
153
- for ancestor in ancestors:
154
- if not (ancestor / _PROJECT_MARKER).exists():
155
- if outermost is not None:
156
- break
157
- continue
158
- outermost = ancestor
159
- return outermost
160
-
161
-
162
- @lru_cache(maxsize=32)
163
- def _first_party_roots(root: Path) -> frozenset[str]:
164
- return frozenset(name for name, _ in _first_party_packages(root))
165
-
166
-
167
- @lru_cache(maxsize=32)
168
- def _first_party_packages(root: Path) -> tuple[tuple[str, Path], ...]:
201
+ def _scan_first_party_packages(root: Path) -> tuple[tuple[str, Path], ...]:
169
202
  found: list[tuple[str, Path]] = []
170
203
  queue: list[tuple[Path, int]] = [(root, 0)]
171
204
  scanned = 0
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
- from functools import lru_cache
5
4
  from io import StringIO
6
5
  import re
7
6
  import tokenize
@@ -103,16 +102,6 @@ def _leading_header_bodies(source: str) -> list[str]:
103
102
  return bodies
104
103
 
105
104
 
106
- @lru_cache(maxsize=256)
107
- def _is_codegen_root(directory: Path) -> bool:
108
- return any((directory / name).exists() for name in _CODEGEN_MARKER_NAMES)
109
-
110
-
111
- @lru_cache(maxsize=256)
112
- def _is_repo_root(directory: Path) -> bool:
113
- return any((directory / name).exists() for name in _REPO_ROOT_MARKERS)
114
-
115
-
116
105
  def is_generated_path(path: Path) -> bool:
117
106
  if any(part.lower() in _GENERATED_DIR_NAMES for part in path.parts):
118
107
  return True
@@ -128,9 +117,16 @@ def is_generated_path(path: Path) -> bool:
128
117
  return False
129
118
 
130
119
 
120
+ def _is_codegen_root(directory: Path) -> bool:
121
+ return any((directory / name).exists() for name in _CODEGEN_MARKER_NAMES)
122
+
123
+
124
+ def _is_repo_root(directory: Path) -> bool:
125
+ return any((directory / name).exists() for name in _REPO_ROOT_MARKERS)
126
+
127
+
131
128
  def clear_path_caches() -> None:
132
- _is_codegen_root.cache_clear()
133
- _is_repo_root.cache_clear()
129
+ """Compatibility no-op; path topology is intentionally observed fresh."""
134
130
 
135
131
 
136
132
  def is_generated(path: Path, source: str) -> bool:
@@ -10,7 +10,7 @@ import re
10
10
  from types import MappingProxyType
11
11
  from typing import TYPE_CHECKING, final
12
12
 
13
- from sarj_python_lint.rules._first_party import project_root
13
+ from sarj_python_lint.rules._first_party import FirstPartyFacts, project_root
14
14
 
15
15
 
16
16
  if TYPE_CHECKING:
@@ -111,8 +111,14 @@ class ProjectIndexSet:
111
111
  self._nominals = MappingProxyType({key: frozenset(value) for key, value in nominals.items()})
112
112
 
113
113
  @classmethod
114
- def build(cls, paths: Sequence[Path], loaded: Mapping[Path, str]) -> Self:
115
- roots = _project_roots(paths)
114
+ def build(
115
+ cls,
116
+ paths: Sequence[Path],
117
+ loaded: Mapping[Path, str],
118
+ *,
119
+ facts: FirstPartyFacts | None = None,
120
+ ) -> Self:
121
+ roots = _project_roots(paths, facts=facts)
116
122
  sources: dict[Path, str] = {}
117
123
  for path, source in loaded.items():
118
124
  try:
@@ -283,11 +289,11 @@ def _module_name(path: Path, roots: Sequence[Path]) -> str | None:
283
289
  return ".".join(parts)
284
290
 
285
291
 
286
- def _project_roots(paths: Sequence[Path]) -> tuple[Path, ...]:
292
+ def _project_roots(paths: Sequence[Path], *, facts: FirstPartyFacts | None = None) -> tuple[Path, ...]:
287
293
  candidates: set[Path] = set()
288
294
  for path in paths:
289
295
  try:
290
- root = project_root(path.resolve())
296
+ root = project_root(path.resolve(), facts=facts)
291
297
  except OSError:
292
298
  continue
293
299
  if root is not None:
@@ -127,7 +127,7 @@ class CreatedAtOrderRequiresTiebreaker(Rule):
127
127
  line=node.lineno,
128
128
  col=node.col_offset + 1,
129
129
  code=self.code,
130
- severity=Severity.WARNING,
130
+ severity=Severity.ERROR,
131
131
  message=(
132
132
  "Store query leaves `created_at` as its final ordering item; add a stable tie-break key "
133
133
  "after it, such as `id` (for example, `ORDER BY created_at DESC, id DESC`)."
@@ -130,7 +130,7 @@ class GetDelegatesToGetMany(Rule):
130
130
  f"`{bulk.name}([key])` so singleton and bulk reads share one contract, or document why "
131
131
  "their semantics differ."
132
132
  ),
133
- severity=Severity.WARNING,
133
+ severity=Severity.ERROR,
134
134
  )
135
135
  )
136
136
  return diagnostics
@@ -150,7 +150,7 @@ class NegativeOnlyHttpStatusAssertion(Rule):
150
150
  line=node.lineno,
151
151
  col=node.col_offset + 1,
152
152
  code=self.code,
153
- severity=Severity.WARNING,
153
+ severity=Severity.ERROR,
154
154
  message=(
155
155
  "this assertion proves only that the response avoided one server-error shape; assert the "
156
156
  "intended exact status and, when relevant, its domain payload or side effect."
@@ -17,6 +17,7 @@ from sarj_python_lint.rule_base import (
17
17
  )
18
18
  from sarj_python_lint.rules._ast_index import nodes
19
19
  from sarj_python_lint.rules._first_party import (
20
+ FirstPartyFacts,
20
21
  has_first_party_source,
21
22
  is_first_party_module,
22
23
  own_top_package,
@@ -84,11 +85,12 @@ class NoFirstPartyPrivateImport(Rule):
84
85
  tree = parse_or_none(path, source)
85
86
  if tree is None:
86
87
  return []
87
- own_top = own_top_package(path)
88
+ facts = self._analysis_session.first_party if self._analysis_session is not None else FirstPartyFacts()
89
+ own_top = own_top_package(path, facts=facts)
88
90
  diags = [
89
91
  Diagnostic(path=path, line=hit.line, col=hit.col, code=self.code, message=_message(hit.module, hit.name))
90
92
  for hit in _private_imports(tree)
91
- if _is_ours(hit.module, path, own_top) and not _is_our_own_internals(hit, path)
93
+ if _is_ours(hit.module, path, own_top, facts) and not _is_our_own_internals(hit, path, facts)
92
94
  ]
93
95
  diags.sort(key=lambda d: (d.line, d.col))
94
96
  return diags
@@ -106,12 +108,12 @@ class _PrivateImport:
106
108
  names_public: bool
107
109
 
108
110
 
109
- def _is_our_own_internals(hit: _PrivateImport, path: Path) -> bool:
111
+ def _is_our_own_internals(hit: _PrivateImport, path: Path, facts: FirstPartyFacts) -> bool:
110
112
  if not hit.is_segment:
111
113
  return False
112
- if not has_first_party_source(hit.module, path):
114
+ if not has_first_party_source(hit.module, path, facts=facts):
113
115
  return True
114
- return hit.names_public and same_distribution(hit.module, path)
116
+ return hit.names_public and same_distribution(hit.module, path, facts=facts)
115
117
 
116
118
 
117
119
  def _message(module: str, name: str) -> str:
@@ -122,11 +124,11 @@ def _message(module: str, name: str) -> str:
122
124
  )
123
125
 
124
126
 
125
- def _is_ours(module: str, path: Path, own_top: str | None) -> bool:
127
+ def _is_ours(module: str, path: Path, own_top: str | None, facts: FirstPartyFacts) -> bool:
126
128
  top = module.partition(".")[0]
127
129
  if own_top is not None and top == own_top:
128
130
  return False
129
- return is_first_party_module(module, path)
131
+ return is_first_party_module(module, path, facts=facts)
130
132
 
131
133
 
132
134
  def _private_imports(tree: ast.Module) -> list[_PrivateImport]: