sarj-python-lint 1.77.3__tar.gz → 1.81.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 (282) hide show
  1. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/PKG-INFO +2 -2
  2. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/pyproject.toml +2 -2
  3. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/__main__.py +2 -6
  4. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix.py +7 -2
  5. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_comments.py +15 -56
  6. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_docs.py +77 -14
  7. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_inert.py +2 -2
  8. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/interpreter_argv.py +3 -5
  9. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/json_boundary.py +2 -2
  10. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rule_base.py +1 -5
  11. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/_comment_narration.py +156 -0
  12. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_comments.py +37 -1
  13. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +1 -1
  14. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_fastapi.py +2 -4
  15. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +6 -1
  16. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/_native_object_contracts.py +734 -0
  17. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_nominal_project.py +37 -16
  18. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/_opaque_contracts.py +1264 -0
  19. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_project_index.py +1 -2
  20. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_prose_budget.py +11 -5
  21. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/_service_http_callbacks.py +403 -0
  22. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/_sql_aggregate_identity.py +356 -0
  23. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/_sql_explain.py +140 -0
  24. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/_sql_policy_source.py +32 -0
  25. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/_transactional_sql.py +389 -0
  26. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +1 -3
  27. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +1 -2
  28. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/excessive_commentary.py +149 -0
  29. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +19 -10
  30. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/mock_without_spec.py +3 -7
  31. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +2 -4
  32. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +32 -67
  33. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +1 -2
  34. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_async_function_cache.py +2 -2
  35. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_comment_cruft.py +112 -41
  36. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +2 -2
  37. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +1 -3
  38. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +1 -2
  39. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +25 -6
  40. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +3 -3
  41. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +2 -2
  42. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +3 -3
  43. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_partial_type_is.py +2 -2
  44. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +1 -2
  45. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +12 -16
  46. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_restated_comment.py +5 -20
  47. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +2 -2
  48. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +1 -2
  49. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_tautological_expect.py +15 -6
  50. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +101 -60
  51. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/over_mocked_test.py +2 -5
  52. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/phase_label_comment.py +1 -2
  53. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +4 -4
  54. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +1 -2
  55. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_library_fake.py +1 -2
  56. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +123 -8
  57. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +1 -2
  58. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +1 -2
  59. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +76 -12
  60. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +8 -2
  61. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +2 -4
  62. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +3 -6
  63. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_str_enum.py +2 -6
  64. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +1 -2
  65. sarj_python_lint-1.81.0/src/sarj_python_lint/rules/redundant_module_docstring.py +104 -0
  66. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +18 -4
  67. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +2 -2
  68. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +11 -6
  69. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +1 -2
  70. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_port_for_service.py +2 -4
  71. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +12 -8
  72. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +5 -4
  73. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/stepdown.py +5 -10
  74. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/trailing_value_narration.py +1 -2
  75. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -2
  76. sarj_python_lint-1.77.3/src/sarj_python_lint/rules/excessive_commentary.py +0 -242
  77. sarj_python_lint-1.77.3/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -232
  78. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/.gitignore +0 -0
  79. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/LICENSE +0 -0
  80. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/README.md +0 -0
  81. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/__init__.py +0 -0
  82. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  83. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  84. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  85. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  86. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  87. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
  88. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  89. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  90. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  91. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_security.py +0 -0
  92. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  93. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
  94. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_file_context.py +0 -0
  95. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_filesystem.py +0 -0
  96. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_fixes.py +0 -0
  97. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_parallel.py +0 -0
  98. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_python_target.py +0 -0
  99. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  100. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_secret_names.py +0 -0
  101. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_source.py +0 -0
  102. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/_version.py +0 -0
  103. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/application_logging.py +0 -0
  104. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/autofix.py +0 -0
  105. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/benchmark.py +0 -0
  106. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/cast_access.py +0 -0
  107. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/py.typed +0 -0
  108. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/ratchet.py +0 -0
  109. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  110. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  111. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  112. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  113. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  114. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  115. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  116. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  117. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  118. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  119. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  120. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  121. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  122. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  123. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  124. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  125. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  126. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  127. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  128. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  129. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  130. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  131. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  132. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  133. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  134. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  135. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  136. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  137. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  138. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  139. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  140. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  141. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  142. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  143. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  144. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  145. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  146. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  147. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  148. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  149. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  150. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  151. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  152. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  153. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  154. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  155. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  156. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  157. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  158. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  159. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  160. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  161. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  162. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  163. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  164. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  165. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  166. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  167. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  168. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  169. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  170. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  171. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  172. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  173. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  174. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  175. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  176. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  177. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  178. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  179. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  180. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  181. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  182. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  183. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  184. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  185. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  186. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  187. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  188. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  189. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  190. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  191. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  192. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  193. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  194. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  195. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  196. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  197. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  198. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  199. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  200. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  201. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  202. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  203. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  204. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  205. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  206. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  207. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  208. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  209. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  210. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  211. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  212. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  213. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  214. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  215. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  216. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  217. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  218. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  219. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  220. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  221. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  222. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  223. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  224. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  225. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  226. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  227. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  228. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  229. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  230. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
  231. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  232. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  233. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  234. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  235. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  236. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  237. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  238. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  239. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  240. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  241. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  242. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  243. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  244. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  245. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  246. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  247. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  248. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  249. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  250. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  251. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  252. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  253. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  254. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  255. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  256. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  257. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  258. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  259. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  260. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  261. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  262. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  263. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  264. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  265. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  266. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  267. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  268. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  269. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  270. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  271. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  272. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  273. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  274. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  275. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  276. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  277. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  278. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  279. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  280. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  281. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  282. {sarj_python_lint-1.77.3 → sarj_python_lint-1.81.0}/src/sarj_python_lint/suppression_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.77.3
