maintainability-agent 2.1.0__tar.gz → 2.2.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 (310) hide show
  1. {maintainability_agent-2.1.0/src/maintainability_agent.egg-info → maintainability_agent-2.2.0}/PKG-INFO +2 -2
  2. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/README.md +1 -1
  3. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/pyproject.toml +1 -1
  4. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0/src/maintainability_agent.egg-info}/PKG-INFO +2 -2
  5. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_agent.egg-info/SOURCES.txt +3 -0
  6. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/__init__.py +1 -1
  7. maintainability_agent-2.2.0/src/maintainability_audit/_gates.py +122 -0
  8. maintainability_agent-2.2.0/src/maintainability_audit/_ratchet.py +109 -0
  9. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/cli.py +14 -46
  10. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/config.py +1 -1
  11. maintainability_agent-2.2.0/tests/test_dimension_ratchet.py +155 -0
  12. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_identity_resolution.py +5 -1
  13. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_scope_conformance.py +6 -4
  14. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/LICENSE +0 -0
  15. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/setup.cfg +0 -0
  16. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_agent.egg-info/dependency_links.txt +0 -0
  17. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_agent.egg-info/entry_points.txt +0 -0
  18. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_agent.egg-info/requires.txt +0 -0
  19. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_agent.egg-info/top_level.txt +0 -0
  20. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/__main__.py +0 -0
  21. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_adapters.py +0 -0
  22. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_analysis.py +0 -0
  23. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_aspects.py +0 -0
  24. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_assets/analyzer-catalog.json +0 -0
  25. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_assets/standard.md +0 -0
  26. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_backfill.py +0 -0
  27. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_bands.py +0 -0
  28. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_built_ins.py +0 -0
  29. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_calibration.py +0 -0
  30. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_catalog.py +0 -0
  31. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_charts.py +0 -0
  32. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_cognitive.py +0 -0
  33. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_config_defaults.py +0 -0
  34. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_conformance.py +0 -0
  35. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_corroborate.py +0 -0
  36. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_coverage_notes.py +0 -0
  37. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_derive.py +0 -0
  38. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_discovery.py +0 -0
  39. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_documents.py +0 -0
  40. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_economics.py +0 -0
  41. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_economics_view.py +0 -0
  42. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_environment.py +0 -0
  43. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_evidence_view.py +0 -0
  44. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_finding_match.py +0 -0
  45. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_first_run.py +0 -0
  46. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_formula.py +0 -0
  47. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_generic.py +0 -0
  48. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_grant_ledger.py +0 -0
  49. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_history_view.py +0 -0
  50. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_hostile_prompt.py +0 -0
  51. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_hotspots.py +0 -0
  52. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_html_report_sections.py +0 -0
  53. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_html_view.py +0 -0
  54. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_identity.py +0 -0
  55. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_jvm_adapters.py +0 -0
  56. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_markdown_sections.py +0 -0
  57. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_masking.py +0 -0
  58. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_mcp_audit.py +0 -0
  59. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_mcp_gate.py +0 -0
  60. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_mcp_grants.py +0 -0
  61. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_mcp_refusals.py +0 -0
  62. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_mcp_resources.py +0 -0
  63. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_mcp_setup.py +0 -0
  64. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_metric_adapters.py +0 -0
  65. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_metrics_types.py +0 -0
  66. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_pillars.py +0 -0
  67. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_practice.py +0 -0
  68. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_pressures.py +0 -0
  69. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_ranges_c.py +0 -0
  70. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_ranges_core.py +0 -0
  71. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_ranges_cpp.py +0 -0
  72. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_ranges_csharp.py +0 -0
  73. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_ranges_fortran.py +0 -0
  74. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_ranges_java.py +0 -0
  75. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_ranges_js.py +0 -0
  76. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_recurrence.py +0 -0
  77. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_runner.py +0 -0
  78. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_safe_write.py +0 -0
  79. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_scan_history.py +0 -0
  80. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_scan_view.py +0 -0
  81. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_second_source.py +0 -0
  82. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_selection.py +0 -0
  83. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_semantic.py +0 -0
  84. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_semantic_policy.py +0 -0
  85. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_semantic_ts.py +0 -0
  86. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_semantic_view.py +0 -0
  87. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_setup_errors.py +0 -0
  88. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_setup_persist.py +0 -0
  89. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_skill_data/SKILL.md +0 -0
  90. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_skill_data/agents/anthropic.yaml +0 -0
  91. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_skill_data/agents/copilot.yaml +0 -0
  92. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_skill_data/agents/openai.yaml +0 -0
  93. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_skill_data/copilot/maintainability-agent.prompt.md +0 -0
  94. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_skill_data/references/finding-taxonomy.md +0 -0
  95. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_skill_install.py +0 -0
  96. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_stored_grants.py +0 -0
  97. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_tdd_view.py +0 -0
  98. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_test_execution.py +0 -0
  99. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_test_pairing.py +0 -0
  100. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_tokens.py +0 -0
  101. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_tool_adapters.py +0 -0
  102. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_trends.py +0 -0
  103. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_user_config.py +0 -0
  104. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_verdict_adapters.py +0 -0
  105. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_verification.py +0 -0
  106. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_work_order.py +0 -0
  107. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_work_order_view.py +0 -0
  108. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_work_order_weights.py +0 -0
  109. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/_xml.py +0 -0
  110. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/baseline.py +0 -0
  111. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/deadcode.py +0 -0
  112. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/declarations.py +0 -0
  113. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/duplication.py +0 -0
  114. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/evidence.py +0 -0
  115. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/git_tools.py +0 -0
  116. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/history.py +0 -0
  117. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/idioms.py +0 -0
  118. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/instructions.py +0 -0
  119. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/mcp_server.py +0 -0
  120. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/metrics.py +0 -0
  121. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/prompts.py +0 -0
  122. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/renderers.py +0 -0
  123. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/report.py +0 -0
  124. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/sarif.py +0 -0
  125. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/scoring.py +0 -0
  126. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/similarity.py +0 -0
  127. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/src/maintainability_audit/source.py +0 -0
  128. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_acquisition_trust.py +0 -0
  129. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_adapter_docs.py +0 -0
  130. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_adapter_recordings.py +0 -0
  131. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_adapters.py +0 -0
  132. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_added_lines.py +0 -0
  133. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analysis_coverage.py +0 -0
  134. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analysis_execution.py +0 -0
  135. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_bridge.py +0 -0
  136. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_config_isolation.py +0 -0
  137. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_estimate_claims.py +0 -0
  138. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_pinning.py +0 -0
  139. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_primary_estimate.py +0 -0
  140. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_provenance.py +0 -0
  141. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_provenance_exclusions.py +0 -0
  142. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_scoring_path.py +0 -0
  143. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_spread_range.py +0 -0
  144. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_analyzer_xml_bounds.py +0 -0
  145. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_annotations_match_behaviour.py +0 -0
  146. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_anticipated_refusals.py +0 -0
  147. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_architecture.py +0 -0
  148. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_artifact_write_route_class.py +0 -0
  149. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_aspect_coverage.py +0 -0
  150. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_asset_snapshot_class.py +0 -0
  151. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_ast_reading_resolves_spellings.py +0 -0
  152. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_audit_components.py +0 -0
  153. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_authorization_freshness.py +0 -0
  154. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_authorship_gates.py +0 -0
  155. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_backfill.py +0 -0
  156. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_band_concealment_class.py +0 -0
  157. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_band_concealment_p3.py +0 -0
  158. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_band_pressures.py +0 -0
  159. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_bands.py +0 -0
  160. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_body_or_signature_class.py +0 -0
  161. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_c_declarations.py +0 -0
  162. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_calibration_corpus.py +0 -0
  163. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_calibration_drift.py +0 -0
  164. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_calibration_tooling.py +0 -0
  165. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_catalog_selection.py +0 -0
  166. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_catalog_tier_promise.py +0 -0
  167. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_chat_primary_docs.py +0 -0
  168. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_checkstyle_adapter.py +0 -0
  169. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_checkstyle_catalog.py +0 -0
  170. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_ci_history_recipe.py +0 -0
  171. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_ci_installs_the_analyzer_pool.py +0 -0
  172. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_claimed_languages.py +0 -0
  173. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_cli.py +0 -0
  174. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_clone_group_class.py +0 -0
  175. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_cognitive_complexity.py +0 -0
  176. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_cohort_statistics.py +0 -0
  177. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_concealment_properties.py +0 -0
  178. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_config_shape.py +0 -0
  179. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_consent_and_grant.py +0 -0
  180. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_consumer_migration.py +0 -0
  181. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_contract_claims.py +0 -0
  182. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_corroborate.py +0 -0
  183. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_coverage_by_language.py +0 -0
  184. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_cpp_declarations.py +0 -0
  185. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_csharp_declarations.py +0 -0
  186. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_d15_composition.py +0 -0
  187. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_d15_goal_directed.py +0 -0
  188. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_dead_code.py +0 -0
  189. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_declaration_edge_cases.py +0 -0
  190. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_declaration_grading.py +0 -0
  191. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_declaration_ranges.py +0 -0
  192. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_declaration_source.py +0 -0
  193. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_declared_authorship.py +0 -0
  194. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_declared_imports.py +0 -0
  195. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_derivation_mix.py +0 -0
  196. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_determinism.py +0 -0
  197. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_direct_runner_availability_class.py +0 -0
  198. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_discovery.py +0 -0
  199. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_doc_claims.py +0 -0
  200. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_docs_links.py +0 -0
  201. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_economic_context.py +0 -0
  202. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_empty_output_not_clean.py +0 -0
  203. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_empty_output_not_clean_adapters.py +0 -0
  204. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_environment_work_order.py +0 -0
  205. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_evidence_invariants.py +0 -0
  206. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_evidence_normalization.py +0 -0
  207. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_evidence_properties.py +0 -0
  208. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_exclusion_dialects.py +0 -0
  209. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_falsifier_standard.py +0 -0
  210. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_file_list_cap_class.py +0 -0
  211. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_finding_identity.py +0 -0
  212. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_first_run_elicitation.py +0 -0
  213. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_first_run_prompt.py +0 -0
  214. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_first_run_reelicitation.py +0 -0
  215. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_fix_breadth_window.py +0 -0
  216. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_fixed_form_fortran.py +0 -0
  217. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_format_ask.py +0 -0
  218. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_fortran_declarations.py +0 -0
  219. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_fortran_metrics.py +0 -0
  220. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_fortran_practice.py +0 -0
  221. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_git_argv.py +0 -0
  222. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_git_read_only.py +0 -0
  223. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_git_readonly_class.py +0 -0
  224. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_grant_only_user_tier.py +0 -0
  225. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_history.py +0 -0
  226. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_history_boundary.py +0 -0
  227. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_history_manifest.py +0 -0
  228. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_history_schema2.py +0 -0
  229. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_history_window.py +0 -0
  230. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_hostile_prompt.py +0 -0
  231. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_html_executive.py +0 -0
  232. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_html_section_parity_class.py +0 -0
  233. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_identity_docs.py +0 -0
  234. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_idiom_divergence.py +0 -0
  235. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_installed_wheel_runtime.py +0 -0
  236. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_instruction_targets.py +0 -0
  237. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_java_range_guard.py +0 -0
  238. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_java_ranges.py +0 -0
  239. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_java_scored.py +0 -0
  240. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_java_wiring.py +0 -0
  241. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_js_complexity_operators.py +0 -0
  242. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_js_declarations.py +0 -0
  243. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_language_coverage.py +0 -0
  244. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_mcp_baseline_payload.py +0 -0
  245. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_mcp_history.py +0 -0
  246. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_mcp_server.py +0 -0
  247. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_migration_1_0.py +0 -0
  248. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_near_duplicates.py +0 -0
  249. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_network_disclosure.py +0 -0
  250. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_one_setup_across_surfaces_class.py +0 -0
  251. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_pattern_budget.py +0 -0
  252. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_phase6_claims.py +0 -0
  253. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_pillars_and_practice.py +0 -0
  254. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_platform_claim.py +0 -0
  255. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_pmd_adapter.py +0 -0
  256. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_pmd_audit_closures.py +0 -0
  257. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_pool_runs_by_config.py +0 -0
  258. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_population_floors.py +0 -0
  259. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_practice_nested_gates_class.py +0 -0
  260. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_presentation_refusals.py +0 -0
  261. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_promises.py +0 -0
  262. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_prompt_escalation_once.py +0 -0
  263. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_prompt_honesty.py +0 -0
  264. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_prompt_location_escalation_class.py +0 -0
  265. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_prove_falsifiers.py +0 -0
  266. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_python_declarations.py +0 -0
  267. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_read_boundary.py +0 -0
  268. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_readme_claims.py +0 -0
  269. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_recurrence.py +0 -0
  270. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_release_plan.py +0 -0
  271. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_release_plan_status.py +0 -0
  272. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_report_remedy_copy.py +0 -0
  273. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_roles_recorded.py +0 -0
  274. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_root_grants.py +0 -0
  275. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_rubric_ignores_project_lint_config.py +0 -0
  276. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_runner.py +0 -0
  277. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_sarif.py +0 -0
  278. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_scan_history.py +0 -0
  279. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_scan_scope.py +0 -0
  280. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_scanning.py +0 -0
  281. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_schema_fidelity.py +0 -0
  282. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_scoring_calibration.py +0 -0
  283. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_semantic_policy.py +0 -0
  284. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_setup_gate_completeness.py +0 -0
  285. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_setup_gate_independent_contract.py +0 -0
  286. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_setup_precondition.py +0 -0
  287. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_skill_install.py +0 -0
  288. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_source_index.py +0 -0
  289. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_spotbugs_adapter.py +0 -0
  290. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_spotbugs_catalog.py +0 -0
  291. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_stage8_contract.py +0 -0
  292. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_tdd_structure.py +0 -0
  293. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_test_execution_class.py +0 -0
  294. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_three_presentations.py +0 -0
  295. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_trends.py +0 -0
  296. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_type_fields_not_functions_class.py +0 -0
  297. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_uat_falsifiers.py +0 -0
  298. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_unread_code.py +0 -0
  299. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_user_config_tier.py +0 -0
  300. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_verification_audit_closures.py +0 -0
  301. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_verified_grade.py +0 -0
  302. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_version_claim.py +0 -0
  303. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_wheel_contents.py +0 -0
  304. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_work_order.py +0 -0
  305. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_work_order_items.py +0 -0
  306. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_work_order_selection.py +0 -0
  307. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_workflow_supply_chain.py +0 -0
  308. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_write_boundary.py +0 -0
  309. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_write_route_class.py +0 -0
  310. {maintainability_agent-2.1.0 → maintainability_agent-2.2.0}/tests/test_written_record.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maintainability-agent
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Maintainability gate and AI remediation prompt generator for CI.
5
5
  Author: Marshall Guillory
