maintainability-agent 3.0.2__tar.gz → 3.1.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 (363) hide show
  1. {maintainability_agent-3.0.2/src/maintainability_agent.egg-info → maintainability_agent-3.1.0}/PKG-INFO +2 -2
  2. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/README.md +1 -1
  3. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/pyproject.toml +1 -1
  4. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0/src/maintainability_agent.egg-info}/PKG-INFO +2 -2
  5. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_agent.egg-info/SOURCES.txt +5 -0
  6. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/__init__.py +1 -1
  7. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_anchor.py +27 -6
  8. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_assets/standard.md +39 -18
  9. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_banner.py +23 -3
  10. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_discovery.py +24 -2
  11. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_environment.py +54 -2
  12. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_in_loop.py +47 -1
  13. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_practice.py +25 -1
  14. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_semantic_ts.py +14 -4
  15. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_setup_persist.py +47 -3
  16. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_test_commands.py +23 -2
  17. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_test_execution.py +23 -2
  18. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/config.py +53 -2
  19. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/metrics.py +21 -1
  20. maintainability_agent-3.1.0/tests/test_check_refuses_plus_only_fragments.py +38 -0
  21. maintainability_agent-3.1.0/tests/test_tree_cannot_opt_the_host_in.py +111 -0
  22. maintainability_agent-3.1.0/tests/test_tree_reads_use_the_primitive.py +167 -0
  23. maintainability_agent-3.1.0/tests/test_unanchored_set_matches_corpus.py +124 -0
  24. maintainability_agent-3.1.0/tests/test_work_order_binds_to_this_interpreter.py +72 -0
  25. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/LICENSE +0 -0
  26. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/setup.cfg +0 -0
  27. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_agent.egg-info/dependency_links.txt +0 -0
  28. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_agent.egg-info/entry_points.txt +0 -0
  29. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_agent.egg-info/requires.txt +0 -0
  30. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_agent.egg-info/top_level.txt +0 -0
  31. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/__main__.py +0 -0
  32. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_adapters.py +0 -0
  33. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_analysis.py +0 -0
  34. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_analyzer_sections.py +0 -0
  35. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_arguments.py +0 -0
  36. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_aspects.py +0 -0
  37. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_assets/analyzer-catalog.json +0 -0
  38. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_attestation.py +0 -0
  39. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_backfill.py +0 -0
  40. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_bands.py +0 -0
  41. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_built_ins.py +0 -0
  42. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_calibration.py +0 -0
  43. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_catalog.py +0 -0
  44. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_charts.py +0 -0
  45. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_cognitive.py +0 -0
  46. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_config_defaults.py +0 -0
  47. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_conformance.py +0 -0
  48. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_corroborate.py +0 -0
  49. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_coverage_notes.py +0 -0
  50. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_criteria_scope.py +0 -0
  51. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_derive.py +0 -0
  52. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_documents.py +0 -0
  53. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_economics.py +0 -0
  54. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_economics_view.py +0 -0
  55. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_evidence_view.py +0 -0
  56. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_finding_match.py +0 -0
  57. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_first_run.py +0 -0
  58. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_formula.py +0 -0
  59. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_gates.py +0 -0
  60. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_generic.py +0 -0
  61. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_grant_ledger.py +0 -0
  62. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_history_view.py +0 -0
  63. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_hostile_prompt.py +0 -0
  64. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_hotspots.py +0 -0
  65. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_html_report_sections.py +0 -0
  66. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_html_view.py +0 -0
  67. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_identity.py +0 -0
  68. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_in_loop_view.py +0 -0
  69. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_jvm_adapters.py +0 -0
  70. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_markdown_sections.py +0 -0
  71. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_masking.py +0 -0
  72. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_mcp_audit.py +0 -0
  73. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_mcp_gate.py +0 -0
  74. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_mcp_grants.py +0 -0
  75. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_mcp_refusals.py +0 -0
  76. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_mcp_resources.py +0 -0
  77. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_mcp_setup.py +0 -0
  78. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_metric_adapters.py +0 -0
  79. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_metrics_types.py +0 -0
  80. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_operator_reads.py +0 -0
  81. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_pillars.py +0 -0
  82. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_precommit.py +0 -0
  83. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_precommit_install.py +0 -0
  84. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_precommit_view.py +0 -0
  85. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_pressures.py +0 -0
  86. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_c.py +0 -0
  87. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_cobol.py +0 -0
  88. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_core.py +0 -0
  89. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_cpp.py +0 -0
  90. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_csharp.py +0 -0
  91. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_fortran.py +0 -0
  92. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_go.py +0 -0
  93. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_java.py +0 -0
  94. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_js.py +0 -0
  95. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_php.py +0 -0
  96. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_ruby.py +0 -0
  97. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_rust.py +0 -0
  98. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ranges_swift.py +0 -0
  99. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_ratchet.py +0 -0
  100. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_recurrence.py +0 -0
  101. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_run_comparison.py +0 -0
  102. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_runner.py +0 -0
  103. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_safe_write.py +0 -0
  104. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_scan_history.py +0 -0
  105. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_scan_view.py +0 -0
  106. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_second_source.py +0 -0
  107. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_selection.py +0 -0
  108. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_semantic.py +0 -0
  109. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_semantic_policy.py +0 -0
  110. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_semantic_view.py +0 -0
  111. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_setup_errors.py +0 -0
  112. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_skill_data/SKILL.md +0 -0
  113. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_skill_data/agents/anthropic.yaml +0 -0
  114. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_skill_data/agents/copilot.yaml +0 -0
  115. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_skill_data/agents/openai.yaml +0 -0
  116. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_skill_data/copilot/maintainability-agent.prompt.md +0 -0
  117. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_skill_data/references/finding-taxonomy.md +0 -0
  118. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_skill_install.py +0 -0
  119. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_stored_grants.py +0 -0
  120. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_tdd_view.py +0 -0
  121. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_test_pairing.py +0 -0
  122. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_tokens.py +0 -0
  123. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_tool_adapters.py +0 -0
  124. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_trends.py +0 -0
  125. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_user_config.py +0 -0
  126. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_verdict_adapters.py +0 -0
  127. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_verification.py +0 -0
  128. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_work_order.py +0 -0
  129. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_work_order_view.py +0 -0
  130. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_work_order_weights.py +0 -0
  131. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/_xml.py +0 -0
  132. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/baseline.py +0 -0
  133. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/cli.py +0 -0
  134. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/deadcode.py +0 -0
  135. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/declarations.py +0 -0
  136. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/duplication.py +0 -0
  137. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/evidence.py +0 -0
  138. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/git_tools.py +0 -0
  139. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/history.py +0 -0
  140. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/idioms.py +0 -0
  141. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/instructions.py +0 -0
  142. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/mcp_server.py +0 -0
  143. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/prompts.py +0 -0
  144. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/renderers.py +0 -0
  145. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/report.py +0 -0
  146. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/sarif.py +0 -0
  147. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/scoring.py +0 -0
  148. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/similarity.py +0 -0
  149. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/src/maintainability_audit/source.py +0 -0
  150. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_acquisition_trust.py +0 -0
  151. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_adapter_docs.py +0 -0
  152. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_adapter_recordings.py +0 -0
  153. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_adapters.py +0 -0
  154. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_added_lines.py +0 -0
  155. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analysis_coverage.py +0 -0
  156. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analysis_execution.py +0 -0
  157. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_bridge.py +0 -0
  158. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_config_isolation.py +0 -0
  159. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_estimate_claims.py +0 -0
  160. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_pinning.py +0 -0
  161. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_primary_estimate.py +0 -0
  162. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_provenance.py +0 -0
  163. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_provenance_exclusions.py +0 -0
  164. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_scoring_path.py +0 -0
  165. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_spread_range.py +0 -0
  166. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_analyzer_xml_bounds.py +0 -0
  167. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_anchor_and_mode.py +0 -0
  168. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_anchor_disclosure.py +0 -0
  169. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_annotations_match_behaviour.py +0 -0
  170. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_anticipated_refusals.py +0 -0
  171. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_architecture.py +0 -0
  172. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_artifact_write_route_class.py +0 -0
  173. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_aspect_coverage.py +0 -0
  174. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_asset_snapshot_class.py +0 -0
  175. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_ast_reading_resolves_spellings.py +0 -0
  176. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_attestation.py +0 -0
  177. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_audit_components.py +0 -0
  178. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_authorization_freshness.py +0 -0
  179. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_authorship_gates.py +0 -0
  180. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_backfill.py +0 -0
  181. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_band_concealment_class.py +0 -0
  182. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_band_concealment_p3.py +0 -0
  183. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_band_pressures.py +0 -0
  184. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_bands.py +0 -0
  185. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_body_or_signature_class.py +0 -0
  186. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_c_declarations.py +0 -0
  187. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_calibration_corpus.py +0 -0
  188. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_calibration_drift.py +0 -0
  189. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_calibration_tooling.py +0 -0
  190. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_catalog_selection.py +0 -0
  191. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_catalog_tier_promise.py +0 -0
  192. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_chat_primary_docs.py +0 -0
  193. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_checkstyle_adapter.py +0 -0
  194. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_checkstyle_catalog.py +0 -0
  195. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_ci_history_recipe.py +0 -0
  196. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_ci_installs_the_analyzer_pool.py +0 -0
  197. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_claimed_languages.py +0 -0
  198. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_cli.py +0 -0
  199. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_clone_group_class.py +0 -0
  200. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_cobol_declarations.py +0 -0
  201. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_cognitive_complexity.py +0 -0
  202. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_cohort_statistics.py +0 -0
  203. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_concealment_properties.py +0 -0
  204. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_config_shape.py +0 -0
  205. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_consent_and_grant.py +0 -0
  206. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_consumer_migration.py +0 -0
  207. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_contract_claims.py +0 -0
  208. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_corroborate.py +0 -0
  209. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_coverage_by_language.py +0 -0
  210. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_cpp_declarations.py +0 -0
  211. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_criteria_completion.py +0 -0
  212. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_csharp_declarations.py +0 -0
  213. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_d15_composition.py +0 -0
  214. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_d15_goal_directed.py +0 -0
  215. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_dead_code.py +0 -0
  216. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_declaration_criteria_scope.py +0 -0
  217. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_declaration_edge_cases.py +0 -0
  218. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_declaration_grading.py +0 -0
  219. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_declaration_ranges.py +0 -0
  220. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_declaration_source.py +0 -0
  221. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_declared_authorship.py +0 -0
  222. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_declared_imports.py +0 -0
  223. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_derivation_mix.py +0 -0
  224. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_determinism.py +0 -0
  225. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_dimension_ratchet.py +0 -0
  226. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_direct_runner_availability_class.py +0 -0
  227. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_discovery.py +0 -0
  228. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_doc_claims.py +0 -0
  229. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_docs_links.py +0 -0
  230. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_economic_context.py +0 -0
  231. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_empty_output_not_clean.py +0 -0
  232. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_empty_output_not_clean_adapters.py +0 -0
  233. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_environment_work_order.py +0 -0
  234. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_evidence_invariants.py +0 -0
  235. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_evidence_normalization.py +0 -0
  236. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_evidence_properties.py +0 -0
  237. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_exclusion_dialects.py +0 -0
  238. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_falsifier_standard.py +0 -0
  239. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_file_list_cap_class.py +0 -0
  240. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_finding_identity.py +0 -0
  241. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_first_run_elicitation.py +0 -0
  242. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_first_run_prompt.py +0 -0
  243. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_first_run_reelicitation.py +0 -0
  244. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_fix_breadth_window.py +0 -0
  245. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_fixed_form_fortran.py +0 -0
  246. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_format_ask.py +0 -0
  247. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_fortran_declarations.py +0 -0
  248. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_fortran_metrics.py +0 -0
  249. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_fortran_practice.py +0 -0
  250. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_git_argv.py +0 -0
  251. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_git_read_only.py +0 -0
  252. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_git_readonly_class.py +0 -0
  253. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_go_declarations.py +0 -0
  254. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_grammar_constructs.py +0 -0
  255. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_grammar_token_coverage.py +0 -0
  256. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_grant_only_user_tier.py +0 -0
  257. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_history.py +0 -0
  258. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_history_boundary.py +0 -0
  259. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_history_manifest.py +0 -0
  260. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_history_schema2.py +0 -0
  261. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_history_window.py +0 -0
  262. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_hostile_prompt.py +0 -0
  263. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_html_executive.py +0 -0
  264. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_html_section_parity_class.py +0 -0
  265. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_identity_docs.py +0 -0
  266. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_identity_resolution.py +0 -0
  267. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_idiom_divergence.py +0 -0
  268. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_in_loop_budgets.py +0 -0
  269. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_in_loop_check.py +0 -0
  270. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_installed_wheel_runtime.py +0 -0
  271. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_instruction_targets.py +0 -0
  272. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_java_range_guard.py +0 -0
  273. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_java_ranges.py +0 -0
  274. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_java_scored.py +0 -0
  275. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_java_wiring.py +0 -0
  276. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_js_complexity_operators.py +0 -0
  277. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_js_declarations.py +0 -0
  278. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_language_coverage.py +0 -0
  279. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_language_support_honesty.py +0 -0
  280. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_mcp_baseline_payload.py +0 -0
  281. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_mcp_history.py +0 -0
  282. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_mcp_server.py +0 -0
  283. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_migration_1_0.py +0 -0
  284. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_near_duplicates.py +0 -0
  285. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_network_disclosure.py +0 -0
  286. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_one_setup_across_surfaces_class.py +0 -0
  287. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_operator_named_paths.py +0 -0
  288. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_operator_named_reads.py +0 -0
  289. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_pattern_budget.py +0 -0
  290. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_phase6_claims.py +0 -0
  291. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_php_declarations.py +0 -0
  292. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_pillars_and_practice.py +0 -0
  293. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_platform_claim.py +0 -0
  294. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_pmd_adapter.py +0 -0
  295. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_pmd_audit_closures.py +0 -0
  296. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_policy_claims.py +0 -0
  297. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_pool_runs_by_config.py +0 -0
  298. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_population_floors.py +0 -0
  299. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_practice_nested_gates_class.py +0 -0
  300. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_precommit_staged.py +0 -0
  301. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_presentation_refusals.py +0 -0
  302. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_promises.py +0 -0
  303. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_prompt_escalation_once.py +0 -0
  304. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_prompt_honesty.py +0 -0
  305. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_prompt_location_escalation_class.py +0 -0
  306. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_prove_falsifiers.py +0 -0
  307. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_python_complexity.py +0 -0
  308. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_python_declarations.py +0 -0
  309. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_read_boundary.py +0 -0
  310. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_readme_claims.py +0 -0
  311. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_recurrence.py +0 -0
  312. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_release_plan.py +0 -0
  313. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_release_plan_status.py +0 -0
  314. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_report_remedy_copy.py +0 -0
  315. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_roles_recorded.py +0 -0
  316. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_root_grants.py +0 -0
  317. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_rubric_ignores_project_lint_config.py +0 -0
  318. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_ruby_declarations.py +0 -0
  319. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_run_comparison.py +0 -0
  320. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_runner.py +0 -0
  321. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_rust_declarations.py +0 -0
  322. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_sarif.py +0 -0
  323. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_scan_history.py +0 -0
  324. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_scan_scope.py +0 -0
  325. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_scanner_unsafe_direction.py +0 -0
  326. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_scanning.py +0 -0
  327. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_schema_fidelity.py +0 -0
  328. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_scope_conformance.py +0 -0
  329. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_scoring_calibration.py +0 -0
  330. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_semantic_policy.py +0 -0
  331. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_setup_gate_completeness.py +0 -0
  332. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_setup_gate_independent_contract.py +0 -0
  333. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_setup_precondition.py +0 -0
  334. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_skill_install.py +0 -0
  335. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_sonar_resolve.py +0 -0
  336. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_source_index.py +0 -0
  337. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_spotbugs_adapter.py +0 -0
  338. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_spotbugs_catalog.py +0 -0
  339. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_stage8_contract.py +0 -0
  340. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_suite_isolation.py +0 -0
  341. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_swift_declarations.py +0 -0
  342. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_tdd_structure.py +0 -0
  343. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_test_commands.py +0 -0
  344. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_test_execution_class.py +0 -0
  345. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_three_presentations.py +0 -0
  346. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_tool_resolution.py +0 -0
  347. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_tree_chosen_spawn.py +0 -0
  348. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_trends.py +0 -0
  349. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_type_fields_not_functions_class.py +0 -0
  350. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_uat_falsifiers.py +0 -0
  351. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_unread_code.py +0 -0
  352. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_user_config_tier.py +0 -0
  353. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_verification_audit_closures.py +0 -0
  354. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_verified_grade.py +0 -0
  355. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_version_claim.py +0 -0
  356. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_wheel_contents.py +0 -0
  357. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_work_order.py +0 -0
  358. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_work_order_items.py +0 -0
  359. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_work_order_selection.py +0 -0
  360. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_workflow_supply_chain.py +0 -0
  361. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_write_boundary.py +0 -0
  362. {maintainability_agent-3.0.2 → maintainability_agent-3.1.0}/tests/test_write_route_class.py +0 -0
  363. {maintainability_agent-3.0.2 → maintainability_agent-3.1.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: 3.0.2
3
+ Version: 3.1.0
4
4
  Summary: Maintainability gate and AI remediation prompt generator for CI.
5
5
  Author: Marshall Guillory
6
6
  License: MIT
@@ -45,7 +45,7 @@ Dynamic: license-file
45
45
  **A deterministic, offline maintainability audit whose output is a _bounded
46
46
  work order_ for an AI coding agent** — a copy-paste prompt, per finding, that
47
47
  says *fix exactly these and refactor nothing else*. Chat-primary; CLI for CI.
48
- Version **3.0.2**.
48
+ Version **3.1.0**.
49
49
 
50
50
  **Planned, not shipped:** the audit tool will become `maintainability-audit`;
51
51
  a new `maintainability-agent` companion will run it and automate human-selected
@@ -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 **3.0.2**.
8
+ Version **3.1.0**.
9
9
 
10
10
  **Planned, not shipped:** the audit tool will become `maintainability-audit`;
11
11
  a new `maintainability-agent` companion will run it and automate human-selected
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maintainability-agent"
7
- version = "3.0.2"
7
+ version = "3.1.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: 3.0.2
3
+ Version: 3.1.0
4
4
  Summary: Maintainability gate and AI remediation prompt generator for CI.
5
5
  Author: Marshall Guillory
6
6
  License: MIT
@@ -45,7 +45,7 @@ Dynamic: license-file
45
45
  **A deterministic, offline maintainability audit whose output is a _bounded
46
46
  work order_ for an AI coding agent** — a copy-paste prompt, per finding, that
47
47
  says *fix exactly these and refactor nothing else*. Chat-primary; CLI for CI.
48
- Version **3.0.2**.
48
+ Version **3.1.0**.
49
49
 
50
50
  **Planned, not shipped:** the audit tool will become `maintainability-audit`;
51
51
  a new `maintainability-agent` companion will run it and automate human-selected
@@ -183,6 +183,7 @@ tests/test_calibration_tooling.py
183
183
  tests/test_catalog_selection.py
184
184
  tests/test_catalog_tier_promise.py
185
185
  tests/test_chat_primary_docs.py
186
+ tests/test_check_refuses_plus_only_fragments.py
186
187
  tests/test_checkstyle_adapter.py
187
188
  tests/test_checkstyle_catalog.py
188
189
  tests/test_ci_history_recipe.py
@@ -337,10 +338,13 @@ tests/test_test_commands.py
337
338
  tests/test_test_execution_class.py
338
339
  tests/test_three_presentations.py
339
340
  tests/test_tool_resolution.py
341
+ tests/test_tree_cannot_opt_the_host_in.py
340
342
  tests/test_tree_chosen_spawn.py
343
+ tests/test_tree_reads_use_the_primitive.py
341
344
  tests/test_trends.py
342
345
  tests/test_type_fields_not_functions_class.py
343
346
  tests/test_uat_falsifiers.py
347
+ tests/test_unanchored_set_matches_corpus.py
344
348
  tests/test_unread_code.py
345
349
  tests/test_user_config_tier.py
346
350
  tests/test_verification_audit_closures.py
@@ -348,6 +352,7 @@ tests/test_verified_grade.py
348
352
  tests/test_version_claim.py
349
353
  tests/test_wheel_contents.py
350
354
  tests/test_work_order.py
355
+ tests/test_work_order_binds_to_this_interpreter.py
351
356
  tests/test_work_order_items.py
352
357
  tests/test_work_order_selection.py
353
358
  tests/test_workflow_supply_chain.py
@@ -1,3 +1,3 @@
1
1
  """Maintainability Audit CI."""
2
2
 
3
- __version__ = "3.0.2"
3
+ __version__ = "3.1.0"
@@ -25,12 +25,33 @@ from __future__ import annotations
25
25
 
26
26
  #: Parsed by a scanner, absent from the reference corpus.
27
27
  #:
28
- #: Stated rather than derived. `tools/calibration/corpus.json` is a
29
- #: repository file and does not ship in the wheel, so a runtime read would
30
- #: be unavailable exactly where a user reads a grade.
31
- #: `test_anchor_disclosure` recomputes `parsed - corpus` from both sources
32
- #: and fails if this stops matching, so the list cannot quietly go stale.
33
- UNANCHORED_LANGUAGES: tuple[str, ...] = ("Swift", "COBOL", "Go", "Rust", "PHP", "Ruby")
28
+ #: Stated rather than derived, and the reason still holds:
29
+ #: `tools/calibration/corpus.json` is a repository file that does not ship
30
+ #: in the wheel, so a runtime read would be unavailable exactly where a
31
+ #: user reads a grade. What a stated list buys in availability it owes in
32
+ #: upkeep, and it did not pay.
33
+ #:
34
+ #: **D143.** This read `("Swift", "COBOL", "Go", "Rust", "PHP", "Ruby")`
35
+ #: after 3.0.0 anchored five of those six. The corpus grew from 112
36
+ #: repositories to 180 and from eight measured languages to thirteen, and
37
+ #: this line did not move — so every skin told a Swift, Go, Rust, PHP or
38
+ #: Ruby reader their grade was provisional against a corpus that had
39
+ #: measured their language for two releases. A caveat that names an
40
+ #: anchored language is not a caution, it is a false statement about the
41
+ #: evidence, and it undersells the thing the recalibration was for.
42
+ #:
43
+ #: COBOL stays, and stays alone. It is parsed and deliberately never
44
+ #: anchored: GitHub holds one COBOL repository above 500 stars and what is
45
+ #: there is curriculum, Java parsers and JavaScript wrappers rather than
46
+ #: COBOL codebases, so the corpus policy excludes it permanently rather
47
+ #: than temporarily. Emptying this tuple would delete a disclosure that is
48
+ #: still true.
49
+ #:
50
+ #: `tests/test_unanchored_set_matches_corpus.py` recomputes
51
+ #: `parsed - corpus` from `DECLARATION_SUFFIXES` and `corpus.json` and
52
+ #: fails if this stops matching — including when a language is *added* to
53
+ #: the corpus, which is the direction that went unnoticed.
54
+ UNANCHORED_LANGUAGES: tuple[str, ...] = ("COBOL",)
34
55
 
35
56
 
36
57
  def unanchored_names() -> str:
@@ -118,10 +118,20 @@ The corpus median lands at **4.0 (B)**: a well-run real codebase earns a B, and
118
118
 
119
119
  ### The reference corpus
120
120
 
121
- Calibration is reproducible, not a snapshot someone took once. The corpus is defined in [`tools/calibration/corpus.json`](../tools/calibration/corpus.json) — 40 mature open-source repositories **pinned to exact commits**, spanning 32 to 18,789 source files and 463,581 declarations across Python, TypeScript and JavaScript:
121
+ Calibration is reproducible, not a snapshot someone took once. The corpus is defined in [`tools/calibration/corpus.json`](../tools/calibration/corpus.json) — **180 mature open-source repositories pinned to exact commits**, spanning 24 to 18,789 source files and 2,749,575 declarations across thirteen languages: Python, TypeScript, JavaScript, Java, C, C++, C#, Fortran, Swift, Go, Rust, PHP and Ruby.
122
+
123
+ The forty repositories 1.x calibrated on are all still in it, at the commits they were measured at then:
122
124
 
123
125
  > angular · ansible · ant-design · anime · axios · Chart.js · code-server · django · echarts · excalidraw · express · fastapi · flask · freeCodeCamp · github-readme-stats · hackingtool · hoppscotch · jquery · keras · localstack · lodash · manim · material-ui · mermaid · models · n8n · nest · playwright · reveal.js · scrapy · strapi · svelte · tabby · tailwindcss · transformers · uBlock · vite · webpack · youtube-dl · yt-dlp
124
126
 
127
+ **This paragraph opened "40 mature open-source repositories … across
128
+ Python, TypeScript and JavaScript" for two releases after 3.0.0 measured
129
+ 180 across thirteen (D143).** The corrected numbers were four paragraphs
130
+ below it the whole time, so a reader met both and had nothing to tell
131
+ them which was current. Same defect as the unanchored caveat below and
132
+ found in the same pass: a count written into prose, with nothing that
133
+ fails when the thing it counts moves.
134
+
125
135
  **What the anchor does not cover.** For five releases this section named a
126
136
  real defect: the corpus was 40 repositories of Python, TypeScript and
127
137
  JavaScript while the scanner parsed eight languages, so Java, C, C++, C#
@@ -131,23 +141,34 @@ median and fortran-lang/stdlib at **1.10x** — the first a true statement
131
141
  about LAPACK relative to mature OSS *web* code, and not a statement about
132
142
  typical Fortran, because no typical Fortran was in the comparison set.
133
143
 
134
- **2.0.0 extended the corpus rather than the caveat.** It is now 112
135
- repositories spanning eight languages: Python, TypeScript, JavaScript,
136
- Java, C, C++, C# and Fortran. The original 40 rows are unchanged and still
137
- pinned to the commits they were measured at, so what moved the constants
138
- is the languages added and nothing else.
139
-
140
- **Six parsed languages are not in it.** Swift (2.4.0), COBOL (2.7.0)
141
- and Go, Rust, PHP and Ruby (2.11.0) shipped after that measurement and
142
- are **unanchored**: a scanner reads them, and the medians their rates are
143
- compared against were measured on other languages. This is the corpus policy decided with 2.4.1 — a
144
- language ships parsed, and the corpus is re-measured once after the
145
- remaining scanners land, because a new scanner moves
146
- `scanner_fingerprint` and invalidates every stored row. The cost is
147
- disclosed rather than absorbed: findings in those languages are as good
148
- as their parser, their grade is provisional, and every surface that
149
- prints a grade says so. This sentence read "every language this scanner
150
- parses" for three releases after it stopped being true.
144
+ **3.0.0 extended the corpus.** It is now **180 repositories spanning
145
+ thirteen languages**: Python, TypeScript, JavaScript, Java, C, C++, C#,
146
+ Fortran, Swift, Go, Rust, PHP and Ruby. The 112 rows measured for 2.0.0
147
+ are unchanged and still pinned to the commits they were measured at, so
148
+ what moved the constants is the languages added and nothing else.
149
+
150
+ **One parsed language is not in it: COBOL.** It is **unanchored** — a
151
+ scanner reads it, and the medians its rates are compared against were
152
+ measured on other languages. The cost is disclosed rather than absorbed:
153
+ findings in COBOL are as good as their parser, a COBOL grade is
154
+ provisional, and every surface that prints a grade says so.
155
+
156
+ COBOL is unanchored **permanently**, not pending a re-measure. Against
157
+ the corpus criteria GitHub holds one COBOL repository above 500 stars,
158
+ and what is public is curriculum, Java parsers and JavaScript wrappers
159
+ rather than COBOL codebases; the language's real corpus is on private
160
+ mainframes. Anchoring it on what exists would build a reference out of
161
+ teaching material — an anchor that *looks* like one, which is worse than
162
+ the disclosed absence.
163
+
164
+ Two corrections are recorded here rather than quietly applied, because
165
+ both were live for multiple releases. This sentence read "every language
166
+ this scanner parses" for three releases after that stopped being true.
167
+ It then named **six** unanchored languages for two releases after 3.0.0
168
+ anchored five of them (D143) — Swift, Go, Rust, PHP and Ruby readers were
169
+ told their grade was provisional against a corpus that had measured their
170
+ language. A caveat naming an anchored language is not a caution; it is a
171
+ false statement about the evidence.
151
172
 
152
173
  Three limits remain, and they are narrower than the one they replace:
153
174
 
@@ -17,9 +17,12 @@ one-way.
17
17
  """
18
18
  from __future__ import annotations
19
19
 
20
+ import os
20
21
  import re
21
22
  from pathlib import Path
22
23
 
24
+ from ._operator_reads import open_regular_file
25
+
23
26
  # Whatever a language uses to open a comment, plus decoration a generator
24
27
  # may pad the line with: `#`, `//`, `*`, `/*`, `!`, `--`, `<!--`, `;`, and
25
28
  # the emoji rules some tools draw. `@` is spared, or `# @generated` would
@@ -40,12 +43,29 @@ BANNER_LINES = 20
40
43
  def banner_says_generated(
41
44
  path: Path, banners: tuple[str, ...]
42
45
  ) -> str | None:
43
- """The banner text, if the head of this file carries one."""
46
+ """The banner text, if the head of this file carries one.
47
+
48
+ The head only, so this stays cheap on a large tree — but read
49
+ *through* a checked handle rather than by name, so a FIFO refuses
50
+ instead of stopping the audit dead (D145). `path.open()` on one waits
51
+ for a writer that never comes, and the previous `except OSError`
52
+ could not have caught that: there is no error to catch, only a
53
+ process that never returns.
54
+ """
44
55
  try:
45
- with path.open(encoding="utf-8", errors="replace") as handle:
46
- head = "".join(next(handle, "") for _ in range(BANNER_LINES)).lower()
56
+ handle = open_regular_file(
57
+ path,
58
+ "Banner detection reads the head of a discovered file; a "
59
+ "device, socket or FIFO would block the audit rather than "
60
+ "declare itself generated.",
61
+ )
47
62
  except OSError:
48
63
  return None
64
+ try:
65
+ with os.fdopen(handle, "r", encoding="utf-8", errors="replace", closefd=False) as opened:
66
+ head = "".join(next(opened, "") for _ in range(BANNER_LINES)).lower()
67
+ finally:
68
+ os.close(handle)
49
69
  previous: str | None = None
50
70
  for raw in head.splitlines():
51
71
  stripped = COMMENT_LEAD.sub("", raw).strip()
@@ -42,6 +42,7 @@ from typing import Any
42
42
 
43
43
  from ._banner import banner_says_generated
44
44
  from ._metrics_types import KNOWN_SOURCE_SUFFIXES, is_test_path
45
+ from ._operator_reads import PathNotAllowed, read_source_file
45
46
  from .metrics import is_excluded
46
47
 
47
48
  # The inventory names languages the way a person does; the analyzer
@@ -207,6 +208,19 @@ class Inventory:
207
208
  return tally
208
209
 
209
210
 
211
+ def _text_of(path: Path) -> str:
212
+ """A repository file's text, read through the regular-file primitive.
213
+
214
+ Discovery reads whatever the tree names — `package.json` anywhere
215
+ beneath the root, `.gitmodules` at it — so every path here is
216
+ repository-controlled and none of it was going through the door
217
+ D131 and D141 built (D145). `read_text` on a FIFO waits for a writer
218
+ that never comes, and no `except OSError` catches a process that
219
+ simply does not return.
220
+ """
221
+ return "\n".join(read_source_file(path))
222
+
223
+
210
224
  def _generated_directories(root: Path, excludes: tuple[str, ...]) -> dict[str, str]:
211
225
  """Directories a `package.json` script deletes and rebuilds."""
212
226
  found: dict[str, str] = {}
@@ -215,7 +229,15 @@ def _generated_directories(root: Path, excludes: tuple[str, ...]) -> dict[str, s
215
229
  if is_excluded(relative, list(excludes)):
216
230
  continue
217
231
  try:
218
- scripts = json.loads(manifest.read_text(encoding="utf-8")).get("scripts", {})
232
+ scripts = json.loads(_text_of(manifest)).get("scripts", {})
233
+ except PathNotAllowed:
234
+ # `PathNotAllowed` is a `ValueError`, so it would otherwise be
235
+ # swallowed by the clause below and the manifest would read as
236
+ # a malformed one rather than a refused one (D145). A FIFO
237
+ # named `package.json` is not bad JSON; it is a path this tool
238
+ # will not read, and saying so is the whole point of routing
239
+ # the read through the primitive.
240
+ raise
219
241
  except (OSError, ValueError, AttributeError):
220
242
  continue
221
243
  if not isinstance(scripts, dict):
@@ -241,7 +263,7 @@ def _vendored_directories(root: Path) -> dict[str, str]:
241
263
  modules = root / ".gitmodules"
242
264
  if modules.exists():
243
265
  try:
244
- text = modules.read_text(encoding="utf-8", errors="replace")
266
+ text = _text_of(modules)
245
267
  except OSError:
246
268
  text = ""
247
269
  for entry in re.finditer(r"^\s*path\s*=\s*(.+?)\s*$", text, re.MULTILINE):
@@ -13,6 +13,8 @@ holds that structurally.
13
13
  """
14
14
  from __future__ import annotations
15
15
 
16
+ import shlex
17
+ import sys
16
18
  from typing import Any
17
19
 
18
20
  # Outcomes a user can act on. `timed-out` and `failed` are excluded on
@@ -53,6 +55,57 @@ _VERIFY: dict[str, str] = {
53
55
  }
54
56
 
55
57
 
58
+ def _remedy_for(slug: str) -> dict[str, str]:
59
+ """The install and verify commands, bound to the interpreter that audits.
60
+
61
+ A remedy is only a remedy if it addresses the same environment the
62
+ next audit reads. `pip install lizard` and `lizard --version` name
63
+ neither: they address whatever `pip` and `lizard` `PATH` happens to
64
+ resolve, which on any machine with more than one Python is a
65
+ different interpreter than the one that just reported the tool
66
+ missing (D144).
67
+
68
+ That is not hypothetical here. D142 was the same gap seen from the
69
+ other side — the agent could not find tools the operator had already
70
+ installed, because `pip` had put them where `PATH` did not look. A
71
+ work order that hands back a bare `pip` sends the operator around
72
+ that loop again, and the second time it looks like the remedy simply
73
+ does not work.
74
+
75
+ So pip-backed adapters emit `{sys.executable} -m pip ...`. The dict
76
+ entries stay spelled as distribution names — `pip install
77
+ fortitude-lint` — because two tests derive the CI install list from
78
+ them, and rewriting the dict would move a fact that belongs at the
79
+ emit site into the data.
80
+
81
+ Verification moves from `--version` to `pip show` for these, and the
82
+ reason is worth stating because the old comment argued the opposite:
83
+ `--version` was chosen to run the same probe the runner runs. Since
84
+ D142 the runner does not probe `PATH` at all — it searches this
85
+ interpreter's script directories — so a bare `{slug} --version`
86
+ verifies a *different* thing than the audit does. `pip show` asks the
87
+ question that actually decides the next run: is this distribution
88
+ installed for this interpreter.
89
+
90
+ Node and JVM overrides are untouched. `npx` and `brew` are not
91
+ interpreter-scoped and rewriting them through `sys.executable` would
92
+ name a Python package that does not exist.
93
+ """
94
+ install = _INSTALL.get(slug, f"pip install {slug}")
95
+ if not install.startswith("pip install"):
96
+ return {"install": install, "verify": _VERIFY.get(slug, f"{slug} --version")}
97
+ distribution = install.split()[-1]
98
+ return {
99
+ "install": _pip_command("install", distribution),
100
+ "verify": _pip_command("show", distribution),
101
+ }
102
+
103
+
104
+ def _pip_command(action: str, distribution: str) -> str:
105
+ """A pip invocation that cannot resolve to another interpreter."""
106
+ return f"{shlex.quote(sys.executable)} -m pip {action} {distribution}"
107
+
108
+
56
109
  def environment_work_order(coverage: list[Any]) -> list[dict[str, str]]:
57
110
  """One item per selected analyzer that could not contribute.
58
111
 
@@ -82,8 +135,7 @@ def environment_work_order(coverage: list[Any]) -> list[dict[str, str]]:
82
135
  items.append({
83
136
  "tool": entry.slug,
84
137
  "reason": entry.detail or f"outcome: {entry.outcome}",
85
- "install": _INSTALL.get(entry.slug, f"pip install {entry.slug}"),
86
- "verify": _VERIFY.get(entry.slug, f"{entry.slug} --version"),
138
+ **_remedy_for(entry.slug),
87
139
  # What installing the tool restores, so a host can explain
88
140
  # the trade instead of relaying a bare command (D9).
89
141
  "concepts": ", ".join(entry.concepts),
@@ -299,6 +299,52 @@ def _is_unified_diff(text: str) -> bool:
299
299
  return has_hunk or (has_old and has_new)
300
300
 
301
301
 
302
+ #: The two marks a unified diff puts in column one on a changed line.
303
+ #: Context lines carry a space, which is why a fragment of *context*
304
+ #: still parses: it is the file's own text with one column of padding,
305
+ #: and refusing it would refuse the ordinary case of a pasted excerpt.
306
+ _CHANGE_MARKS = ("+", "-")
307
+
308
+
309
+ def _is_diff_fragment(text: str) -> bool:
310
+ """Whether this is the changed lines of a diff with the frame cut off.
311
+
312
+ D138 loosened `_is_unified_diff` so a hunk-only paste is refused,
313
+ because a chat window shows the hunk and not the file headers. This
314
+ is the paste one step smaller again, and the commonest of all: an
315
+ agent copies the *green* lines out of a review pane and pipes them
316
+ to `--check`. There is no `@@` and there are no `---`/`+++`
317
+ headers, so every shape check above passes it through, the brace
318
+ scanners find nothing in `+function hello() {`, and
319
+ `declarations_read` reports true (D146).
320
+
321
+ That is the same absence-read-as-a-pass D133 and D138 each closed
322
+ one fragment shape to the left, and the third time it arrived the
323
+ detector was still being widened by example rather than by rule. So
324
+ the rule is stated here: a body whose every non-blank line begins
325
+ with a change mark is a diff fragment, whatever suffix it was
326
+ named. Nothing else in this project's fourteen languages is written
327
+ that way, and a file that genuinely is would still be measured as
328
+ zero declarations rather than silently reported as read.
329
+
330
+ **The mention-versus-assertion guard survives, and by construction
331
+ rather than by luck.** Somebody writing *about* a diff writes prose
332
+ or code around the quoted line — `text = '+function hello()'` — and
333
+ that surrounding line does not begin with a mark, so the body is not
334
+ all-marked and parses normally. The guard is the same one
335
+ `_HUNK_HEADER`'s anchored `match` gives the hunk check: a mark has
336
+ to hold column one of *every* line to count, and a mention never
337
+ does.
338
+
339
+ Blank lines are ignored rather than counted against the fragment,
340
+ because a copied hunk keeps its blank lines unmarked.
341
+ """
342
+ marked = [line for line in text.splitlines() if line.strip()]
343
+ if not marked:
344
+ return False
345
+ return all(line[0] in _CHANGE_MARKS for line in marked)
346
+
347
+
302
348
  def _parses(path: str, text: str) -> bool:
303
349
  """Whether the content is what its extension claims to be.
304
350
 
@@ -309,7 +355,7 @@ def _parses(path: str, text: str) -> bool:
309
355
  is absence read as a pass, arriving through the exact feature whose
310
356
  docstring promises to refuse it (Gemini's field check).
311
357
  """
312
- if _is_unified_diff(text):
358
+ if _is_unified_diff(text) or _is_diff_fragment(text):
313
359
  return False
314
360
  if Path(path).suffix != ".py":
315
361
  # No parser for the brace languages, and zero declarations is not
@@ -31,11 +31,13 @@ engineering practice made from the outside.
31
31
 
32
32
  from __future__ import annotations
33
33
 
34
+ import os
34
35
  import re
35
36
  from dataclasses import dataclass, field
36
37
  from pathlib import Path
37
38
  from typing import Any
38
39
 
40
+ from ._operator_reads import open_regular_file
39
41
  from .metrics import is_excluded
40
42
 
41
43
  # A repository holding config but running none of it. See the module
@@ -139,10 +141,32 @@ class Practice:
139
141
 
140
142
 
141
143
  def _read(path: Path, limit: int = 200_000) -> str:
144
+ """A bounded read of a repository file, through a checked handle.
145
+
146
+ The head only, so this stays cheap on a large tree — but read
147
+ *through* a checked handle rather than by name, so a FIFO refuses
148
+ instead of stopping the audit dead (D145). `path.open()` on one waits
149
+ for a writer that never comes, and the previous `except OSError`
150
+ could not have caught that: there is no error to catch, only a
151
+ process that never returns.
152
+
153
+ Practice detection reads whatever the tree names as CI and toolchain
154
+ configuration, so every path here is repository-controlled.
155
+ """
142
156
  try:
143
- return path.read_text(encoding="utf-8", errors="replace")[:limit]
157
+ handle = open_regular_file(
158
+ path,
159
+ "Practice detection reads CI and toolchain files from the "
160
+ "audited tree; a device, socket or FIFO would block the audit "
161
+ "rather than describe a practice.",
162
+ )
144
163
  except OSError:
145
164
  return ""
165
+ try:
166
+ with os.fdopen(handle, "r", encoding="utf-8", errors="replace", closefd=False) as opened:
167
+ return opened.read(limit)
168
+ finally:
169
+ os.close(handle)
146
170
 
147
171
 
148
172
  def _ci_files(root: Path) -> list[Path]:
@@ -20,6 +20,7 @@ import re
20
20
  from pathlib import Path
21
21
  from typing import Any
22
22
 
23
+ from ._operator_reads import read_source_file
23
24
  from ._runner import Invocation, locate, run
24
25
  from .metrics import within
25
26
 
@@ -49,12 +50,21 @@ _QUOTED = re.compile(r"\"([A-Za-z_][\w-]*)\"")
49
50
 
50
51
 
51
52
  def recorded_type_analysis(root: Path) -> dict[str, Any] | None:
52
- """The checked-in recording, when the repository keeps one."""
53
+ """The checked-in recording, when the repository keeps one.
54
+
55
+ `is_file()` used to stand in for both questions — does it exist, and
56
+ is it readable — and answered a FIFO with "absent" (D145). A
57
+ repository could then name `RECORDED_ANALYSIS` as a pipe and have
58
+ the tool report no recording rather than refuse one, which is the
59
+ same skip-and-pretend-absent shape D141 closed in the scan walk.
60
+ Existence is asked here; kind is asked by the primitive, which
61
+ refuses rather than waiting for a writer.
62
+ """
53
63
  path = root / RECORDED_ANALYSIS
54
- if not path.is_file():
64
+ if not path.exists():
55
65
  return None
56
66
  try:
57
- payload = json.loads(path.read_text(encoding="utf-8"))
67
+ payload = json.loads("\n".join(read_source_file(path)))
58
68
  except ValueError:
59
69
  return None
60
70
  return payload if isinstance(payload, dict) and payload.get("tool") else None
@@ -226,7 +236,7 @@ def operation_sets(root: Path) -> list[dict[str, Any]]:
226
236
  """
227
237
  found: list[dict[str, Any]] = []
228
238
  for path in _typescript_sources(root):
229
- text = path.read_text(encoding="utf-8", errors="replace")
239
+ text = "\n".join(read_source_file(path))
230
240
  dispatch = frozenset(
231
241
  name for pair in _DISPATCH.findall(text) for name in pair if name
232
242
  )
@@ -12,7 +12,7 @@ from typing import Any
12
12
 
13
13
  from ._safe_write import write_bounded
14
14
  from ._setup_errors import SetupRequired
15
- from ._user_config import write_user_answers
15
+ from ._user_config import user_config_answers, write_user_answers
16
16
  from .config import CONFIG_FILENAME, discovered_config, load_config
17
17
  from .config import _configured as _read_config
18
18
 
@@ -75,6 +75,33 @@ def _is_bounds_only(answers: dict[str, Any]) -> bool:
75
75
  return bool(given) and given <= names
76
76
 
77
77
 
78
+ def _user_tier_with(**changes: Any) -> dict[str, Any]:
79
+ """The user's stored answers plus these, and nothing from the tree.
80
+
81
+ Both staged writers used to hand `write_user_answers` the
82
+ **repository's** document, because that was the dict they had built
83
+ for the repository write and it looked like "the config". So a
84
+ two-stage setup — answer the rates, or the test command — copied
85
+ whatever the audited tree had written under `analyzers.acquire_tools`,
86
+ `test_execution.requested` and `expected_commands.test` straight into
87
+ the user tier, which is the one tier `acquisition_permitted` and
88
+ `opted_in_command` trust (D147).
89
+
90
+ Four lines in a pull request, then any staged reply, and the tree had
91
+ granted itself `npx --yes` and a command to run. Neither check was
92
+ wrong: the tier they read had been told the tree's answers were the
93
+ user's.
94
+
95
+ So the user tier is now built **from the user tier**, the way
96
+ `_persist_answers` builds it from the setup payload. Merged rather
97
+ than replaced, for the same reason `_persist_answers` merges: a
98
+ stage-two reply must not reset the answers stage one wrote (D13).
99
+ """
100
+ stored = dict(user_config_answers() or {})
101
+ stored.update(changes)
102
+ return stored
103
+
104
+
78
105
  def _apply_bounds(root: Path, answers: dict[str, Any]) -> dict[str, Any]:
79
106
  """Fill the rates into a configuration that already has its answers.
80
107
 
@@ -91,7 +118,7 @@ def _apply_bounds(root: Path, answers: dict[str, Any]) -> dict[str, Any]:
91
118
  Path(root), config_path,
92
119
  json.dumps(stored, indent=2, sort_keys=True) + "\n",
93
120
  )
94
- write_user_answers(stored)
121
+ write_user_answers(_user_tier_with(economic_context=economics))
95
122
  return load_config(str(config_path))
96
123
 
97
124
 
@@ -114,14 +141,31 @@ def _apply_command(root: Path, answers: dict[str, Any]) -> dict[str, Any]:
114
141
  commands = dict(stored.get("expected_commands") or {})
115
142
  commands["test"] = shlex.split(command)
116
143
  stored["expected_commands"] = commands
144
+ # The user tier carries the command as well as the request,
145
+ # because `opted_in_command` reads both from there: a person
146
+ # consents to the command they were shown, and leaving the
147
+ # command to the repository would keep the consent and hand back
148
+ # the choice of what it means (D147).
149
+ user_tier = _user_tier_with(
150
+ expected_commands={
151
+ **(user_config_answers() or {}).get("expected_commands", {}),
152
+ "test": shlex.split(command),
153
+ },
154
+ )
117
155
  else:
118
156
  stored["test_execution"] = {"requested": False}
157
+ user_tier = _user_tier_with(
158
+ test_execution={
159
+ **(user_config_answers() or {}).get("test_execution", {}),
160
+ "requested": False,
161
+ },
162
+ )
119
163
  config_path = Path(root) / CONFIG_FILENAME
120
164
  write_bounded(
121
165
  Path(root), config_path,
122
166
  json.dumps(stored, indent=2, sort_keys=True) + "\n",
123
167
  )
124
- write_user_answers(stored)
168
+ write_user_answers(user_tier)
125
169
  return load_config(str(config_path))
126
170
 
127
171