3
+ Version: 1.81.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/
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
21
  Requires-Dist: ruff>=0.17.2
22
- Requires-Dist: sarj-rule-contracts==0.4.1
22
+ Requires-Dist: sarj-rule-contracts==0.4.2
23
23
  Requires-Dist: sqlglot==30.24.0
24
24
  Requires-Dist: typer==0.27.3
25
25
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.77.3"
3
+ version = "1.81.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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["sarj-rule-contracts==0.4.1", "packaging==26.3", "ruff>=0.17.2", "sqlglot==30.24.0", "typer==0.27.3"]
18
+ dependencies = ["sarj-rule-contracts==0.4.2", "packaging==26.3", "ruff>=0.17.2", "sqlglot==30.24.0", "typer==0.27.3"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -166,6 +166,7 @@ def analyze(
166
166
 
167
167
  _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
168
168
  {
169
+ "SARJ020": frozenset({"SARJ437"}),
169
170
  "SARJ054": frozenset({"SARJ419"}),
170
171
  "SARJ448": frozenset({"SARJ419"}),
171
172
  "SARJ523": frozenset({"SARJ419"}),
@@ -340,14 +341,9 @@ def _suppressed_diagnostics(
340
341
 
341
342
  def _suppressed_codes(codes: dict[str, set[Severity]]) -> set[tuple[str, Severity]]:
342
343
  suppressed = _precedence_suppressions(codes)
343
- # SARJ094 is deliberately staged as a warning while teams calibrate the
344
- # FastAPI contract policy. Its route-aware return finding still owns the
345
- # overlapping annotation failure: emitting the blocking SARJ447 twin would
346
- # defeat warning staging and give two remediations for one return contract.
344
+ # SARJ094 warning staging owns the overlapping return failure; its blocking twin would defeat that policy.
347
345
  if "SARJ094" in codes:
348
346
  suppressed.update(_route_contract_suppressions(codes))
349
- # The promoted signature-restatement rules now block. Keep their errors and
350
- # drop only the lower-severity typed-docstring twin at the same owner.
351
347
  if _typed_docstring_warning_is_superseded(codes):
352
348
  suppressed.add(("SARJ092", Severity.WARNING))
353
349
  return suppressed
@@ -33,6 +33,7 @@ from sarj_python_lint.rule_base import Severity
33
33
  from sarj_python_lint.rules import (
34
34
  REGISTRY,
35
35
  duplicate_adjacent_exception_handler as exception_rule,
36
+ no_comment_cruft as comment_cruft_rule,
36
37
  prefer_collection_comprehension as collection_rule,
37
38
  prefer_or_pattern as pattern_rule,
38
39
  )
@@ -201,7 +202,11 @@ def _comment_fix(source: _Source, diagnostic: Diagnostic) -> list[TextEdit]:
201
202
  if token is None:
202
203
  return []
203
204
  body = token.string.removeprefix("#").strip()
204
- if diagnostic.code == "SARJ016" and _SEPARATOR.fullmatch(body) is None:
205
+ if (
206
+ diagnostic.code == "SARJ016"
207
+ and _SEPARATOR.fullmatch(body) is None
208
+ and not comment_cruft_rule.is_section_label(body)
209
+ ):
205
210
  return []
206
211
  start = source.starts[token.start[0] - 1]
207
212
  if source.text[start : start + token.start[1]].strip():
@@ -1312,7 +1317,7 @@ _BUILDERS: Final[dict[str, Callable[[_Source, Diagnostic], list[TextEdit]]]] = {
1312
1317
  "SARJ011": _security_fix,
1313
1318
  **dict.fromkeys(_COMMENT_CODES, _comment_fix),
1314
1319
  **dict.fromkeys(_WALRUS_CODES, _walrus_fix),
1315
- **dict.fromkeys({"SARJ050", "SARJ086", "SARJ087", "SARJ092", "SARJ420"}, _docstring_fix),
1320
+ **dict.fromkeys({"SARJ050", "SARJ086", "SARJ087", "SARJ092", "SARJ099", "SARJ420"}, _docstring_fix),
1316
1321
  "SARJ023": _stepdown_fix,
1317
1322
  "SARJ024": _repeated_fix,
1318
1323
  "SARJ032": _exhaustiveness_fix,
@@ -9,7 +9,8 @@ from typing import TYPE_CHECKING, final
9
9
  from sarj_rule_contracts import TextEdit
10
10
 
11
11
  from sarj_python_lint.rule_base import Severity
12
- from sarj_python_lint.rules.excessive_commentary import empty_collection_name
12
+ from sarj_python_lint.rules._comment_narration import CommentNarration, same_words
13
+ from sarj_python_lint.rules._prose_budget import groups
13
14
  from sarj_python_lint.rules.trailing_value_narration import UNIT_WORDS, number_key
14
15
 
15
16
 
@@ -18,16 +19,12 @@ if TYPE_CHECKING:
18
19
  from sarj_python_lint.rule_base import Diagnostic
19
20
 
20
21
 
21
- _IDENTIFIER = re.compile(r"[a-z]+(?:_[a-z]+)+\Z")
22
- _WORDS = re.compile(r"[A-Za-z]+(?:\s+[A-Za-z]+)*\.?\Z")
23
22
  _NUMBER_AND_UNIT = re.compile(r"([0-9][0-9_]*(?:\.[0-9_]+)?)\s*([A-Za-z]+)\Z")
24
23
  _UNUSED_API_DIRECTIVE = re.compile(
25
24
  r"#\s*ruff:\s*(?:noqa\s*:\s*TID251|file-ignore\s*\[\s*(?:TID251|banned-api)\s*\])\s*\Z",
26
25
  re.IGNORECASE,
27
26
  )
28
27
  _MIN_PARAGRAPH_LINES = 4
29
- _INITIALIZER_AND_RETURN_COUNT = 2
30
- _ACCUMULATOR_METHODS = frozenset({"add", "append", "extend", "update"})
31
28
 
32
29
 
33
30
  @final
@@ -46,6 +43,8 @@ class CommentFixes:
46
43
  self.statements: dict[int, list[ast.stmt]] = {}
47
44
  for statement in context.nodes(ast.stmt):
48
45
  self.statements.setdefault(statement.lineno, []).append(statement)
46
+ self.prose_groups = {(group.line, group.col): group for group in groups(context.path, context.source)}
47
+ self.narration: CommentNarration | None = None
49
48
 
50
49
  def fixes(self, diagnostic: Diagnostic) -> list[TextEdit]:
51
50
  token = self.comments.get(diagnostic.line)
@@ -82,7 +81,7 @@ class CommentFixes:
82
81
  return (
83
82
  statement.col_offset == comment.start[1]
84
83
  and statement.lineno == statement.end_lineno
85
- and _same_words(comment, name)
84
+ and same_words(comment.string.removeprefix("#").strip(), name)
86
85
  )
87
86
  case _:
88
87
  return False
@@ -116,35 +115,19 @@ class CommentFixes:
116
115
  return False
117
116
 
118
117
  def _restates_accumulation(self, first: tokenize.TokenInfo) -> list[tokenize.TokenInfo]:
119
- paragraph: list[tokenize.TokenInfo] = []
120
- line = first.start[0]
121
- while (comment := self.comments.get(line)) is not None:
122
- if comment.start[1] != first.start[1] or self.lines[line - 1][: comment.start[1]].strip():
123
- return []
124
- paragraph.append(comment)
125
- line += 1
126
- if len(paragraph) < _MIN_PARAGRAPH_LINES:
118
+ group = self.prose_groups.get((first.start[0], first.start[1] + 1))
119
+ if group is None:
127
120
  return []
128
- initialized = self._only_statement(line)
129
- if initialized is None or (name := empty_collection_name(initialized, self.context)) is None:
130
- return []
131
- owner = self.context.parents.get(initialized)
132
- if not isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef)):
133
- return []
134
- if len(owner.body) != len(paragraph) + _INITIALIZER_AND_RETURN_COUNT or owner.body[0] is not initialized:
135
- return []
136
- returned = owner.body[-1]
121
+ if self.narration is None:
122
+ self.narration = CommentNarration(self.context)
123
+ matched = self.narration.match(group)
137
124
  if (
138
- not isinstance(returned, ast.Return)
139
- or not isinstance(returned.value, ast.Name)
140
- or returned.value.id != name
125
+ matched is not None
126
+ and matched.kind == "accumulation"
127
+ and len(matched.comment_lines) >= _MIN_PARAGRAPH_LINES
141
128
  ):
142
- return []
143
- for comment, statement in zip(paragraph, owner.body[1:-1], strict=True):
144
- called = _accumulated_call(statement, name)
145
- if called is None or not _same_words(comment, called):
146
- return []
147
- return paragraph
129
+ return [self.comments[line] for line in matched.comment_lines]
130
+ return []
148
131
 
149
132
  def _remove(self, comment: tokenize.TokenInfo) -> TextEdit:
150
133
  line = comment.start[0]
@@ -157,16 +140,6 @@ class CommentFixes:
157
140
  )
158
141
 
159
142
 
160
- def _same_words(comment: tokenize.TokenInfo, identifier: str) -> bool:
161
- identifier = identifier.strip("_")
162
- body = comment.string.removeprefix("#").strip()
163
- return (
164
- _IDENTIFIER.fullmatch(identifier) is not None
165
- and _WORDS.fullmatch(body) is not None
166
- and body.removesuffix(".").casefold().split() == identifier.split("_")
167
- )
168
-
169
-
170
143
  def _literal_name_assignment(statement: ast.stmt) -> bool:
171
144
  return (
172
145
  isinstance(statement, ast.Assign)
@@ -175,17 +148,3 @@ def _literal_name_assignment(statement: ast.stmt) -> bool:
175
148
  and isinstance(statement.value, ast.Constant)
176
149
  and isinstance(statement.value.value, (str, bytes, int, float, complex, bool, type(None)))
177
150
  )
178
-
179
-
180
- def _accumulated_call(statement: ast.stmt, collection: str) -> str | None:
181
- match statement:
182
- case ast.Expr(
183
- value=ast.Call(
184
- func=ast.Attribute(value=ast.Name(id=name), attr=method),
185
- args=[ast.Call(func=ast.Name(id=called), args=[], keywords=[])],
186
- keywords=[],
187
- )
188
- ):
189
- return called if name == collection and method in _ACCUMULATOR_METHODS else None
190
- case _:
191
- return None
@@ -2,8 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from functools import cached_property
5
- from io import StringIO
5
+ from io import BytesIO, StringIO
6
6
  import re
7
+ import tokenize
7
8
  from typing import TYPE_CHECKING, final
8
9
 
9
10
  from sarj_rule_contracts import TextEdit
@@ -12,8 +13,10 @@ from sarj_python_lint._autofix_inert import inert_private_scope
12
13
  from sarj_python_lint.rule_base import Severity
13
14
  from sarj_python_lint.rules import REGISTRY
14
15
  from sarj_python_lint.rules._closed_helpers import dynamic_namespace
15
- from sarj_python_lint.rules._docstrings import ARG_SECTIONS, section_ranges
16
+ from sarj_python_lint.rules._comments import has_legal_notice
17
+ from sarj_python_lint.rules._docstrings import ARG_SECTIONS, docstring_expression, section_ranges
16
18
  from sarj_python_lint.rules._prose_budget import groups
19
+ from sarj_python_lint.rules.no_unnecessary_docstring import module_docstring_may_escape
17
20
 
18
21
 
19
22
  if TYPE_CHECKING:
@@ -178,6 +181,13 @@ class DocstringFixes:
178
181
  )
179
182
 
180
183
  def fixes(self, diagnostic: Diagnostic) -> list[TextEdit]:
184
+ if diagnostic.code in {"SARJ099", "SARJ420"}:
185
+ module_edits = self._module(diagnostic)
186
+ if module_edits:
187
+ return module_edits
188
+ return self._function(diagnostic)
189
+
190
+ def _function(self, diagnostic: Diagnostic) -> list[TextEdit]:
181
191
  for owner in self.context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
182
192
  if not owner.body or not private_unescaped_function(owner, self.context):
183
193
  continue
@@ -196,33 +206,87 @@ class DocstringFixes:
196
206
  return self._labels(expression, diagnostic)
197
207
  return []
198
208
 
209
+ def _module(self, diagnostic: Diagnostic) -> list[TextEdit]:
210
+ tree = self.context.tree
211
+ if tree is None or not tree.body:
212
+ return []
213
+ expression = docstring_expression(tree)
214
+ if expression is None:
215
+ return []
216
+ if (
217
+ (expression.lineno, expression.col_offset + 1) not in self.unconsumed
218
+ or diagnostic.line != expression.lineno
219
+ or self._standalone(expression) is None
220
+ ):
221
+ return []
222
+ if (
223
+ dynamic_namespace(self.context)
224
+ or frame_metadata_observed(self.context)
225
+ or any(node.attr == "modules" for node in self.context.nodes(ast.Attribute))
226
+ or any(node.attr == "import_module" for node in self.context.nodes(ast.Attribute))
227
+ or any(node.name == "import_module" for node in self.context.nodes(ast.alias))
228
+ ):
229
+ return []
230
+ if module_docstring_may_escape(self.context):
231
+ return []
232
+ if has_legal_notice(ast.get_docstring(tree, clean=False) or ""):
233
+ return []
234
+ if not self._module_header_preserved(expression):
235
+ return []
236
+ return self._remove(tree, expression)
237
+
238
+ def _module_header_preserved(self, expression: ast.Expr) -> bool:
239
+ end_line = expression.end_lineno or expression.lineno
240
+ if any(
241
+ token.type == tokenize.COMMENT and expression.lineno <= token.start[0] <= end_line
242
+ for token in tokenize.generate_tokens(StringIO(self.context.source, newline="").readline)
243
+ ):
244
+ return False
245
+ original = self.context.source
246
+ candidate = original[: self.starts[expression.lineno - 1]] + original[self.starts[end_line] :]
247
+ if candidate.startswith("#!") != original.startswith("#!"):
248
+ return False
249
+ try:
250
+ original_encoding = tokenize.detect_encoding(BytesIO(original.encode("utf-8")).readline)[0]
251
+ candidate_encoding = tokenize.detect_encoding(BytesIO(candidate.encode("utf-8")).readline)[0]
252
+ except SyntaxError, LookupError:
253
+ return False
254
+ return original_encoding == candidate_encoding
255
+
199
256
  def _literal(self, expression: ast.Expr) -> tuple[int, int, str] | None:
257
+ bounds = self._standalone(expression)
258
+ if bounds is None:
259
+ return None
260
+ start, end = bounds
261
+ literal = self.context.source[start:end]
262
+ if literal[:3] not in {'"""', "'''"} or literal[-3:] != literal[:3] or "\\" in literal:
263
+ return None
264
+ return start, end, literal
265
+
266
+ def _standalone(self, expression: ast.Expr) -> tuple[int, int] | None:
200
267
  end_line = expression.end_lineno
201
268
  end_column = expression.end_col_offset
202
269
  if end_line is None or end_column is None:
203
270
  return None
204
271
  start = self._offset(expression.lineno, expression.col_offset)
205
272
  end = self._offset(end_line, end_column)
206
- literal = self.context.source[start:end]
207
- if literal[:3] not in {'"""', "'''"} or literal[-3:] != literal[:3] or "\\" in literal:
208
- return None
209
273
  if self.context.source[self.starts[expression.lineno - 1] : start].strip():
210
274
  return None
211
275
  if self.context.source[end : self.starts[end_line]].strip():
212
276
  return None
213
- return start, end, literal
277
+ return start, end
214
278
 
215
279
  def _offset(self, line: int, byte_column: int) -> int:
216
280
  return self.starts[line - 1] + len(self.lines[line - 1].encode("utf-8")[:byte_column].decode("utf-8"))
217
281
 
218
- def _remove(self, owner: _Function, expression: ast.Expr) -> list[TextEdit]:
219
- if not owner.body[1:]:
282
+ def _remove(self, owner: ast.Module | _Function, expression: ast.Expr) -> list[TextEdit]:
283
+ if not owner.body[1:] and not isinstance(owner, ast.Module):
220
284
  return []
221
- next_statement = owner.body[1]
222
285
  if (
223
- isinstance(next_statement, ast.Expr)
224
- and isinstance(next_statement.value, ast.Constant)
225
- and isinstance(next_statement.value.value, str)
286
+ len(owner.body) > 1
287
+ and isinstance(owner.body[1], ast.Expr)
288
+ and isinstance(owner.body[1].value, ast.Constant)
289
+ and isinstance(owner.body[1].value.value, str)
226
290
  ):
227
291
  return []
228
292
  end_line = expression.end_lineno or expression.lineno
@@ -239,8 +303,7 @@ class DocstringFixes:
239
303
  else frozenset({"Returns", "Return", "Yields", "Yield"})
240
304
  )