6
6
  License: MIT
@@ -44,7 +44,7 @@ Dynamic: license-file
44
44
  **A deterministic, offline maintainability audit whose output is a _bounded
45
45
  work order_ for an AI coding agent** — a copy-paste prompt, per finding, that
46
46
  says *fix exactly these and refactor nothing else*. Chat-primary; CLI for CI.
47
- Version **2.1.0**.
47
+ Version **2.2.0**.
48
48
 
49
49
  **Languages parsed:** Python, Java, C, C++, C#, Fortran (free-form *and*
50
50
  fixed-form), and the JS/TS family — each by a scanner written for it, and
@@ -5,7 +5,7 @@
5
5
  **A deterministic, offline maintainability audit whose output is a _bounded
6
6
  work order_ for an AI coding agent** — a copy-paste prompt, per finding, that
7
7
  says *fix exactly these and refactor nothing else*. Chat-primary; CLI for CI.
8
- Version **2.1.0**.
8
+ Version **2.2.0**.
9
9
 
10
10
  **Languages parsed:** Python, Java, C, C++, C#, Fortran (free-form *and*
11
11
  fixed-form), and the JS/TS family — each by a scanner written for it, and
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maintainability-agent"
7
- version = "2.1.0"
7
+ version = "2.2.0"
8
8
  description = "Maintainability gate and AI remediation prompt generator for CI."