241
305
  ranges = [(start, end) for name, start, end in section_ranges(value.value) if name in wanted]
242
- # The detector proves redundancy only for its preferred section heading.
243
- # Other heading variants can carry independent, meaningful contracts.
306
+ # Other section headings may carry independent contracts; only the detector-owned heading can be removed.
244
307
  if len(ranges) != 1:
245
308
  return []
246
309
  document = value.value
@@ -74,7 +74,7 @@ _PRIMITIVE_CLASSES: Final[dict[str, type[object]]] = {
74
74
  @dataclass(slots=True)
75
75
  class _Value:
76
76
  kind: _Kind
77
- literal: object = None
77
+ literal: bool | int | float | complex | str | bytes | None = None
78
78
  known: bool = False
79
79
  items: list[_Value] = field(default_factory=list)
80
80
  function: _Function | None = None
@@ -108,7 +108,7 @@ def _constant(value: object) -> _Value:
108
108
  bytes: _Kind.BYTES,
109
109
  }
110
110
  kind = kinds.get(type(value))
111
- if kind is None:
111
+ if kind is None or not isinstance(value, bool | int | float | complex | str | bytes | None):
112
112
  raise _UnsafeError
113
113
  return _Value(kind, value, known=True)
114
114
 
@@ -122,7 +122,7 @@ _MAX_WRAPPERS = 8
122
122
 