9
9
  readme = "README.md"
10
10
  # 3.11, not 3.10. `_discovery`, `_pillars` and `_runner` all import
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maintainability-agent
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Maintainability gate and AI remediation prompt generator for CI.
5
5
  Author: Marshall Guillory
6
6
  License: MIT
@@ -44,7 +44,7 @@ Dynamic: license-file
44
44
  **A deterministic, offline maintainability audit whose output is a _bounded
45
45
  work order_ for an AI coding agent** — a copy-paste prompt, per finding, that
46
46
  says *fix exactly these and refactor nothing else*. Chat-primary; CLI for CI.
47
- Version **2.1.0**.
47
+ Version **2.2.0**.
48
48
 
49
49
  **Languages parsed:** Python, Java, C, C++, C#, Fortran (free-form *and*
50
50
  fixed-form), and the JS/TS family — each by a scanner written for it, and
@@ -33,6 +33,7 @@ src/maintainability_audit/_evidence_view.py
33
33
  src/maintainability_audit/_finding_match.py
34
34
  src/maintainability_audit/_first_run.py
35
35
  src/maintainability_audit/_formula.py
36
+ src/maintainability_audit/_gates.py
36
37
  src/maintainability_audit/_generic.py
37
38
  src/maintainability_audit/_grant_ledger.py
38
39
  src/maintainability_audit/_history_view.py
@@ -62,6 +63,7 @@ src/maintainability_audit/_ranges_csharp.py
62
63
  src/maintainability_audit/_ranges_fortran.py
63
64
  src/maintainability_audit/_ranges_java.py
64
65
  src/maintainability_audit/_ranges_js.py
66
+ src/maintainability_audit/_ratchet.py
65
67
  src/maintainability_audit/_recurrence.py
66
68
  src/maintainability_audit/_runner.py
67
69
  src/maintainability_audit/_safe_write.py
@@ -187,6 +189,7 @@ tests/test_declared_authorship.py
187
189
  tests/test_declared_imports.py
188
190
  tests/test_derivation_mix.py
189
191
  tests/test_determinism.py
192
+ tests/test_dimension_ratchet.py
190
193
  tests/test_direct_runner_availability_class.py
191
194
  tests/test_discovery.py
192
195
  tests/test_doc_claims.py
@@ -1,3 +1,3 @@
1
1
  """Maintainability Audit CI."""
2
2
 
3
- __version__ = "2.1.0"
3
+ __version__ = "2.2.0"
@@ -0,0 +1,122 @@
1
+ """What happens after the audit and before the exit code.
2
+
3
+ Split from `cli.py` when it crossed this project's 500-line file gate —
4
+ the same reason `_mcp_setup`, `_adapters` and `test_architecture` were
5
+ split before it.
6
+
7
+ The split is not arbitrary. Everything here composes from the **finished
8
+ report**: whether the diff obeyed its work order, whether a dimension
9
+ slipped since the last comparable scan, and what those two facts mean for
10
+ the process exit code. None of it measures a tree and none of it may reach
11
+ scoring — a fact about an agent's behaviour, or about the difference
12
+ between two scans, is not evidence about the code's condition.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ from .baseline import findings_not_in_baseline
22
+ from .git_tools import added_lines, changed_paths
23
+
24
+
25
+ def attach_conformance(args: argparse.Namespace, report: dict) -> None:
26
+ """Record how the diff relates to the work order, when asked.
27
+
28
+ Assembly, not scoring: the record is attached to the report and reaches
29
+ no dimension, because whether a diff was obedient is a fact about an
30
+ agent's behaviour and not evidence about the code's condition.
31
+ """
32
+ if not args.conformance:
33
+ return
34
+ from ._conformance import scope_conformance
35
+
36
+ root = Path(report["root"])
37
+ changed = changed_paths(root, args.conformance)
38
+ added = added_lines(root, args.conformance)
39
+ report["scope_conformance"] = scope_conformance(
40
+ report, changed, args.conformance, added
41
+ )
42
+
43
+
44
+ def attach_ratchet(report: dict, history_path: Path) -> None:
45
+ """Record whether any category slipped since the last comparable scan."""
46
+ from ._ratchet import dimension_ratchet
47
+ from ._scan_history import read_history
48
+
49
+ if not history_path.exists():
50
+ return
51
+ report["dimension_ratchet"] = dimension_ratchet(read_history(history_path))
52
+
53
+
54
+ def _attach_post_audit_records(
55
+ args: argparse.Namespace, report: dict, history_path: Path
56
+ ) -> None:
57
+ """Records composed from the finished report, before it is rendered.
58
+
59
+ Both read the report rather than the tree, and neither reaches scoring:
60
+ whether a diff obeyed its work order, and whether a dimension slipped
61
+ since the last comparable scan, are facts about a change and its
62
+ history — not evidence about the code's condition.
63
+ """
64
+ attach_conformance(args, report)
65
+ attach_ratchet(report, history_path)
66
+
67
+
68
+ def _conformance_exit(args: argparse.Namespace, report: dict) -> int:
69
+ """0 to continue; otherwise the exit code the conformance gate demands."""
70
+ if not args.fail_on_out_of_scope:
71
+ return 0
72
+ record = report.get("scope_conformance")
73
+ if record is None:
74
+ print("--fail-on-out-of-scope needs --conformance REVSPEC", file=sys.stderr)
75
+ return 2
76
+ # `clean`, not `conformant`: staying in scope while silencing the finding
77
+ # is the evasion the pairing exists to catch, and a gate that accepted it
78
+ # would be satisfied by the thing it guards against.
79
+ return 0 if record["clean"] else 1
80
+
81
+
82
+ def _regression_exit(args: argparse.Namespace, report: dict) -> int:
83
+ """0 to continue; otherwise the exit code the ratchet demands."""
84
+ if not args.fail_on_regression:
85
+ return 0
86
+ ratchet = report.get("dimension_ratchet")
87
+ if ratchet is None:
88
+ print("--fail-on-regression needs a recorded history (--record-history)",
89
+ file=sys.stderr)
90
+ return 2
91
+ # A run that could not compare does not pass quietly: exiting 0 where the
92
+ # question was never asked is the "absence read as a pass" shape.
93
+ #
94
+ # The reason is not interpolated — it derives from recorded scan history,
95
+ # and code scanning is right that history fields have no business in a log
96
+ # line. It stays in the report, which a caller reads deliberately.
97
+ # Suppressing that alert was refused: this release ships the check that
98
+ # treats a suppression as a finding rather than a fix.
99
+ if not ratchet["comparable"]:
100
+ print("no comparable scan for --fail-on-regression; see "
101
+ "dimension_ratchet.reason in the report", file=sys.stderr)
102
+ return 2
103
+ return 1 if ratchet["regressed"] else 0
104
+
105
+
106
+ def audit_exit_code(args: argparse.Namespace, report: dict) -> int:
107
+ if args.fail_on_new:
108
+ # Structured matching, never a label-set difference: a label
109
+ # changes under `git mv` and same-name reorder, and failing a
110
+ # build over either is a false report about the change.
111
+ root = Path(report["root"])
112
+ if findings_not_in_baseline(report, args.baseline, root):
113
+ return 1
114
+ if args.fail_on_gate and report["hard_gate_failures"]:
115
+ return 1
116
+ conformance = _conformance_exit(args, report)
117
+ if conformance:
118
+ return conformance
119
+ regression = _regression_exit(args, report)
120
+ if regression:
121
+ return regression
122
+ return 0
@@ -0,0 +1,109 @@
1
+ """Did any dimension get worse than the last comparable scan?
2
+
3
+ The third mechanism behind the attestation. `--fail-on-new` already catches
4
+ a finding that clears and returns, using structured identity so a rename is
5
+ not a new finding. What nothing caught is the slower failure: a change that
6
+ improves one dimension while quietly regressing another. Both scans are
7
+ green by every existing gate, the overall estimate can even rise, and the
8
+ thing that got worse is never named.
9
+
10
+ **The comparison is only honest inside one segment.** History is already
11
+ split wherever the instrument changed — a different calibration constant, a
12
+ different analyzer population — because two scans taken with different
13
+ instruments cannot be subtracted. `_scan_history.segments` owns that rule,
14
+ and this module refuses rather than reaching across a break. A ratchet that
15
+ compared a 1.x score to a 2.0 score would have reported every repository on
16
+ earth as catastrophically regressed on the day the corpus was extended, and
17
+ it would have been the ratchet that was wrong.
18
+
19
+ So there are three outcomes, not two: **held**, **regressed**, and **not
20
+ comparable**. The third is not a pass. It says the question could not be
21
+ asked, which is what a reader needs in order to know they are not being
22
+ reassured by silence.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any
28
+
29
+ from ._scan_history import ScanRecord, segments
30
+
31
+ #: Scores are reported to one decimal, so a difference below this is the
32
+ #: rounding boundary rather than a movement. Named rather than inlined
33
+ #: because it is a judgment: too tight and every scan trips on noise, too
34
+ #: loose and a real slide hides under the threshold.
35
+ TOLERANCE = 0.05
36
+
37
+
38
+ def _series(record: ScanRecord, field: str) -> dict[str, float]:
39
+ values = getattr(record, field, None) or {}
40
+ return {
41
+ name: float(value) for name, value in values.items()
42
+ if isinstance(value, (int, float))
43
+ }
44
+
45
+
46
+ def _regressions(
47
+ previous: ScanRecord, current: ScanRecord, field: str
48
+ ) -> list[dict[str, Any]]:
49
+ """Names that dropped, with the size of the drop."""
50
+ before, after = _series(previous, field), _series(current, field)
51
+ found = []
52
+ for name, was in sorted(before.items()):
53
+ # A name the current scan does not carry is not a regression: the
54
+ # aspect became unmeasurable, which the evidence model already
55
+ # reports as `Unknown` rather than as a zero. Scoring it as a drop
56
+ # to nothing would turn withheld evidence into a failure, which is
57
+ # the exact inversion P3 forbids.
58
+ if name not in after:
59
+ continue
60
+ now = after[name]
61
+ if was - now > TOLERANCE:
62
+ found.append({
63
+ "name": name, "was": round(was, 2),
64
+ "now": round(now, 2), "drop": round(was - now, 2),
65
+ })
66
+ return found
67
+
68
+
69
+ def dimension_ratchet(records: list[ScanRecord]) -> dict[str, Any]:
70
+ """Compare the newest scan with the previous comparable one.
71
+
72
+ `comparable` is the load-bearing field. False means the two scans sit
73
+ either side of an instrument change, or there is no earlier scan at
74
+ all — in both cases the question was not asked, and a caller must not
75
+ read that as "nothing regressed".
76
+ """
77
+ if len(records) < 2:
78
+ return {
79
+ "comparable": False,
80
+ "reason": "no earlier scan to compare against",
81
+ "regressed": [],
82
+ }
83
+
84
+ latest = segments(records)[-1]
85
+ if len(latest.records) < 2:
86
+ return {
87
+ "comparable": False,
88
+ "reason": (
89
+ latest.break_reason
90
+ or "the previous scan was taken with a different instrument"
91
+ ),
92
+ "regressed": [],
93
+ }
94
+
95
+ previous, current = latest.records[-2], latest.records[-1]
96
+ categories = _regressions(previous, current, "categories")
97
+ aspects = _regressions(previous, current, "aspects")
98
+
99
+ return {
100
+ "comparable": True,
101
+ "against": getattr(previous, "commit", None),
102
+ "categories": categories,
103
+ "aspects": aspects,
104
+ # Categories only. An aspect can move while the category holding it
105
+ # holds, and failing on every aspect wobble makes the gate noise.
106
+ # Aspects are reported so a reader can see what moved underneath.
107
+ "regressed": categories,
108
+ "held": not categories,
109
+ }
@@ -14,6 +14,10 @@ from ._first_run import (
14
14
  maybe_prompt_first_run,
15
15
  maybe_prompt_test_command,
16
16
  )