123
123
 
124
124
  class UnprovableCommandError(ValueError):
125
- """A wrapper executable position cannot be established safely."""
125
+ pass
126
126
 
127
127
 
128
128
  def unwrap_command(
@@ -210,8 +210,7 @@ def _env_wrapper_index(arguments: Sequence[str]) -> int:
210
210
  index += 1
211
211
  break
212
212
  index += _env_option_size(arguments, index)
213
- # Assignments follow options, including after --; later option-looking words
214
- # are executable operands, not additional env options.
213
+ # After env assignments begin, option-looking words are executable operands, including after --.
215
214
  while index < len(arguments) and "=" in arguments[index]:
216
215
  if arguments[index].startswith("="):
217
216
  msg = "env assignment requires a nonempty variable name"
@@ -484,8 +483,7 @@ def _shell_short_option(arguments: Sequence[str], index: int, values: frozenset[
484
483
  for flag in flags:
485
484
  if flag in values:
486
485
  operand = _following_operand(arguments, index + consumed - 1)
487
- # Bash shopt (-O) has a separate, version-dependent namespace. Keep
488
- # it unproven until a native option contract is available.
486
+ # Bash shopt (-O) uses a separate versioned namespace; no native contract is proven here.
489
487
  if flag == "O" or operand not in _SHELL_NAMED_OPTIONS:
490
488
  return ParsedOption(InterpreterInvocation("unknown"))
491
489
  consumed += 1
@@ -10,8 +10,8 @@ def parse_json(text: str) -> object:
10
10
  raise ValueError(message) from error
11
11
 
12
12
 
13
- def _unique_pairs(pairs: list[tuple[str, object]]) -> dict[str, object]:
14
- result: dict[str, object] = {}
13
+ def _unique_pairs[T](pairs: list[tuple[str, T]]) -> dict[str, T]:
14
+ result: dict[str, T] = {}
15
15
  for key, value in pairs:
16
16
  if key in result:
17
17
  message = f"duplicate JSON key: {key}"
@@ -77,11 +77,7 @@ class ColumnEncoding(StrEnum):
77
77
 
78
78
  @dataclass(frozen=True, slots=True)
79
79
  class TextEdit:
80
- """An exact, half-open range of one-based lines and UTF-8 byte columns.
81
-
82
- Columns refer to decoded source without its original leading BOM.
83
- """
84
-
80
+ # Coordinates are one-based UTF-8 bytes after BOM removal; end positions are exclusive.
85
81
  start_line: int
86
82
  start_col: int
87
83
  end_line: int
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from bisect import bisect_left
5
+ import re
6
+ from typing import TYPE_CHECKING, Literal, NamedTuple, final
7
+
8
+ from sarj_python_lint.rules._comments import comment_line_numbers, has_narration_qualifier, is_protected
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from sarj_python_lint._file_context import PythonFileContext
13
+ from sarj_python_lint.rules._prose_budget import ProseGroup
14
+
15
+
16
+ _IDENTIFIER = re.compile(r"[a-z]+(?:_[a-z]+)+\Z")
17
+ _WORDS = re.compile(r"[A-Za-z]+(?:\s+[A-Za-z]+)*\.?\Z")
18
+ _ACCUMULATOR_METHODS = frozenset({"add", "append", "extend", "update"})
19
+ _MIN_LINES = 2
20
+ _MIN_WORDS = 12
21
+
22
+
23
+ class NarrationMatch(NamedTuple):
24
+ kind: Literal["accumulation", "actions"]
25
+ comment_lines: tuple[int, ...]
26
+
27
+
28
+ @final
29
+ class CommentNarration:
30
+ def __init__(self, context: PythonFileContext) -> None:
31
+ self.context = context
32
+ self.comment_lines = tuple(sorted(comment_line_numbers(context.source)))
33
+ self.statements: dict[int, list[ast.stmt]] = {}
34
+ for statement in context.nodes(ast.stmt):
35
+ self.statements.setdefault(statement.lineno, []).append(statement)
36
+
37
+ def match(self, group: ProseGroup) -> NarrationMatch | None:
38
+ lines = group.text.splitlines()
39
+ if (
40
+ group.kind != "comment"
41
+ or len(lines) < _MIN_LINES
42
+ or len(group.text.split()) < _MIN_WORDS
43
+ or any(not line.strip() for line in lines)
44
+ or any(has_narration_qualifier(line) or is_protected(line) for line in lines)
45
+ ):
46
+ return None
47
+ following = self.statements.get(group.line + len(lines), [])
48
+ if len(following) != 1:
49
+ return None
50
+ first = following[0]
51
+ owner = self.context.parents.get(first)
52
+ if (
53
+ not isinstance(owner, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef))
54
+ or owner.body[0] is not first
55
+ or first.col_offset != group.col - 1
56
+ or self._intervening_comment(first, owner.body[-1])
57
+ ):
58
+ return None
59
+ if self._accumulation(owner, lines):
60
+ return NarrationMatch("accumulation", tuple(range(group.line, group.line + len(lines))))
61
+ if (
62
+ len(owner.body) == len(lines)
63
+ and not any(isinstance(node, ast.Return) for node in owner.body[:-1])
64
+ and all(
65
+ (called := _direct_action_call(statement)) is not None and same_words(line, called)
66
+ for line, statement in zip(lines, owner.body, strict=True)
67
+ )
68
+ ):
69
+ return NarrationMatch("actions", tuple(range(group.line, group.line + len(lines))))
70
+ return None
71
+
72
+ def _intervening_comment(self, first: ast.stmt, last: ast.stmt) -> bool:
73
+ index = bisect_left(self.comment_lines, first.lineno)
74
+ return index < len(self.comment_lines) and self.comment_lines[index] <= (last.end_lineno or last.lineno)
75
+
76
+ def _accumulation(self, owner: ast.Module | ast.FunctionDef | ast.AsyncFunctionDef, lines: list[str]) -> bool:
77
+ if not isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef)) or len(owner.body) != len(lines) + 2:
78
+ return False
79
+ name = empty_collection_name(owner.body[0], self.context)
80
+ returned = owner.body[-1]
81
+ if (
82
+ name is None
83
+ or not isinstance(returned, ast.Return)
84
+ or not isinstance(returned.value, ast.Name)
85
+ or returned.value.id != name
86
+ ):
87
+ return False
88
+ return all(
89
+ (called := _accumulated_call(statement, name)) is not None and same_words(line, called)
90
+ for line, statement in zip(lines, owner.body[1:-1], strict=True)
91
+ )
92
+
93
+
94
+ def same_words(body: str, identifier: str) -> bool:
95
+ identifier = identifier.strip("_")
96
+ return (
97
+ _IDENTIFIER.fullmatch(identifier) is not None
98
+ and _WORDS.fullmatch(body) is not None
99
+ and body.removesuffix(".").casefold().split() == identifier.split("_")
100
+ )
101
+
102
+
103
+ def empty_collection_name(statement: ast.stmt, context: PythonFileContext) -> str | None:
104
+ target: ast.expr | None = None
105
+ value: ast.expr | None = None
106
+ if isinstance(statement, ast.AnnAssign):
107
+ target, value = statement.target, statement.value
108
+ elif isinstance(statement, ast.Assign) and len(statement.targets) == 1:
109
+ target, value = statement.targets[0], statement.value
110
+ if not isinstance(target, ast.Name) or value is None:
111
+ return None
112
+ if isinstance(value, (ast.List, ast.Set)) and not value.elts:
113
+ return target.id
114
+ if isinstance(value, ast.Dict) and not value.keys:
115
+ return target.id
116
+ if (
117
+ isinstance(value, ast.Call)
118
+ and isinstance(value.func, ast.Name)
119
+ and value.func.id in {"dict", "list", "set"}
120
+ and not value.args
121
+ and not value.keywords
122
+ ):
123
+ provenance = context.static_resource_provenance
124
+ if provenance.builtin_namespace_untouched and provenance.binding(value.func.id, value.func) is None:
125
+ return target.id
126
+ return None
127
+
128
+
129
+ def _direct_action_call(statement: ast.stmt) -> str | None:
130
+ match statement:
131
+ case ast.Expr(value=value) | ast.Assign(targets=[ast.Name()], value=value) | ast.Return(value=value):
132
+ return _zero_argument_call(value)
133
+ case _:
134
+ return None
135
+
136
+
137
+ def _zero_argument_call(value: ast.AST | None) -> str | None:
138
+ match value:
139
+ case ast.Call(func=ast.Name(id=called), args=[], keywords=[]):
140
+ return called
141
+ case _:
142
+ return None
143
+
144
+
145
+ def _accumulated_call(statement: ast.stmt, collection: str) -> str | None:
146
+ match statement:
147
+ case ast.Expr(
148
+ value=ast.Call(
149
+ func=ast.Attribute(value=ast.Name(id=name), attr=method),
150
+ args=[value],
151
+ keywords=[],
152
+ )
153
+ ):
154
+ return _zero_argument_call(value) if name == collection and method in _ACCUMULATOR_METHODS else None
155
+ case _:
156
+ return None
@@ -16,6 +16,16 @@ if TYPE_CHECKING:
16
16
  from pathlib import Path