17
+ from ._gates import (
18
+ _attach_post_audit_records,
19
+ audit_exit_code,
20
+ )
17
21
  from ._mcp_audit import record_scan_and_attach
18
22
  from ._safe_write import write_artifact
19
23
  from ._scan_history import (
@@ -24,7 +28,6 @@ from ._user_config import mark_repo_seen
24
28
  from ._work_order import SELECTABLE, combined_delta, select
25
29
  from .baseline import (
26
30
  finding_fingerprints,
27
- findings_not_in_baseline,
28
31
  load_baseline,
29
32
  write_baseline,
30
33
  )
@@ -36,7 +39,7 @@ from .config import (
36
39
  load_config,
37
40
  repository_path,
38
41
  )
39
- from .git_tools import added_lines, changed_paths
42
+ from .git_tools import changed_paths
40
43
  from .instructions import (
41
44
  INSTRUCTION_TARGETS,
42
45
  UnknownTarget,
@@ -65,6 +68,14 @@ def _add_gate_arguments(parser: argparse.ArgumentParser) -> None:
65
68
  "else'; without this the bound is an instruction nothing checks. "
66
69
  "Reports only unless --fail-on-out-of-scope is passed.",
67
70
  )
71
+ parser.add_argument(
72
+ "--fail-on-regression", action="store_true",
73
+ help="Exit 1 when any scoring category is lower than the previous "
74
+ "comparable scan. Reads the recorded history, and refuses to "
75
+ "compare across an instrument change rather than reporting a "
76
+ "false regression: two scans taken under different calibration "
77
+ "cannot be subtracted.",
78
+ )
68
79
  parser.add_argument(
69
80
  "--fail-on-out-of-scope", action="store_true",
70
81
  help="With --conformance, exit 1 when the diff touched files the work "
@@ -226,48 +237,6 @@ def write_outputs(args: argparse.Namespace, report: dict, rendered: str) -> None
226
237
  json.dumps(report_to_sarif(report), indent=2) + "\n", json_artifact=True)
227
238
 
228
239
 
229
- def attach_conformance(args: argparse.Namespace, report: dict) -> None:
230
- """Record how the diff relates to the work order, when asked.
231
-
232
- Assembly, not scoring: the record is attached to the report and reaches
233
- no dimension, because whether a diff was obedient is a fact about an
234
- agent's behaviour and not evidence about the code's condition.
235
- """
236
- if not args.conformance:
237
- return
238
- from ._conformance import scope_conformance
239
-
240
- root = Path(report["root"])
241
- changed = changed_paths(root, args.conformance)
242
- added = added_lines(root, args.conformance)
243
- report["scope_conformance"] = scope_conformance(
244
- report, changed, args.conformance, added
245
- )
246
-
247
-
248
- def audit_exit_code(args: argparse.Namespace, report: dict) -> int:
249
- if args.fail_on_new:
250
- # Structured matching, never a label-set difference: a label
251
- # changes under `git mv` and same-name reorder, and failing a
252
- # build over either is a false report about the change.
253
- root = Path(report["root"])
254
- if findings_not_in_baseline(report, args.baseline, root):
255
- return 1
256
- if args.fail_on_gate and report["hard_gate_failures"]:
257
- return 1
258
- if args.fail_on_out_of_scope:
259
- record = report.get("scope_conformance")
260
- if record is None:
261
- print("--fail-on-out-of-scope needs --conformance REVSPEC", file=sys.stderr)
262
- return 2
263
- # `clean`, not `conformant`: staying in scope while silencing the
264
- # finding is the evasion the pairing exists to catch, and a gate
265
- # that accepted it would be satisfied by the thing it guards against.
266
- if not record["clean"]:
267
- return 1
268
- return 0
269
-
270
-
271
240
  def _analyzers_resolved(args: argparse.Namespace, config: dict) -> bool:
272
241
  """Whether the pool runs: flags beat config, config beats nothing.
273
242
 
@@ -325,7 +294,6 @@ def _install_skill_action(args: argparse.Namespace) -> int:
325
294
  return 0
326
295
 
327
296
 
328
-
329
297
  def _target_paths(pairs: list[str] | None) -> dict[str, str]:
330
298
  """`TARGET=PATH` arguments, as a mapping.
331
299
 
@@ -429,7 +397,7 @@ def main(argv: list[str] | None = None) -> int:
429
397
  record=bool(record), want_targets=bool(args.prompt_output))