17
17
 
18
18
 
19
+ _LICENSE_RE = re.compile(
20
+ r"copyright|license|licen[cs]ed|spdx|permission is hereby granted|all rights reserved",
21
+ re.IGNORECASE,
22
+ )
23
+
24
+
25
+ def has_legal_notice(body: str) -> bool:
26
+ return bool(_LICENSE_RE.search(body))
27
+
28
+
19
29
  # S1 — external references: URLs, tickets, RFCs, PEPs, CVEs, issues, and handles.
20
30
  _REF_RE = re.compile(
21
31
  r"https?://|\bRFC[- ]?\d+|\bPEP[- ]?\d+|\bCVE-\d{4}|"
@@ -57,7 +67,7 @@ _NEGATION_RE = re.compile(
57
67
  # S6 — upstream/vendor quirk, workaround provenance, or an external contract.
58
68
  _UPSTREAM_RE = re.compile(
59
69
  r"\b(?:upstream|workaround|quirk|backport|vendored|regression|fixed upstream|"
60
- r"requires?\b|convention\b|rate.?limit|deprecat|opts? in(?:to)?\b|"
70
+ r"require[sd]?\b|convention\b|rate.?limit|deprecat|opts? in(?:to)?\b|"
61
71
  r"raises?\b.{0,60}\b(?:when|if|unless)\b)",
62
72
  re.IGNORECASE,
63
73
  )
@@ -113,6 +123,32 @@ def has_external_reference(body: str) -> bool:
113
123
  return bool(_REF_RE.search(body))
114
124
 
115
125
 
126
+ _NARRATION_MODALITY_RE = re.compile(r"\b(?:can|could|should|shall|may|might|must|will|would|cannot)\b", re.IGNORECASE)
127
+ _NARRATION_LEAD_IN_RE = re.compile(r":$")
128
+ _NARRATION_EMPHASIS_RE = re.compile(r"\*\w[^*]*\*|`[^`]+`")
129
+ _NARRATION_NEGATION_RE = re.compile(r"\b(?:no|not|never|neither|nor|without|none|non)\b", re.IGNORECASE)
130
+ _NARRATION_QUALIFIER_RE = re.compile(
131
+ r"\b(?:all|any|both|each|every|some|only|need|needs|needed|require|requires|required)\b|"
132
+ r"^\s*(?:and|after|before|finally|given|next|then|when)\b",
133
+ re.IGNORECASE,
134
+ )
135
+ _NARRATION_NAVIGATION_RE = re.compile(r"^\s*(?:sheet|slide|step)\s+\d+\s*(?:[:.\-]|$)", re.IGNORECASE)
136
+
137
+
138
+ def has_narration_qualifier(body: str) -> bool:
139
+ return any(
140
+ pattern.search(body)
141
+ for pattern in (
142
+ _NARRATION_MODALITY_RE,
143
+ _NARRATION_LEAD_IN_RE,
144
+ _NARRATION_EMPHASIS_RE,
145
+ _NARRATION_NEGATION_RE,
146
+ _NARRATION_QUALIFIER_RE,
147
+ _NARRATION_NAVIGATION_RE,
148
+ )
149
+ )
150
+
151
+
116
152
  # Below this length an inflection strip would eat the word itself.
117
153
  _MIN_STEM_LENGTH = 3
118
154
 
@@ -124,7 +124,7 @@ class _DataUnions:
124
124
  message=f"Data union `{name}` has no shared disjoint Literal discriminator. "
125
125
  "Add a common field such as `kind` or `scope` with non-overlapping Literal values "
126
126
  "on every variant; preserve constructor and serialized contracts when choosing defaults.",
127
- severity=Severity.WARNING,
127
+ severity=Severity.ERROR,
128
128
  )
129
129
  )
130
130
  return findings