430
398
  # Before rendering, so the record is in the report every presentation
431
399
  # and every consumer reads, rather than only in the exit code.
432
- attach_conformance(args, report)
400
+ _attach_post_audit_records(args, report, history_path)
433
401
  rendered = _render_presentation(args, report, history_path)
434
402
  write_outputs(args, report, rendered)
435
403
  mark_repo_seen(root) # completed audit: not a first run now, whatever the gate says (D13)
@@ -11,7 +11,7 @@ from ._config_defaults import DEFAULT_CONFIG, DEFAULT_IDIOM_GROUPS
11
11
  # still the door every caller comes through.
12
12
  __all__ = ["DEFAULT_CONFIG", "DEFAULT_IDIOM_GROUPS", "PROJECT_URL", "VERSION"]
13
13
 
14
- VERSION = "2.1.0"
14
+ VERSION = "2.2.0"
15
15
 
16
16
  PROJECT_URL = "https://github.com/marshallguillory86/maintainability-agent"
17
17
 
@@ -0,0 +1,155 @@
1
+ """A change that improves one dimension while regressing another.
2
+
3
+ `--fail-on-new` catches a finding that clears and returns. Nothing caught
4
+ the slower failure: modularity up, testability quietly down, both scans
5
+ green by every existing gate, and the thing that got worse never named.
6
+
7
+ The judgment that matters here is not the subtraction. It is **when
8
+ subtracting is allowed at all**. Two scans taken with different
9
+ instruments — a different calibration constant, a different analyzer
10
+ population — cannot be compared, and `_scan_history.segments` already owns
11
+ that rule for the trend charts. A ratchet that ignored it would have
12
+ reported every repository on earth as catastrophically regressed on the day
13
+ 2.0.0 extended the corpus and moved `CALIBRATION_C` from 5.8843 to 8.7161,
14
+ and the ratchet would have been the thing that was wrong.
15
+
16
+ So "not comparable" is a third outcome, and it is not a pass.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from maintainability_audit._ratchet import TOLERANCE, dimension_ratchet
22
+ from maintainability_audit._scan_history import ScanRecord
23
+
24
+
25
+ def _record(
26
+ commit: str,
27
+ categories: dict[str, float],
28
+ *,
29
+ calibration: float = 8.7161,
30
+ aspects: dict[str, float] | None = None,
31
+ ) -> ScanRecord:
32
+ return ScanRecord(
33
+ recorded_at=f"2026-09-0{commit[-1]}T00:00:00Z",
34
+ commit=commit,
35
+ branch="main",
36
+ scope="repository",
37
+ rubric_version="2",
38
+ calibration=calibration,
39
+ thresholds_digest="same",
40
+ analyzers=("lizard", "jscpd"),
41
+ scored_languages=("python",),
42
+ estimate=4.0,
43
+ categories=categories,
44
+ aspects=aspects or {},
45
+ )
46
+
47
+
48
+ def test_a_dimension_that_slipped_is_named() -> None:
49
+ """The failure the ratchet exists for: one up, one down, both green."""
50
+ records = [
51
+ _record("aaaaaa1", {"modularity": 4.0, "testability": 4.2}),
52
+ _record("aaaaaa2", {"modularity": 4.4, "testability": 3.6}),
53
+ ]
54
+
55
+ result = dimension_ratchet(records)
56
+
57
+ assert result["comparable"] is True
58
+ assert result["held"] is False
59
+ assert [item["name"] for item in result["regressed"]] == ["testability"]
60
+ assert result["regressed"][0]["drop"] == 0.6
61
+ assert result["regressed"][0]["was"] == 4.2
62
+
63
+
64
+ def test_an_improvement_is_not_a_regression() -> None:
65
+ records = [
66
+ _record("aaaaaa1", {"modularity": 4.0}),
67
+ _record("aaaaaa2", {"modularity": 4.5}),
68
+ ]
69
+
70
+ assert dimension_ratchet(records)["held"] is True
71
+
72
+
73
+ def test_movement_inside_the_rounding_boundary_is_not_a_slide() -> None:
74
+ """Scores are reported to one decimal; noise below that is not news."""
75
+ records = [
76
+ _record("aaaaaa1", {"modularity": 4.00}),
77
+ _record("aaaaaa2", {"modularity": 4.00 - TOLERANCE / 2}),
78
+ ]
79
+
80
+ assert dimension_ratchet(records)["held"] is True
81
+
82
+
83
+ def test_scans_across_an_instrument_change_are_not_comparable() -> None:
84
+ """The case that would have made this ratchet a liability.
85
+
86
+ 2.0.0 moved `CALIBRATION_C` 5.8843 -> 8.7161. Every score moved with it,
87
+ and subtracting across that boundary would report a catastrophic
88
+ regression everywhere while nothing about the code had changed.
89
+ """
90
+ records = [
91
+ _record("aaaaaa1", {"modularity": 4.6}, calibration=5.8843),
92
+ _record("aaaaaa2", {"modularity": 3.1}, calibration=8.7161),
93
+ ]
94
+
95
+ result = dimension_ratchet(records)
96
+
97
+ assert result["comparable"] is False, (
98
+ "scores from two different calibrations were subtracted"
99
+ )
100
+ assert result["regressed"] == []
101
+ assert "instrument" in result["reason"] or "different" in result["reason"]
102
+
103
+
104
+ def test_not_comparable_is_not_a_pass() -> None:
105
+ """A caller must be able to tell "nothing regressed" from "nobody asked".
106
+
107
+ Both have an empty `regressed` list, so the distinction has to live
108
+ somewhere a gate can read, and `held` must not be true where the
109
+ question was never asked.
110
+ """
111
+ single = dimension_ratchet([_record("aaaaaa1", {"modularity": 4.0})])
112
+
113
+ assert single["comparable"] is False
114
+ assert single["regressed"] == []
115
+ assert single.get("held") is not True, (
116
+ "a scan with nothing to compare against reported that it held"
117
+ )
118
+
119
+
120
+ def test_an_aspect_that_became_unmeasurable_is_not_a_drop() -> None:
121
+ """Withheld evidence must never read as a failure.
122
+
123
+ P3: withholding evidence cannot improve the grade — and the inversion
124
+ matters just as much. An aspect that stops being measurable is reported
125
+ as Unknown by the evidence model, not as a zero, so scoring its absence
126
+ as a fall to nothing would turn missing evidence into a regression.
127
+ """
128
+ records = [
129
+ _record("aaaaaa1", {"modularity": 4.0}, aspects={"documentation": 4.5}),
130
+ _record("aaaaaa2", {"modularity": 4.0}, aspects={}),
131
+ ]
132
+
133
+ result = dimension_ratchet(records)
134
+
135
+ assert result["aspects"] == []
136
+ assert result["held"] is True
137
+
138
+
139
+ def test_aspects_are_reported_but_do_not_decide_the_verdict() -> None:
140
+ """An aspect can wobble while its category holds.
141
+
142
+ Failing on every aspect movement makes the gate noise, and a noisy gate
143
+ gets switched off — but hiding what moved underneath makes the verdict
144
+ unactionable, so aspects are reported beside the decision.
145
+ """
146
+ records = [
147
+ _record("aaaaaa1", {"modularity": 4.0}, aspects={"file_size": 4.4}),
148
+ _record("aaaaaa2", {"modularity": 4.0}, aspects={"file_size": 3.2}),
149
+ ]
150
+
151
+ result = dimension_ratchet(records)
152
+
153
+ assert [item["name"] for item in result["aspects"]] == ["file_size"]
154
+ assert result["regressed"] == []
155
+ assert result["held"] is True
@@ -288,7 +288,11 @@ def test_fail_on_new_uses_structured_matching_not_a_label_set_difference() -> No
288
288
  offenders.append(f"{path.name}:{node.lineno}")
289
289
  assert not offenders, f"label-set finding comparison remains at {offenders}"
290
290
 
291
- cli_tree = ast.parse((package / "cli.py").read_text(encoding="utf-8"))
291
+ # `audit_exit_code` moved to `_gates` in 2.2.0, when `cli.py` crossed
292
+ # the 500-line file gate. The guard follows the function rather than the
293
+ # filename: what it pins is that the gate matches findings structurally,
294
+ # wherever that gate lives.
295
+ cli_tree = ast.parse((package / "_gates.py").read_text(encoding="utf-8"))
292
296
  audit_exit = next(
293
297
  node for node in cli_tree.body
294
298
  if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
@@ -220,10 +220,11 @@ def test_the_gate_reads_clean_not_conformant(tmp_path: Path) -> None:
220
220
  `audit_exit_code`, because the decision this test is about lives in the
221
221
  gate and not in the record.
222
222
  """
223
- from maintainability_audit.cli import audit_exit_code
223
+ from maintainability_audit._gates import audit_exit_code
224
224
 
225
225
  args = SimpleNamespace(
226
226
  fail_on_new=False, fail_on_gate=False, fail_on_out_of_scope=True,
227
+ fail_on_regression=False,
227
228
  baseline=None, conformance="main...HEAD",
228
229
  )
229
230
  silenced = {"hard_gate_failures": [], "scope_conformance": {
@@ -244,10 +245,11 @@ def test_asking_to_fail_without_asking_to_check_is_refused() -> None:
244
245
  which is the "absence read as a pass" shape this project treats as a
245
246
  defect class rather than an inconvenience.
246
247
  """
247
- from maintainability_audit.cli import audit_exit_code
248
+ from maintainability_audit._gates import audit_exit_code
248
249
 
249
250
  args = SimpleNamespace(
250
251
  fail_on_new=False, fail_on_gate=False, fail_on_out_of_scope=True,
252
+ fail_on_regression=False,
251
253
  baseline=None, conformance=None,
252
254
  )
253
255
 
@@ -255,7 +257,7 @@ def test_asking_to_fail_without_asking_to_check_is_refused() -> None:
255
257
 
256
258
 
257
259
  def test_attach_conformance_does_nothing_unless_asked() -> None:
258
- from maintainability_audit.cli import attach_conformance
260
+ from maintainability_audit._gates import attach_conformance
259
261
 
260
262
  report = {"root": ".", "work_order": []}
261
263
  attach_conformance(SimpleNamespace(conformance=None), report)
@@ -273,7 +275,7 @@ def test_attach_conformance_records_against_a_real_diff(tmp_path: Path) -> None:
273
275
  """
274
276
  import subprocess
275
277
 
276
- from maintainability_audit.cli import attach_conformance
278
+ from maintainability_audit._gates import attach_conformance
277
279
 
278
280
  def run(*args: str) -> None:
279
281
  subprocess.run(args, cwd=tmp_path, check=True, capture_output=True, timeout=120) # noqa: S603