code-towel 1.414__tar.gz → 1.618__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 (416) hide show
  1. {code_towel-1.414 → code_towel-1.618}/CHANGELOG.md +26 -0
  2. {code_towel-1.414 → code_towel-1.618}/CONTRIBUTING.md +1 -1
  3. {code_towel-1.414/src/code_towel.egg-info → code_towel-1.618}/PKG-INFO +131 -15
  4. {code_towel-1.414 → code_towel-1.618}/README.md +130 -14
  5. code_towel-1.618/SECURITY.md +19 -0
  6. {code_towel-1.414 → code_towel-1.618}/docs/ARCHITECTURE.md +23 -13
  7. {code_towel-1.414 → code_towel-1.618}/docs/KNOWN_LIMITATIONS.md +11 -5
  8. code_towel-1.618/docs/QUICKSTART.md +66 -0
  9. {code_towel-1.414 → code_towel-1.618}/docs/RELEASING.md +5 -5
  10. {code_towel-1.414 → code_towel-1.618}/docs/USAGE_GUIDE.md +34 -27
  11. code_towel-1.618/docs/proposals/reuse-existing-function.md +117 -0
  12. {code_towel-1.414 → code_towel-1.618}/pyproject.toml +1 -4
  13. {code_towel-1.414 → code_towel-1.618}/scripts/add_copyright_headers.py +1 -1
  14. {code_towel-1.414 → code_towel-1.618}/scripts/ecosystem_check.py +41 -7
  15. {code_towel-1.414 → code_towel-1.618}/scripts/promote_dry_helpers.py +1 -1
  16. {code_towel-1.414 → code_towel-1.618/src/code_towel.egg-info}/PKG-INFO +131 -15
  17. {code_towel-1.414 → code_towel-1.618}/src/code_towel.egg-info/SOURCES.txt +7 -18
  18. code_towel-1.618/src/code_towel.egg-info/entry_points.txt +2 -0
  19. {code_towel-1.414 → code_towel-1.618}/src/towel/__init__.py +1 -1
  20. {code_towel-1.414 → code_towel-1.618}/src/towel/changes.py +8 -7
  21. {code_towel-1.414 → code_towel-1.618}/src/towel/cli.py +140 -73
  22. {code_towel-1.414 → code_towel-1.618}/src/towel/renaming.py +16 -7
  23. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/__init__.py +1 -1
  24. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/assignment_analyzer.py +3 -21
  25. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/binding_detector.py +4 -8
  26. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/block_signature.py +1 -1
  27. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/builtins.py +1 -1
  28. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/definite_assignment.py +10 -25
  29. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/exceptions.py +1 -1
  30. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/extractor.py +24 -20
  31. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/instantiation.py +10 -0
  32. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/models.py +9 -5
  33. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/nominal_unifier.py +1 -1
  34. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/orphan_detector.py +1 -1
  35. code_towel-1.618/src/towel/unification/parameters.py +41 -0
  36. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/pipeline.py +9 -59
  37. code_towel-1.618/src/towel/unification/progress.py +43 -0
  38. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/project_layout.py +100 -12
  39. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/refactor_engine.py +709 -602
  40. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/scope_analyzer.py +8 -26
  41. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/semantic_safety.py +76 -12
  42. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/unifier.py +19 -24
  43. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/visitors.py +1 -1
  44. {code_towel-1.414 → code_towel-1.618}/test_examples/README.md +49 -19
  45. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/README.md +7 -14
  46. {code_towel-1.414 → code_towel-1.618}/tests/OBSERVATIONAL_EQUIVALENCE.md +37 -80
  47. code_towel-1.618/tests/README.md +84 -0
  48. {code_towel-1.414 → code_towel-1.618}/tests/automatic_equivalence_tester.py +1 -4
  49. {code_towel-1.414 → code_towel-1.618}/tests/generate_baseline.py +1 -1
  50. {code_towel-1.414 → code_towel-1.618}/tests/test_adversarial_renaming.py +28 -0
  51. {code_towel-1.414 → code_towel-1.618}/tests/test_adversarial_semantics.py +6 -1
  52. {code_towel-1.414 → code_towel-1.618}/tests/test_ancestor_insertion.py +1 -1
  53. {code_towel-1.414 → code_towel-1.618}/tests/test_assignment_analyzer_comprehensive.py +1 -2
  54. {code_towel-1.414 → code_towel-1.618}/tests/test_backend_layouts.py +60 -2
  55. {code_towel-1.414 → code_towel-1.618}/tests/test_binding_detector.py +1 -2
  56. {code_towel-1.414 → code_towel-1.618}/tests/test_binding_detector_edge_cases.py +1 -1
  57. {code_towel-1.414 → code_towel-1.618}/tests/test_bindings.py +7 -2
  58. {code_towel-1.414 → code_towel-1.618}/tests/test_breakers.py +1 -1
  59. {code_towel-1.414 → code_towel-1.618}/tests/test_builtins_comprehensive.py +1 -1
  60. {code_towel-1.414 → code_towel-1.618}/tests/test_candidate_index.py +1 -13
  61. {code_towel-1.414 → code_towel-1.618}/tests/test_cli_integration.py +28 -20
  62. {code_towel-1.414 → code_towel-1.618}/tests/test_comprehensive_coverage.py +1 -1
  63. {code_towel-1.414 → code_towel-1.618}/tests/test_crossfile_observational_equivalence.py +1 -1
  64. {code_towel-1.414 → code_towel-1.618}/tests/test_engine_adversarial.py +3 -3
  65. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_additional_branches.py +2 -2
  66. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_comprehensive.py +55 -19
  67. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_edge_cases.py +15 -16
  68. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_return_statements.py +2 -2
  69. {code_towel-1.414 → code_towel-1.618}/tests/test_final_coverage_push.py +0 -1
  70. {code_towel-1.414 → code_towel-1.618}/tests/test_free_variable_correspondence.py +2 -2
  71. {code_towel-1.414 → code_towel-1.618}/tests/test_helper_inventory.py +44 -0
  72. {code_towel-1.414 → code_towel-1.618}/tests/test_helpers.py +0 -1
  73. {code_towel-1.414 → code_towel-1.618}/tests/test_nominal_unifier.py +1 -1
  74. {code_towel-1.414 → code_towel-1.618}/tests/test_observational_equivalence.py +2 -4
  75. {code_towel-1.414 → code_towel-1.618}/tests/test_orphan_detector_comprehensive.py +1 -1
  76. code_towel-1.618/tests/test_out_of_place_cycle_regression.py +117 -0
  77. code_towel-1.618/tests/test_parameters.py +29 -0
  78. code_towel-1.618/tests/test_project_layout_and_imports.py +263 -0
  79. {code_towel-1.414 → code_towel-1.618}/tests/test_project_layout_behavior.py +1 -1
  80. {code_towel-1.414 → code_towel-1.618}/tests/test_promotion_and_mangling.py +36 -0
  81. {code_towel-1.414 → code_towel-1.618}/tests/test_refactor_engine_comprehensive.py +8 -7
  82. {code_towel-1.414 → code_towel-1.618}/tests/test_refactor_engine_edge_cases.py +1 -1
  83. {code_towel-1.414 → code_towel-1.618}/tests/test_refactor_engine_more.py +0 -1
  84. {code_towel-1.414 → code_towel-1.618}/tests/test_refactor_engine_targeted_branches.py +5 -9
  85. {code_towel-1.414 → code_towel-1.618}/tests/test_refactoring_engine.py +0 -1
  86. {code_towel-1.414 → code_towel-1.618}/tests/test_regression.py +1 -2
  87. {code_towel-1.414 → code_towel-1.618}/tests/test_scope_analyzer_comprehensive.py +1 -1
  88. {code_towel-1.414 → code_towel-1.618}/tests/test_semantic_safety_regressions.py +51 -2
  89. {code_towel-1.414 → code_towel-1.618}/tests/test_signature_prefilter.py +1 -1
  90. code_towel-1.618/tests/test_trivial_helper_filter.py +55 -0
  91. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_additional_branches.py +1 -1
  92. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_bound_variables.py +1 -1
  93. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_comprehensive.py +1 -1
  94. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_edge_cases.py +1 -1
  95. {code_towel-1.414 → code_towel-1.618}/tests/test_validation_partial_lifetime.py +2 -2
  96. {code_towel-1.414 → code_towel-1.618}/tests/test_variable_capture_bug.py +2 -2
  97. {code_towel-1.414 → code_towel-1.618}/uv.lock +1 -1
  98. code_towel-1.414/SECURITY.md +0 -13
  99. code_towel-1.414/docs/BUG_HANDOFF_2025-12-03.md +0 -278
  100. code_towel-1.414/docs/CROSS_FILE_REFACTORING.md +0 -233
  101. code_towel-1.414/docs/JUSTFILE_REFERENCE.md +0 -361
  102. code_towel-1.414/docs/README_DIRECTORY_USAGE.md +0 -265
  103. code_towel-1.414/docs/TESTING_AUDIT.md +0 -281
  104. code_towel-1.414/docs/TROUBLESHOOTING.md +0 -284
  105. code_towel-1.414/docs/UNIFICATION_IMPLEMENTATION.md +0 -257
  106. code_towel-1.414/scripts/dry +0 -7
  107. code_towel-1.414/scripts/preview +0 -162
  108. code_towel-1.414/src/code_towel.egg-info/entry_points.txt +0 -4
  109. code_towel-1.414/src/towel/unification/ast_normalizer.py +0 -237
  110. code_towel-1.414/src/towel/unification/ast_pretty_printer.py +0 -133
  111. code_towel-1.414/src/towel/unification/visitor_utils.py +0 -83
  112. code_towel-1.414/tests/README.md +0 -87
  113. code_towel-1.414/tests/test_ast_normalizer_comprehensive.py +0 -644
  114. code_towel-1.414/tests/test_ast_pretty_printer.py +0 -77
  115. code_towel-1.414/tests/test_ast_pretty_printer_comprehensive.py +0 -550
  116. code_towel-1.414/tests/test_ast_pretty_printer_more.py +0 -26
  117. code_towel-1.414/tests/test_legacy_normalizers.py +0 -69
  118. code_towel-1.414/tests/test_project_layout_and_imports.py +0 -145
  119. code_towel-1.414/tests/test_visitor_utils_comprehensive.py +0 -363
  120. {code_towel-1.414 → code_towel-1.618}/.flake8 +0 -0
  121. {code_towel-1.414 → code_towel-1.618}/.pre-commit-config.yaml +0 -0
  122. {code_towel-1.414 → code_towel-1.618}/.templates/README.md +0 -0
  123. {code_towel-1.414 → code_towel-1.618}/.templates/binding_constructs_comprehensive.py +0 -0
  124. {code_towel-1.414 → code_towel-1.618}/.templates/bindings_comprehensions.py +0 -0
  125. {code_towel-1.414 → code_towel-1.618}/.templates/bindings_for_loops.py +0 -0
  126. {code_towel-1.414 → code_towel-1.618}/.templates/closure_adversarial.py +0 -0
  127. {code_towel-1.414 → code_towel-1.618}/.templates/complex_expressions.py +0 -0
  128. {code_towel-1.414 → code_towel-1.618}/.templates/control_flow_adversarial.py +0 -0
  129. {code_towel-1.414 → code_towel-1.618}/.templates/edge_cases_stress_test.py +0 -0
  130. {code_towel-1.414 → code_towel-1.618}/.templates/example1_simple.py +0 -0
  131. {code_towel-1.414 → code_towel-1.618}/.templates/example2_classes.py +0 -0
  132. {code_towel-1.414 → code_towel-1.618}/.templates/example3_file1.py +0 -0
  133. {code_towel-1.414 → code_towel-1.618}/.templates/example3_file2.py +0 -0
  134. {code_towel-1.414 → code_towel-1.618}/.templates/example4_complex.py +0 -0
  135. {code_towel-1.414 → code_towel-1.618}/.templates/exception_adversarial.py +0 -0
  136. {code_towel-1.414 → code_towel-1.618}/.templates/fstrings_constants.py +0 -0
  137. {code_towel-1.414 → code_towel-1.618}/.templates/functional_patterns.py +0 -0
  138. {code_towel-1.414 → code_towel-1.618}/.templates/global_nonlocal_examples.py +0 -0
  139. {code_towel-1.414 → code_towel-1.618}/.templates/hygienic_naming.py +0 -0
  140. {code_towel-1.414 → code_towel-1.618}/.templates/method_chains.py +0 -0
  141. {code_towel-1.414 → code_towel-1.618}/.templates/nested_structures.py +0 -0
  142. {code_towel-1.414 → code_towel-1.618}/.templates/real_world_patterns.py +0 -0
  143. {code_towel-1.414 → code_towel-1.618}/.templates/referential_transparency.py +0 -0
  144. {code_towel-1.414 → code_towel-1.618}/.templates/return_values.py +0 -0
  145. {code_towel-1.414 → code_towel-1.618}/.templates/scoping_edge_cases.py +0 -0
  146. {code_towel-1.414 → code_towel-1.618}/.templates/side_effects_adversarial.py +0 -0
  147. {code_towel-1.414 → code_towel-1.618}/.templates/syntactic_coverage_comprehensive.py +0 -0
  148. {code_towel-1.414 → code_towel-1.618}/.templates/tricky_edge_cases_adversarial.py +0 -0
  149. {code_towel-1.414 → code_towel-1.618}/CODE_OF_CONDUCT.md +0 -0
  150. {code_towel-1.414 → code_towel-1.618}/LICENSE +0 -0
  151. {code_towel-1.414 → code_towel-1.618}/MANIFEST.in +0 -0
  152. {code_towel-1.414 → code_towel-1.618}/docs/ADVERSARIAL_REVIEW.md +0 -0
  153. {code_towel-1.414 → code_towel-1.618}/docs/OPEN_SOURCE_AUDIT.md +0 -0
  154. {code_towel-1.414 → code_towel-1.618}/docs/PRODUCTION_READINESS.md +0 -0
  155. {code_towel-1.414 → code_towel-1.618}/docs/RELEASE_LOG.md +0 -0
  156. {code_towel-1.414 → code_towel-1.618}/justfile +0 -0
  157. {code_towel-1.414 → code_towel-1.618}/scripts/bench_refactor.py +0 -0
  158. {code_towel-1.414 → code_towel-1.618}/scripts/debug_pair.py +0 -0
  159. {code_towel-1.414 → code_towel-1.618}/scripts/ecosystem/manifest.toml +0 -0
  160. {code_towel-1.414 → code_towel-1.618}/scripts/report_net_benefit_diff.py +0 -0
  161. {code_towel-1.414 → code_towel-1.618}/scripts/run_crossfile_coverage.py +0 -0
  162. {code_towel-1.414 → code_towel-1.618}/scripts/set_version.py +0 -0
  163. {code_towel-1.414 → code_towel-1.618}/scripts/trace_proposals.py +0 -0
  164. {code_towel-1.414 → code_towel-1.618}/scripts/verify-examples +0 -0
  165. {code_towel-1.414 → code_towel-1.618}/setup.cfg +0 -0
  166. {code_towel-1.414 → code_towel-1.618}/src/code_towel.egg-info/dependency_links.txt +0 -0
  167. {code_towel-1.414 → code_towel-1.618}/src/code_towel.egg-info/requires.txt +0 -0
  168. {code_towel-1.414 → code_towel-1.618}/src/code_towel.egg-info/top_level.txt +0 -0
  169. {code_towel-1.414 → code_towel-1.618}/src/towel/filesystem.py +0 -0
  170. {code_towel-1.414 → code_towel-1.618}/src/towel/py.typed +0 -0
  171. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/structural_memo.py +0 -0
  172. {code_towel-1.414 → code_towel-1.618}/src/towel/unification/thunk_inlining.py +0 -0
  173. {code_towel-1.414 → code_towel-1.618}/test_examples/binding_constructs_comprehensive.py +0 -0
  174. {code_towel-1.414 → code_towel-1.618}/test_examples/bindings_comprehensions.py +0 -0
  175. {code_towel-1.414 → code_towel-1.618}/test_examples/bindings_for_loops.py +0 -0
  176. {code_towel-1.414 → code_towel-1.618}/test_examples/closure_adversarial.py +0 -0
  177. {code_towel-1.414 → code_towel-1.618}/test_examples/complex_expressions.py +0 -0
  178. {code_towel-1.414 → code_towel-1.618}/test_examples/control_flow_adversarial.py +0 -0
  179. {code_towel-1.414 → code_towel-1.618}/test_examples/edge_cases_stress_test.py +0 -0
  180. {code_towel-1.414 → code_towel-1.618}/test_examples/example1_simple.py +0 -0
  181. {code_towel-1.414 → code_towel-1.618}/test_examples/example2_classes.py +0 -0
  182. {code_towel-1.414 → code_towel-1.618}/test_examples/example3_file1.py +0 -0
  183. {code_towel-1.414 → code_towel-1.618}/test_examples/example3_file2.py +0 -0
  184. {code_towel-1.414 → code_towel-1.618}/test_examples/example4_complex.py +0 -0
  185. {code_towel-1.414 → code_towel-1.618}/test_examples/exception_adversarial.py +0 -0
  186. {code_towel-1.414 → code_towel-1.618}/test_examples/fstrings_constants.py +0 -0
  187. {code_towel-1.414 → code_towel-1.618}/test_examples/functional_patterns.py +0 -0
  188. {code_towel-1.414 → code_towel-1.618}/test_examples/global_nonlocal_examples.py +0 -0
  189. {code_towel-1.414 → code_towel-1.618}/test_examples/hygienic_naming.py +0 -0
  190. {code_towel-1.414 → code_towel-1.618}/test_examples/method_chains.py +0 -0
  191. {code_towel-1.414 → code_towel-1.618}/test_examples/nested_structures.py +0 -0
  192. {code_towel-1.414 → code_towel-1.618}/test_examples/real_world_patterns.py +0 -0
  193. {code_towel-1.414 → code_towel-1.618}/test_examples/referential_transparency.py +0 -0
  194. {code_towel-1.414 → code_towel-1.618}/test_examples/return_values.py +0 -0
  195. {code_towel-1.414 → code_towel-1.618}/test_examples/scoping_edge_cases.py +0 -0
  196. {code_towel-1.414 → code_towel-1.618}/test_examples/side_effects_adversarial.py +0 -0
  197. {code_towel-1.414 → code_towel-1.618}/test_examples/syntactic_coverage_comprehensive.py +0 -0
  198. {code_towel-1.414 → code_towel-1.618}/test_examples/tricky_edge_cases_adversarial.py +0 -0
  199. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/multi_level/api/checkout.py +0 -0
  200. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/multi_level/core/services/payment.py +0 -0
  201. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/multi_level/utils/validators.py +0 -0
  202. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/nested_structure/src/data_processor.py +0 -0
  203. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/simple_crossfile/admin_service.py +0 -0
  204. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile/simple_crossfile/user_service.py +0 -0
  205. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/multi_level/api/checkout.py +0 -0
  206. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/multi_level/core/services/payment.py +0 -0
  207. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/multi_level/utils/validators.py +0 -0
  208. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/nested_structure/src/data_processor.py +0 -0
  209. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/simple_crossfile/admin_service.py +0 -0
  210. {code_towel-1.414 → code_towel-1.618}/test_examples_crossfile_expected_output/simple_crossfile/user_service.py +0 -0
  211. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/binding_constructs_comprehensive.py +0 -0
  212. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/bindings_comprehensions.py +0 -0
  213. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/bindings_for_loops.py +0 -0
  214. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/closure_adversarial.py +0 -0
  215. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/complex_expressions.py +0 -0
  216. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/control_flow_adversarial.py +0 -0
  217. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/edge_cases_stress_test.py +0 -0
  218. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example1_simple.py +0 -0
  219. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example2_classes.py +0 -0
  220. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example3_file1.py +0 -0
  221. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example3_file2.py +0 -0
  222. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/example4_complex.py +0 -0
  223. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/exception_adversarial.py +0 -0
  224. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/fstrings_constants.py +0 -0
  225. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/functional_patterns.py +0 -0
  226. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/global_nonlocal_examples.py +0 -0
  227. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/hygienic_naming.py +0 -0
  228. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/method_chains.py +0 -0
  229. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/nested_structures.py +0 -0
  230. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/real_world_patterns.py +0 -0
  231. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/referential_transparency.py +0 -0
  232. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/return_values.py +0 -0
  233. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/scoping_edge_cases.py +0 -0
  234. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/side_effects_adversarial.py +0 -0
  235. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/syntactic_coverage_comprehensive.py +0 -0
  236. {code_towel-1.414 → code_towel-1.618}/test_examples_expected_output/tricky_edge_cases_adversarial.py +0 -0
  237. {code_towel-1.414 → code_towel-1.618}/tests/__init__.py +0 -0
  238. {code_towel-1.414 → code_towel-1.618}/tests/conftest.py +0 -0
  239. {code_towel-1.414 → code_towel-1.618}/tests/crossfile_equivalence_tester.py +0 -0
  240. {code_towel-1.414 → code_towel-1.618}/tests/edge_case_values.py +0 -0
  241. {code_towel-1.414 → code_towel-1.618}/tests/equivalence_targets.py +0 -0
  242. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h01_side_effect_order.py +0 -0
  243. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h02_conditional_eval.py +0 -0
  244. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h03_loop_reeval.py +0 -0
  245. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h04_closure_freevar.py +0 -0
  246. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h05_cond_return_plus_retvar.py +0 -0
  247. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h06_del_in_block.py +0 -0
  248. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h07_except_unbind.py +0 -0
  249. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h08_late_binding_closure.py +0 -0
  250. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h10_short_circuit.py +0 -0
  251. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h13_dunder_file.py +0 -0
  252. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h26_match_capture.py +0 -0
  253. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h38_closure_after_block.py +0 -0
  254. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h39_augassign_after_block.py +0 -0
  255. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h51_semicolons.py +0 -0
  256. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h52_del_after_block.py +0 -0
  257. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h53_nested_func_reads_retvar.py +0 -0
  258. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h54_attr_param_property.py +0 -0
  259. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h55_walrus_used_after.py +0 -0
  260. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h56_global_write.py +0 -0
  261. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h57_exception_in_param.py +0 -0
  262. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h58_nested_def_freevar_reassigned.py +0 -0
  263. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h59_retvar_conditional_unbound.py +0 -0
  264. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h60_finally_return.py +0 -0
  265. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/h61_str_method_param.py +0 -0
  266. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r01_property_thunk_order.py +0 -0
  267. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r02_cluster_attrs.py +0 -0
  268. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r07_attr_two_positions.py +0 -0
  269. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r100_clustered_site_with_live_binding.py +0 -0
  270. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r101_same_named_method_forces_module_helper.py +0 -0
  271. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r11_same_function_pair.py +0 -0
  272. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r14_starred_param.py +0 -0
  273. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r22_fstring_attr.py +0 -0
  274. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r26_attr_store_param.py +0 -0
  275. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r27_del_subscript.py +0 -0
  276. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r32_method_callee.py +0 -0
  277. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r38_name_two_positions.py +0 -0
  278. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r39_cluster_alpha.py +0 -0
  279. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r40_class_hierarchy.py +0 -0
  280. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r41_conditional_call_param.py +0 -0
  281. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r42_loop_call_param.py +0 -0
  282. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r43_opaque_decorator_receiver.py +0 -0
  283. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r44_cluster_nested_overlap.py +0 -0
  284. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r45_async_no_await.py +0 -0
  285. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r46_nonlocal_counter.py +0 -0
  286. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r47_lru_cache_method.py +0 -0
  287. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r50_helper_name_collision.py +0 -0
  288. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r53_try_else_binding.py +0 -0
  289. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r54_with_binding_used_after.py +0 -0
  290. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r55_chained_assign.py +0 -0
  291. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r57_star_unpack.py +0 -0
  292. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r58_escaping_closure.py +0 -0
  293. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r60_local_import.py +0 -0
  294. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r61_same_function_clean.py +0 -0
  295. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r62_recursion.py +0 -0
  296. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r63_except_var_used_in_handler.py +0 -0
  297. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r66_conditional_callee.py +0 -0
  298. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r69_annassign.py +0 -0
  299. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r72_global_promoted.py +0 -0
  300. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r73_slice_vs_index.py +0 -0
  301. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r74_starred_vs_plain.py +0 -0
  302. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r75_prior_helper_param_names.py +0 -0
  303. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r76_return_order.py +0 -0
  304. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r77_mixed_return_and_variables.py +0 -0
  305. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r78_walrus_in_parameter.py +0 -0
  306. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r79_list_display_in_loop.py +0 -0
  307. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r80_inlined_leading_thunk.py +0 -0
  308. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r81_conditionally_bound_free_variable.py +0 -0
  309. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r82_local_classes_same_name.py +0 -0
  310. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r83_tab_indented_class.py +0 -0
  311. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r84_warn_stacklevel.py +0 -0
  312. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r85_conditionally_bound_parameter.py +0 -0
  313. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r86_annotated_assignment_live.py +0 -0
  314. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r87_nested_function_in_method.py +0 -0
  315. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r88_elif_branch.py +0 -0
  316. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r89_conditionally_bound_return.py +0 -0
  317. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r90_cluster_across_classes.py +0 -0
  318. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r91_class_body_helper.py +0 -0
  319. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r92_parameterless_class_body_helper.py +0 -0
  320. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r93_import_name_differs.py +0 -0
  321. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r94_import_binds_live_name.py +0 -0
  322. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r95_type_checking_annotation.py +0 -0
  323. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r96_unpacked_targets_live.py +0 -0
  324. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r97_helper_in_function_with_outside_site.py +0 -0
  325. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r98_same_named_methods_nested_helpers.py +0 -0
  326. {code_towel-1.414 → code_towel-1.618}/tests/hostile_cases/r99_partial_return_branches.py +0 -0
  327. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf3_same_named_local_function/pkg/__init__.py +0 -0
  328. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf3_same_named_local_function/pkg/a.py +0 -0
  329. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf3_same_named_local_function/pkg/b.py +0 -0
  330. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf3_same_named_local_function/run.py +0 -0
  331. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf4_same_named_class/pkg/__init__.py +0 -0
  332. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf4_same_named_class/pkg/a.py +0 -0
  333. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf4_same_named_class/pkg/b.py +0 -0
  334. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf4_same_named_class/run.py +0 -0
  335. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf5_dunder_file/pkg/__init__.py +0 -0
  336. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf5_dunder_file/pkg/a.py +0 -0
  337. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf5_dunder_file/pkg/b.py +0 -0
  338. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf5_dunder_file/run.py +0 -0
  339. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/__init__.py +0 -0
  340. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/a.py +0 -0
  341. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/b.py +0 -0
  342. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/util1.py +0 -0
  343. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/pkg/util2.py +0 -0
  344. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf6_same_alias_different_import/run.py +0 -0
  345. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf7_docstring_import/pkg/__init__.py +0 -0
  346. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf7_docstring_import/pkg/a.py +0 -0
  347. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf7_docstring_import/pkg/b.py +0 -0
  348. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf7_docstring_import/run.py +0 -0
  349. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/__init__.py +0 -0
  350. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/a.py +0 -0
  351. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/b.py +0 -0
  352. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/c.py +0 -0
  353. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/d.py +0 -0
  354. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/pkg/e.py +0 -0
  355. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf8_helper_name_collision/run.py +0 -0
  356. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/__init__.py +0 -0
  357. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/one/__init__.py +0 -0
  358. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/one/base.py +0 -0
  359. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/one/introspect.py +0 -0
  360. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/one/revoke.py +0 -0
  361. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/two/__init__.py +0 -0
  362. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/two/access.py +0 -0
  363. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/two/base.py +0 -0
  364. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/pkg/two/request.py +0 -0
  365. {code_towel-1.414 → code_towel-1.618}/tests/hostile_crossfile/xf9_same_named_base_class/run.py +0 -0
  366. {code_towel-1.414 → code_towel-1.618}/tests/run_tests.py +0 -0
  367. {code_towel-1.414 → code_towel-1.618}/tests/test_analysis_sessions.py +0 -0
  368. {code_towel-1.414 → code_towel-1.618}/tests/test_bindings_additional.py +0 -0
  369. {code_towel-1.414 → code_towel-1.618}/tests/test_change_transactions.py +0 -0
  370. {code_towel-1.414 → code_towel-1.618}/tests/test_cli_output_safety.py +0 -0
  371. {code_towel-1.414 → code_towel-1.618}/tests/test_copy_preservation.py +0 -0
  372. {code_towel-1.414 → code_towel-1.618}/tests/test_crossfile_integration.py +0 -0
  373. {code_towel-1.414 → code_towel-1.618}/tests/test_definite_assignment.py +0 -0
  374. {code_towel-1.414 → code_towel-1.618}/tests/test_engine_failure_visibility.py +0 -0
  375. {code_towel-1.414 → code_towel-1.618}/tests/test_equivalence_harness_regressions.py +0 -0
  376. {code_towel-1.414 → code_towel-1.618}/tests/test_equivalence_target_selection.py +0 -0
  377. {code_towel-1.414 → code_towel-1.618}/tests/test_exceptions.py +0 -0
  378. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_augassign_and_fstrings.py +0 -0
  379. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_callee_and_multi_return.py +0 -0
  380. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_final_lines.py +0 -0
  381. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_generate_call_variants.py +0 -0
  382. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_more_paths.py +0 -0
  383. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_preamble_and_call_mapping.py +0 -0
  384. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_remaining_branches.py +0 -0
  385. {code_towel-1.414 → code_towel-1.618}/tests/test_extractor_substituter_and_helpers.py +0 -0
  386. {code_towel-1.414 → code_towel-1.618}/tests/test_frame_sensitivity_scan.py +0 -0
  387. {code_towel-1.414 → code_towel-1.618}/tests/test_fstrings.py +0 -0
  388. {code_towel-1.414 → code_towel-1.618}/tests/test_hostile_battery.py +0 -0
  389. {code_towel-1.414 → code_towel-1.618}/tests/test_hostile_crossfile_battery.py +0 -0
  390. {code_towel-1.414 → code_towel-1.618}/tests/test_import_bindings.py +0 -0
  391. {code_towel-1.414 → code_towel-1.618}/tests/test_import_resolution.py +0 -0
  392. {code_towel-1.414 → code_towel-1.618}/tests/test_instantiation_check.py +0 -0
  393. {code_towel-1.414 → code_towel-1.618}/tests/test_method_level_and_attributes.py +0 -0
  394. {code_towel-1.414 → code_towel-1.618}/tests/test_method_receiver_arguments.py +0 -0
  395. {code_towel-1.414 → code_towel-1.618}/tests/test_orphan_detection.py +0 -0
  396. {code_towel-1.414 → code_towel-1.618}/tests/test_overlap_filtering.py +0 -0
  397. {code_towel-1.414 → code_towel-1.618}/tests/test_parent_watchdog.py +0 -0
  398. {code_towel-1.414 → code_towel-1.618}/tests/test_pipeline_api.py +0 -0
  399. {code_towel-1.414 → code_towel-1.618}/tests/test_pipeline_phases.py +0 -0
  400. {code_towel-1.414 → code_towel-1.618}/tests/test_progress_modes.py +0 -0
  401. {code_towel-1.414 → code_towel-1.618}/tests/test_regressions_broader.py +0 -0
  402. {code_towel-1.414 → code_towel-1.618}/tests/test_release_regressions.py +0 -0
  403. {code_towel-1.414 → code_towel-1.618}/tests/test_rename_parameters_and_methods.py +0 -0
  404. {code_towel-1.414 → code_towel-1.618}/tests/test_return_values.py +0 -0
  405. {code_towel-1.414 → code_towel-1.618}/tests/test_scope_analyzer.py +0 -0
  406. {code_towel-1.414 → code_towel-1.618}/tests/test_semantic_guards_extended.py +0 -0
  407. {code_towel-1.414 → code_towel-1.618}/tests/test_structural_memo.py +0 -0
  408. {code_towel-1.414 → code_towel-1.618}/tests/test_thunk_inlining.py +0 -0
  409. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_batch2.py +0 -0
  410. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_batch3.py +0 -0
  411. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_batch4.py +0 -0
  412. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_batch5.py +0 -0
  413. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_comprehensions.py +0 -0
  414. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_core.py +0 -0
  415. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_more_branches.py +0 -0
  416. {code_towel-1.414 → code_towel-1.618}/tests/test_unifier_state_reset.py +0 -0
@@ -7,6 +7,32 @@ ecosystem evidence behind each claim. The format follows
7
7
  [Keep a Changelog](https://keepachangelog.com/), and the project aims to follow
8
8
  [Semantic Versioning](https://semver.org/).
9
9
 
10
+ ## [Unreleased]
11
+
12
+ ## [1.618] — 2026-09-16
13
+
14
+ ### Changed
15
+ - Cross-file helpers are imported relatively by default (`from .module import
16
+ helper`), which stays valid when an out-of-place output is adopted into its
17
+ real location. An absolute import is used only when a packaging marker
18
+ (`pyproject.toml`/`setup.*`) anchors the module name, so the previous
19
+ behavior is preserved for in-place refactoring of a packaged project.
20
+ - Trivial forwarding helpers are no longer proposed: a block whose helper body
21
+ would be a single `raise`, a `return` of one call, or a bare call adds
22
+ indirection without sharing logic. Construct the engine with
23
+ `skip_trivial_helpers=False` to restore the old behavior.
24
+ - A cross-file helper is placed in a module that does not close an import cycle.
25
+ When the shared block spans modules, the helper is hosted in one the others
26
+ already import rather than adding a back-edge; the extraction is declined only
27
+ when no placement is safe (a genuine pre-existing cycle). This replaces the
28
+ earlier behavior of hosting the helper in the first module and declining
29
+ whenever that would cycle.
30
+
31
+ ### Fixed
32
+ - The rename tool no longer refuses a module merely because it contains a local
33
+ variable or parameter named `vars`, `globals`, `locals`, `eval`, or `exec`;
34
+ it flags only a genuine reference to the builtin.
35
+
10
36
  ## [1.414] — 2026-09-15
11
37
 
12
38
  First release prepared under the open-source audit. Beta: the engineering and
@@ -172,4 +172,4 @@ Thank you for contributing to Towel!
172
172
 
173
173
  For reproducible tool versions, use `uv sync --frozen --extra dev` and the commands in README.md. CI runs the full tests and an unconditional 85% coverage gate for each supported Python version. `just release VERSION` prepares local distributions only; publication requires maintainer review of the current audit and policy decisions.
174
174
 
175
- Release maintainers should follow [docs/RELEASING.md](docs/RELEASING.md). Legacy AST normalization helpers are compatibility utilities, not safe preprocessing passes; do not use them to justify behavior-preservation claims.
175
+ Release maintainers should follow [docs/RELEASING.md](docs/RELEASING.md).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-towel
3
- Version: 1.414
3
+ Version: 1.618
4
4
  Summary: A Python tool that DRYs your code - finds and refactors repeated code using unification-based analysis
5
5
  Author: Eric Allen
6
6
  License-Expression: Apache-2.0
@@ -36,19 +36,85 @@ Dynamic: license-file
36
36
 
37
37
  Towel finds repeated Python code using unification and proposes helper-function extractions.
38
38
 
39
+ > **Install from PyPI as [`code-towel`](https://pypi.org/project/code-towel/)** (the command is `towel`):
40
+ > `pip install code-towel`
41
+ > Do **not** install `towel`: `pip install towel` and `uvx towel` fetch a different, unrelated project.
42
+
39
43
  **Release status: 1.414 (beta).** Every accepted proposal is verified by instantiating the helper with each call's arguments and comparing the result with the block it replaces; arguments that could have observable effects or fresh identity are evaluated inside the helper at their original position. A standing ecosystem check refactors 91 public projects and runs each one's own test suite before and after: 75 pass identically, 13 produce no proposal, and 3 differ only in documented frame-sensitive or source-observing ways (see below). Refactoring is still a change to your code: preview first, review the diff, and run your tests. [Known limitations](docs/KNOWN_LIMITATIONS.md) lists what is verified, what is rejected, and what remains outside the model; [the readiness report](docs/PRODUCTION_READINESS.md) records the evidence.
40
44
 
45
+ **New here?** The [Quick start](docs/QUICKSTART.md) gets you from install to a reviewed refactoring in four steps.
46
+
47
+ ## What it does
48
+
49
+ Towel finds code that is repeated across your functions and pulls each group of
50
+ duplicates into one shared helper, rewriting the copies as calls to it. It works
51
+ by *anti-unification*: it computes the least-general generalization of the
52
+ matching blocks, so the parts that are the same become the helper's body and the
53
+ parts that differ become its parameters.
54
+
55
+ Given two functions that share a block:
56
+
57
+ ```python
58
+ def order_summary(order):
59
+ items = [i for i in order.items if i.in_stock]
60
+ subtotal = sum(i.price for i in items)
61
+ total = round(subtotal * 1.08, 2)
62
+ return f"Order {order.id}: ${total}"
63
+
64
+ def quote_summary(quote):
65
+ items = [i for i in quote.items if i.in_stock]
66
+ subtotal = sum(i.price for i in items)
67
+ total = round(subtotal * 1.08, 2)
68
+ return f"Quote {quote.id}: ${total}"
69
+ ```
70
+
71
+ Towel proposes the shared block as a helper (emitted with a placeholder name you
72
+ rename afterward) and rewrites both functions to call it:
73
+
74
+ ```python
75
+ def __extracted_func_0(record):
76
+ items = [i for i in record.items if i.in_stock]
77
+ subtotal = sum(i.price for i in items)
78
+ total = round(subtotal * 1.08, 2)
79
+ return total
80
+
81
+ def order_summary(order):
82
+ total = __extracted_func_0(order)
83
+ return f"Order {order.id}: ${total}"
84
+
85
+ def quote_summary(quote):
86
+ total = __extracted_func_0(quote)
87
+ return f"Quote {quote.id}: ${total}"
88
+ ```
89
+
90
+ What makes Towel different from a search-and-replace is that it is conservative
91
+ and verified. It proposes an extraction only when it can prove the result runs
92
+ the same as the original — instantiating the helper with each call's arguments
93
+ and comparing against the block it replaced — and it refuses cases it cannot
94
+ establish rather than guess. It skips trivial extractions that would add
95
+ indirection without sharing real logic, wraps arguments that must not be
96
+ evaluated eagerly in zero-argument `lambda`s (see
97
+ [below](#why-some-arguments-are-wrapped-in-lambda)), and leaves naming to you.
98
+ Nothing is written without your say-so: the workflow is preview, refactor into a
99
+ copy, review the diff, and run your tests.
100
+
41
101
  ## Install
42
102
 
43
- The runtime uses only the standard library; optional `tqdm` provides progress bars. Platform, CPU, memory, and disk requirements are in [Requirements](#requirements) below.
103
+ The PyPI package is **`code-towel`**; installing it gives you the **`towel`** command.
44
104
 
45
105
  ```bash
46
- python -m pip install .
106
+ pip install code-towel
47
107
  towel --version
48
108
  towel --help
49
109
  ```
50
110
 
51
- The commands below describe this checkout; previously published distributions may differ.
111
+ Install `code-towel`, not `towel`: the name `towel` on PyPI is a different, unrelated project, so `pip install towel` and `uvx towel` will not install this tool. To run it with `uvx` without installing, name the package explicitly:
112
+
113
+ ```bash
114
+ uvx --from code-towel towel --help
115
+ ```
116
+
117
+ The runtime uses only the standard library; optional `tqdm` provides progress bars. Platform, CPU, memory, and disk requirements are in [Requirements](#requirements) below.
52
118
 
53
119
  ## Requirements
54
120
 
@@ -79,7 +145,8 @@ The two largest projects in the ecosystem check, networkx and Sphinx, are the sl
79
145
  ## Use
80
146
 
81
147
  ```bash
82
- # Read-only analysis
148
+ # Read-only analysis: lists each opportunity with the extracted helper and,
149
+ # per call site, the original block (-) next to the generated call (+)
83
150
  towel preview path/to/project
84
151
 
85
152
  # Write to a new output directory
@@ -98,7 +165,7 @@ To roll back an interrupted batch, use `towel recover /path/to/.towel-transactio
98
165
 
99
166
  For detailed conservative rejection reasons, set `DEBUG_PROPOSAL_REJECTIONS=1` when running preview.
100
167
 
101
- `towel-dry` and `towel-preview` are compatibility entry points. `python scripts/dry` delegates to the same installed CLI. Run `towel dry --help` for import-layout, iteration, and progress options.
168
+ Run `towel dry --help` for import-layout, iteration, and progress options.
102
169
 
103
170
  ## What is analyzed
104
171
 
@@ -106,10 +173,51 @@ The pipeline parses modules, analyzes scopes, collects functions and classes, co
106
173
 
107
174
  Differing sub-expressions become helper parameters. Names, literals, and tuples of those are passed eagerly; any other expression is passed as a zero-argument thunk and evaluated inside the helper where the original expression stood, so evaluation order, count, and conditionality are preserved. Expressions that read names bound inside the block are lambda-lifted with those names as arguments. A thunk the helper would evaluate first, once, and unconditionally is passed eagerly instead, since nothing can observe the difference. Before a proposal is offered, the helper is instantiated with each call's arguments and must reproduce the original block up to renamed binders.
108
175
 
109
- The refactoring pipeline preserves the original Python operators. The legacy `ast_normalizer` utilities remain available with deprecation warnings for compatibility, but can change Python behavior and are not used by this pipeline. Generator/suspension operations and frame-sensitive calls such as `locals()` are conservatively rejected. Nested blocks that bind names used outside the block are rejected until full control-flow liveness is supported. Static local import cycles and cross-module global declarations are rejected. This reduces the number of proposals rather than claiming an unsupported transformation is safe.
176
+ The refactoring pipeline preserves the original Python operators. Generator/suspension operations and frame-sensitive calls such as `locals()` are conservatively rejected. Nested blocks that bind names used outside the block are rejected until full control-flow liveness is supported. Static local import cycles and cross-module global declarations are rejected. This reduces the number of proposals rather than claiming an unsupported transformation is safe.
110
177
 
111
178
  Dynamic imports, reflection, arbitrary callbacks, runtime rebinding, metaclasses, and external side effects limit what can be established statically. Each engine owns a bounded analysis session with content checks and isolated AST snapshots. The test import-isolation harness and an individual engine instance require sequential use. Candidates involving detected namespace rebinding, frame inspection, or comprehension assignment expressions are rejected; opaque external reflection and rebinding remain outside the supported model.
112
179
 
180
+ ## Why some arguments are wrapped in `lambda`
181
+
182
+ When the differing sub-expression between two duplicates is more than a plain value, Towel passes it as a zero-argument lambda (a *thunk*) and calls it inside the helper at the exact place the original expression stood. In the output that looks like this:
183
+
184
+ ```python
185
+ notify(lambda: welcome_email(user))
186
+ ```
187
+
188
+ To a casual reader the lambda looks redundant. It is not. Passing the expression directly would evaluate it once, unconditionally, at the call site, and that changes behavior whenever the original evaluated it conditionally, more than once, or not at all. Consider two blocks that differ only in one call (illustrative):
189
+
190
+ ```python
191
+ # before
192
+ if user.active:
193
+ send(welcome_email(user))
194
+ ```
195
+
196
+ Pass that call eagerly and it runs for every user, active or not:
197
+
198
+ ```python
199
+ # WRONG: welcome_email(user) runs unconditionally, with its side effects, and
200
+ # can raise, even when user.active is False
201
+ def notify(message, user):
202
+ if user.active:
203
+ send(message)
204
+
205
+ notify(welcome_email(user), user)
206
+ ```
207
+
208
+ The thunk defers it to the spot the original code ran it, preserving the condition:
209
+
210
+ ```python
211
+ # correct
212
+ def notify(build_message, user):
213
+ if user.active:
214
+ send(build_message())
215
+
216
+ notify(lambda: welcome_email(user), user)
217
+ ```
218
+
219
+ The same reasoning covers an expression inside a loop (the thunk runs once per iteration, as the original did) or one that might raise. Towel keeps the wrapper only where it can matter: an expression the helper would evaluate first, once, and unconditionally is passed directly, because then nothing can observe the difference. So a `lambda:` in the output is a deliberate marker that Towel is preserving that argument's timing, count, or conditionality; its absence means eager evaluation was proven equivalent.
220
+
113
221
  ## Naming the helpers with an LLM
114
222
 
115
223
  Towel deliberately generates meaningless names, `__extracted_func_3` and `__param_0`, and leaves the naming to a separate, review-first step. Extraction is a verified mechanical transformation; choosing a good name is a judgment call, so the two are kept apart. The intended workflow hands the naming to a coding assistant, because the assistant reads far better names out of the call sites than any heuristic, and you review its choices before they touch the code.
@@ -131,6 +239,8 @@ towel rename-helpers path/to/cleaned --rename-file renames.json
131
239
 
132
240
  The JSON inventory gives the assistant what it needs to name well: every helper with its scope, source, and call sites, and for each parameter its evaluation kind (`value`, `thunk`, `lifted`, `receiver`) and the actual argument expressions passed at every call site. A parameter that always receives `user.email` and `account.email` should become `email`, and the argument expressions are how the assistant sees that.
133
241
 
242
+ Each helper also carries a `changes` list: for every call site, the exact original block it replaced (`before`) next to the generated call (`after`). Seeing what the code did before extraction is what lets an assistant finish good names, write a docstring, and infer parameter and return types. This comes from a small `.towel-helpers.json` that `towel dry` writes next to its output; it is only for the naming step and is safe to delete afterward.
243
+
134
244
  Each inventory entry carries the exact mapping key to use as a rename target: `"path.py:helper"` renames a module-level helper together with its importers, `"helper"` renames a unique class-level helper together with every attribute reference, and `"path.py:helper.__param_0"` renames a parameter within the helper's scope. The rename is applied as one atomic batch with scope and importer checks; a name collision, a mangled name, or a dynamic reference aborts the whole batch and reports the reason, so a bad suggestion changes nothing. `--dry-run` reports the same JSON without writing.
135
245
 
136
246
  The shared `towel-rename` skill (in the agent-skills repository) walks an assistant through the whole loop: extract, review the diff, name, apply, and re-test. An interactive prompt mode is also available for naming by hand, and it calls no LLM service.
@@ -160,15 +270,21 @@ Behavioral tests compare sampled return values and types, exceptions, output, an
160
270
 
161
271
  ## Documentation
162
272
 
163
- - [Contributing](CONTRIBUTING.md)
164
- - [Security policy](SECURITY.md)
165
- - [Architecture](docs/ARCHITECTURE.md)
166
- - [Known limitations](docs/KNOWN_LIMITATIONS.md)
167
- - [Production readiness and ecosystem evidence](docs/PRODUCTION_READINESS.md)
168
- - [Adversarial review](docs/ADVERSARIAL_REVIEW.md)
169
- - [Historical issues](KNOWN_ISSUES.md)
273
+ Start here:
274
+
275
+ - [Quick start](docs/QUICKSTART.md) — install and refactor in four steps
276
+ - [Known limitations](docs/KNOWN_LIMITATIONS.md) — what is verified, what is rejected, and what is outside the model
277
+ - [Python API guide](docs/USAGE_GUIDE.md) — using `UnificationRefactorEngine` directly
278
+
279
+ How it works and why to trust it:
280
+
281
+ - [Architecture](docs/ARCHITECTURE.md) — the pipeline, the algorithms, and their references
282
+ - [Production readiness](docs/PRODUCTION_READINESS.md) — the ecosystem evidence behind the claims
283
+ - [Adversarial review](docs/ADVERSARIAL_REVIEW.md) — defects found and repaired
284
+
285
+ Project:
170
286
 
171
- Historical notes and example outputs document earlier versions and may describe behavior superseded by the current audit. The current CLI help and source define the available interface.
287
+ - [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security policy](SECURITY.md) · [Releasing](docs/RELEASING.md)
172
288
 
173
289
  ## License
174
290
 
@@ -2,19 +2,85 @@
2
2
 
3
3
  Towel finds repeated Python code using unification and proposes helper-function extractions.
4
4
 
5
+ > **Install from PyPI as [`code-towel`](https://pypi.org/project/code-towel/)** (the command is `towel`):
6
+ > `pip install code-towel`
7
+ > Do **not** install `towel`: `pip install towel` and `uvx towel` fetch a different, unrelated project.
8
+
5
9
  **Release status: 1.414 (beta).** Every accepted proposal is verified by instantiating the helper with each call's arguments and comparing the result with the block it replaces; arguments that could have observable effects or fresh identity are evaluated inside the helper at their original position. A standing ecosystem check refactors 91 public projects and runs each one's own test suite before and after: 75 pass identically, 13 produce no proposal, and 3 differ only in documented frame-sensitive or source-observing ways (see below). Refactoring is still a change to your code: preview first, review the diff, and run your tests. [Known limitations](docs/KNOWN_LIMITATIONS.md) lists what is verified, what is rejected, and what remains outside the model; [the readiness report](docs/PRODUCTION_READINESS.md) records the evidence.
6
10
 
11
+ **New here?** The [Quick start](docs/QUICKSTART.md) gets you from install to a reviewed refactoring in four steps.
12
+
13
+ ## What it does
14
+
15
+ Towel finds code that is repeated across your functions and pulls each group of
16
+ duplicates into one shared helper, rewriting the copies as calls to it. It works
17
+ by *anti-unification*: it computes the least-general generalization of the
18
+ matching blocks, so the parts that are the same become the helper's body and the
19
+ parts that differ become its parameters.
20
+
21
+ Given two functions that share a block:
22
+
23
+ ```python
24
+ def order_summary(order):
25
+ items = [i for i in order.items if i.in_stock]
26
+ subtotal = sum(i.price for i in items)
27
+ total = round(subtotal * 1.08, 2)
28
+ return f"Order {order.id}: ${total}"
29
+
30
+ def quote_summary(quote):
31
+ items = [i for i in quote.items if i.in_stock]
32
+ subtotal = sum(i.price for i in items)
33
+ total = round(subtotal * 1.08, 2)
34
+ return f"Quote {quote.id}: ${total}"
35
+ ```
36
+
37
+ Towel proposes the shared block as a helper (emitted with a placeholder name you
38
+ rename afterward) and rewrites both functions to call it:
39
+
40
+ ```python
41
+ def __extracted_func_0(record):
42
+ items = [i for i in record.items if i.in_stock]
43
+ subtotal = sum(i.price for i in items)
44
+ total = round(subtotal * 1.08, 2)
45
+ return total
46
+
47
+ def order_summary(order):
48
+ total = __extracted_func_0(order)
49
+ return f"Order {order.id}: ${total}"
50
+
51
+ def quote_summary(quote):
52
+ total = __extracted_func_0(quote)
53
+ return f"Quote {quote.id}: ${total}"
54
+ ```
55
+
56
+ What makes Towel different from a search-and-replace is that it is conservative
57
+ and verified. It proposes an extraction only when it can prove the result runs
58
+ the same as the original — instantiating the helper with each call's arguments
59
+ and comparing against the block it replaced — and it refuses cases it cannot
60
+ establish rather than guess. It skips trivial extractions that would add
61
+ indirection without sharing real logic, wraps arguments that must not be
62
+ evaluated eagerly in zero-argument `lambda`s (see
63
+ [below](#why-some-arguments-are-wrapped-in-lambda)), and leaves naming to you.
64
+ Nothing is written without your say-so: the workflow is preview, refactor into a
65
+ copy, review the diff, and run your tests.
66
+
7
67
  ## Install
8
68
 
9
- The runtime uses only the standard library; optional `tqdm` provides progress bars. Platform, CPU, memory, and disk requirements are in [Requirements](#requirements) below.
69
+ The PyPI package is **`code-towel`**; installing it gives you the **`towel`** command.
10
70
 
11
71
  ```bash
12
- python -m pip install .
72
+ pip install code-towel
13
73
  towel --version
14
74
  towel --help
15
75
  ```
16
76
 
17
- The commands below describe this checkout; previously published distributions may differ.
77
+ Install `code-towel`, not `towel`: the name `towel` on PyPI is a different, unrelated project, so `pip install towel` and `uvx towel` will not install this tool. To run it with `uvx` without installing, name the package explicitly:
78
+
79
+ ```bash
80
+ uvx --from code-towel towel --help
81
+ ```
82
+
83
+ The runtime uses only the standard library; optional `tqdm` provides progress bars. Platform, CPU, memory, and disk requirements are in [Requirements](#requirements) below.
18
84
 
19
85
  ## Requirements
20
86
 
@@ -45,7 +111,8 @@ The two largest projects in the ecosystem check, networkx and Sphinx, are the sl
45
111
  ## Use
46
112
 
47
113
  ```bash
48
- # Read-only analysis
114
+ # Read-only analysis: lists each opportunity with the extracted helper and,
115
+ # per call site, the original block (-) next to the generated call (+)
49
116
  towel preview path/to/project
50
117
 
51
118
  # Write to a new output directory
@@ -64,7 +131,7 @@ To roll back an interrupted batch, use `towel recover /path/to/.towel-transactio
64
131
 
65
132
  For detailed conservative rejection reasons, set `DEBUG_PROPOSAL_REJECTIONS=1` when running preview.
66
133
 
67
- `towel-dry` and `towel-preview` are compatibility entry points. `python scripts/dry` delegates to the same installed CLI. Run `towel dry --help` for import-layout, iteration, and progress options.
134
+ Run `towel dry --help` for import-layout, iteration, and progress options.
68
135
 
69
136
  ## What is analyzed
70
137
 
@@ -72,10 +139,51 @@ The pipeline parses modules, analyzes scopes, collects functions and classes, co
72
139
 
73
140
  Differing sub-expressions become helper parameters. Names, literals, and tuples of those are passed eagerly; any other expression is passed as a zero-argument thunk and evaluated inside the helper where the original expression stood, so evaluation order, count, and conditionality are preserved. Expressions that read names bound inside the block are lambda-lifted with those names as arguments. A thunk the helper would evaluate first, once, and unconditionally is passed eagerly instead, since nothing can observe the difference. Before a proposal is offered, the helper is instantiated with each call's arguments and must reproduce the original block up to renamed binders.
74
141
 
75
- The refactoring pipeline preserves the original Python operators. The legacy `ast_normalizer` utilities remain available with deprecation warnings for compatibility, but can change Python behavior and are not used by this pipeline. Generator/suspension operations and frame-sensitive calls such as `locals()` are conservatively rejected. Nested blocks that bind names used outside the block are rejected until full control-flow liveness is supported. Static local import cycles and cross-module global declarations are rejected. This reduces the number of proposals rather than claiming an unsupported transformation is safe.
142
+ The refactoring pipeline preserves the original Python operators. Generator/suspension operations and frame-sensitive calls such as `locals()` are conservatively rejected. Nested blocks that bind names used outside the block are rejected until full control-flow liveness is supported. Static local import cycles and cross-module global declarations are rejected. This reduces the number of proposals rather than claiming an unsupported transformation is safe.
76
143
 
77
144
  Dynamic imports, reflection, arbitrary callbacks, runtime rebinding, metaclasses, and external side effects limit what can be established statically. Each engine owns a bounded analysis session with content checks and isolated AST snapshots. The test import-isolation harness and an individual engine instance require sequential use. Candidates involving detected namespace rebinding, frame inspection, or comprehension assignment expressions are rejected; opaque external reflection and rebinding remain outside the supported model.
78
145
 
146
+ ## Why some arguments are wrapped in `lambda`
147
+
148
+ When the differing sub-expression between two duplicates is more than a plain value, Towel passes it as a zero-argument lambda (a *thunk*) and calls it inside the helper at the exact place the original expression stood. In the output that looks like this:
149
+
150
+ ```python
151
+ notify(lambda: welcome_email(user))
152
+ ```
153
+
154
+ To a casual reader the lambda looks redundant. It is not. Passing the expression directly would evaluate it once, unconditionally, at the call site, and that changes behavior whenever the original evaluated it conditionally, more than once, or not at all. Consider two blocks that differ only in one call (illustrative):
155
+
156
+ ```python
157
+ # before
158
+ if user.active:
159
+ send(welcome_email(user))
160
+ ```
161
+
162
+ Pass that call eagerly and it runs for every user, active or not:
163
+
164
+ ```python
165
+ # WRONG: welcome_email(user) runs unconditionally, with its side effects, and
166
+ # can raise, even when user.active is False
167
+ def notify(message, user):
168
+ if user.active:
169
+ send(message)
170
+
171
+ notify(welcome_email(user), user)
172
+ ```
173
+
174
+ The thunk defers it to the spot the original code ran it, preserving the condition:
175
+
176
+ ```python
177
+ # correct
178
+ def notify(build_message, user):
179
+ if user.active:
180
+ send(build_message())
181
+
182
+ notify(lambda: welcome_email(user), user)
183
+ ```
184
+
185
+ The same reasoning covers an expression inside a loop (the thunk runs once per iteration, as the original did) or one that might raise. Towel keeps the wrapper only where it can matter: an expression the helper would evaluate first, once, and unconditionally is passed directly, because then nothing can observe the difference. So a `lambda:` in the output is a deliberate marker that Towel is preserving that argument's timing, count, or conditionality; its absence means eager evaluation was proven equivalent.
186
+
79
187
  ## Naming the helpers with an LLM
80
188
 
81
189
  Towel deliberately generates meaningless names, `__extracted_func_3` and `__param_0`, and leaves the naming to a separate, review-first step. Extraction is a verified mechanical transformation; choosing a good name is a judgment call, so the two are kept apart. The intended workflow hands the naming to a coding assistant, because the assistant reads far better names out of the call sites than any heuristic, and you review its choices before they touch the code.
@@ -97,6 +205,8 @@ towel rename-helpers path/to/cleaned --rename-file renames.json
97
205
 
98
206
  The JSON inventory gives the assistant what it needs to name well: every helper with its scope, source, and call sites, and for each parameter its evaluation kind (`value`, `thunk`, `lifted`, `receiver`) and the actual argument expressions passed at every call site. A parameter that always receives `user.email` and `account.email` should become `email`, and the argument expressions are how the assistant sees that.
99
207
 
208
+ Each helper also carries a `changes` list: for every call site, the exact original block it replaced (`before`) next to the generated call (`after`). Seeing what the code did before extraction is what lets an assistant finish good names, write a docstring, and infer parameter and return types. This comes from a small `.towel-helpers.json` that `towel dry` writes next to its output; it is only for the naming step and is safe to delete afterward.
209
+
100
210
  Each inventory entry carries the exact mapping key to use as a rename target: `"path.py:helper"` renames a module-level helper together with its importers, `"helper"` renames a unique class-level helper together with every attribute reference, and `"path.py:helper.__param_0"` renames a parameter within the helper's scope. The rename is applied as one atomic batch with scope and importer checks; a name collision, a mangled name, or a dynamic reference aborts the whole batch and reports the reason, so a bad suggestion changes nothing. `--dry-run` reports the same JSON without writing.
101
211
 
102
212
  The shared `towel-rename` skill (in the agent-skills repository) walks an assistant through the whole loop: extract, review the diff, name, apply, and re-test. An interactive prompt mode is also available for naming by hand, and it calls no LLM service.
@@ -126,15 +236,21 @@ Behavioral tests compare sampled return values and types, exceptions, output, an
126
236
 
127
237
  ## Documentation
128
238
 
129
- - [Contributing](CONTRIBUTING.md)
130
- - [Security policy](SECURITY.md)
131
- - [Architecture](docs/ARCHITECTURE.md)
132
- - [Known limitations](docs/KNOWN_LIMITATIONS.md)
133
- - [Production readiness and ecosystem evidence](docs/PRODUCTION_READINESS.md)
134
- - [Adversarial review](docs/ADVERSARIAL_REVIEW.md)
135
- - [Historical issues](KNOWN_ISSUES.md)
239
+ Start here:
240
+
241
+ - [Quick start](docs/QUICKSTART.md) — install and refactor in four steps
242
+ - [Known limitations](docs/KNOWN_LIMITATIONS.md) — what is verified, what is rejected, and what is outside the model
243
+ - [Python API guide](docs/USAGE_GUIDE.md) — using `UnificationRefactorEngine` directly
244
+
245
+ How it works and why to trust it:
246
+
247
+ - [Architecture](docs/ARCHITECTURE.md) — the pipeline, the algorithms, and their references
248
+ - [Production readiness](docs/PRODUCTION_READINESS.md) — the ecosystem evidence behind the claims
249
+ - [Adversarial review](docs/ADVERSARIAL_REVIEW.md) — defects found and repaired
250
+
251
+ Project:
136
252
 
137
- Historical notes and example outputs document earlier versions and may describe behavior superseded by the current audit. The current CLI help and source define the available interface.
253
+ - [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security policy](SECURITY.md) · [Releasing](docs/RELEASING.md)
138
254
 
139
255
  ## License
140
256
 
@@ -0,0 +1,19 @@
1
+ # Security policy
2
+
3
+ Towel rewrites source code. Review the generated diff and run the affected project's own tests before adopting it; see [docs/KNOWN_LIMITATIONS.md](docs/KNOWN_LIMITATIONS.md) for what is verified and what is not.
4
+
5
+ ## Supported versions
6
+
7
+ Security fixes are provided for the latest released version, currently **1.414**. Earlier versions are not supported; the PyPI releases `1.0.0`–`1.0.4` are yanked for broken import handling and are not a recommended installation target. Upgrade to the latest release rather than relying on a fix to an older one.
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ Report suspected vulnerabilities privately through GitHub's **"Report a vulnerability"** button on this repository's **Security** tab (Security → Advisories → Report a vulnerability). That opens a private security advisory visible only to the maintainer. Please do not open a public issue for a security report, and do not include credentials or confidential source code in a report.
12
+
13
+ This is a solo-maintained project with no formal response-time commitment. Reports are handled on a best-effort basis; you will receive an acknowledgment when a report is triaged. If a report leads to a fix, it ships in a new release, and the affected versions are noted in the advisory and the [changelog](CHANGELOG.md).
14
+
15
+ ## Execution and source safety
16
+
17
+ Towel's analysis parses Python syntax without executing the analyzed project. The behavioral test harness executes fixtures and must be used only on trusted code. The rename assistant prints a prompt containing source excerpts for the user to transfer manually; consider the destination before sharing confidential code.
18
+
19
+ Directory refactoring excludes symlinked Python files and refuses overlapping input/output trees. Generated files are compiled before writing. Writes use staged byte plans, per-file atomic replacement, rollback, and an interruption-recovery journal. Apply and recovery require exclusive write access; an unrelated writer can race snapshot checks. A batch is not globally atomic to readers, and a sequence of extractions is not a single atomic operation. Compilation does not prove behavior preservation. Static analysis cannot fully model reflection, dynamic imports, arbitrary callbacks, or external effects. Keep the original version under source control and review every generated diff.
@@ -42,7 +42,10 @@ fixed-point loop (below).
42
42
  blocks that unify with the accepted template and can share the helper.
43
43
  9. **Materialize.** `extractor.py` renders the helper and the call sites,
44
44
  compiles the generated Python to confirm it parses, and detects overlapping
45
- replacements.
45
+ replacements. A proposal whose rendered helper body is a single forwarding
46
+ statement — a lone `raise`, a `return` of one call, or a bare call — is
47
+ dropped here: it would only add indirection. Construct the engine with
48
+ `skip_trivial_helpers=False` to keep such helpers.
46
49
  10. **Apply.** `changes.py` turns accepted proposals into an immutable byte
47
50
  plan and applies it transactionally (see *Application and recovery*).
48
51
 
@@ -172,8 +175,23 @@ helper can be imported correctly. It reads the build backend from
172
175
  `pyproject.toml` and derives import roots for setuptools (including
173
176
  `package-dir` mappings and the classic `src` layout), Hatch (wheel
174
177
  `packages`/`sources`), Flit, Poetry (`packages` with `from`), and pdm
175
- (`package-dir`). A layout it cannot resolve safely raises rather than guessing;
176
- the ecosystem check reports those as `UNSUPPORTED`. `semantic_safety.py`'s
178
+ (`package-dir`). An unrecognized backend falls back to conventional inference:
179
+ a package or module named after the distribution, in the project root or under
180
+ `src`. Only a layout that cannot be resolved either way raises rather than
181
+ guessing; the ecosystem check reports those as `UNSUPPORTED`.
182
+
183
+ For a helper shared between two files in the same package, `refactor_engine.py`
184
+ generates a relative import (`from .module import helper`, or a deeper
185
+ `..sub.module`). A relative import encodes only the intrinsic same-package
186
+ relationship, so it stays valid wherever the code lands — in particular when an
187
+ out-of-place output is adopted into its real location, the documented workflow —
188
+ and it matches the intra-package style the code already uses. An absolute import
189
+ is used only when the discovered layout is anchored by a real packaging marker
190
+ (`ProjectLayout.metadata_root`), so the absolute name survives relocation; a flat
191
+ module with no package, where a relative import would not resolve, keeps a bare
192
+ absolute name.
193
+
194
+ `semantic_safety.py`'s
177
195
  `would_create_import_cycle` follows static imports through local modules,
178
196
  caching each module's import edges by path, mtime, and size, and rejects a
179
197
  helper placement that would close a cycle.
@@ -273,16 +291,6 @@ mapping as one atomic batch with scope and importer checks; a collision, a
273
291
  mangled name, or a dynamic reference aborts the whole batch and reports why, so
274
292
  a bad suggestion changes nothing. See the README for the end-to-end workflow.
275
293
 
276
- ## Legacy normalization utilities
277
-
278
- [`ast_normalizer.py`](../src/towel/unification/ast_normalizer.py) is an
279
- importable compatibility module that emits `DeprecationWarning` when its
280
- transformers are constructed. Its assignment-to-augmented-assignment and
281
- arithmetic rewrites are **not used by the production pipeline** and are unsafe
282
- for arbitrary Python (`x = x + y` and `x += y` differ in aliasing and operator
283
- dispatch; reordering changes evaluation order and overloaded behavior). Do not
284
- reintroduce them into analysis or use them as an equivalence oracle.
285
-
286
294
  ## Verification and evidence
287
295
 
288
296
  The transformation's safety rests on the per-proposal instantiation invariant.
@@ -341,6 +349,8 @@ but the ideas and their names are from the literature.
341
349
  | Liveness and orphans | `definite_assignment.py`, `orphan_detector.py` |
342
350
  | Safety guards, import cycles, pre-scan | `semantic_safety.py` |
343
351
  | Helper and call-site rendering | `extractor.py`, `thunk_inlining.py` |
352
+ | Parameter enumeration | `parameters.py` |
353
+ | Progress reporting | `progress.py` |
344
354
  | Structural memoization | `structural_memo.py` |
345
355
  | Cross-file layout | `project_layout.py` |
346
356
  | Data model | `models.py` |
@@ -127,6 +127,10 @@ uncertainty. Common reasons a real duplicate is not extracted:
127
127
 
128
128
  - A differing sub-expression is a slice, a starred item, or a whole f-string;
129
129
  these are container syntax rather than values.
130
+ - The extracted helper body would be a single forwarding statement — a lone
131
+ `raise`, a `return` of one call, or a bare call. Such a helper shares no
132
+ logic, only a name, so it is skipped by default; construct the engine with
133
+ `skip_trivial_helpers=False` to keep it.
130
134
  - The block deletes, rebinds, or declares a name the caller keeps using.
131
135
  - A nested function or lambda shares a rebound name with the block.
132
136
  - The helper would need more than the configured maximum parameters.
@@ -143,11 +147,13 @@ uncertainty. Common reasons a real duplicate is not extracted:
143
147
  would have to be rendered inside the preceding branch's `else`; the
144
148
  `elif`'s own body and further branches remain candidates. This gives up a
145
149
  valid extraction when the preceding branch always exits (tabulate).
146
- - Project layouts other than setuptools, Hatch, Flit, Poetry, and pdm
147
- conventions are refused for directory mode because import roots cannot be
148
- inferred safely; the ecosystem check reports these as `UNSUPPORTED`.
149
- Poetry ``packages`` entries with ``to`` or glob patterns, and a pdm
150
- ``package-dir`` pattern, are refused likewise.
150
+ - setuptools, Hatch, Flit, Poetry, and pdm layouts are read from their own
151
+ configuration. Any other build backend (for example ``flit_scm``) falls back
152
+ to conventional inference: a package or module named after the distribution,
153
+ in the project root or under ``src``. A project whose layout cannot be
154
+ resolved either way is refused for directory mode, and the ecosystem check
155
+ reports it as `UNSUPPORTED`. Poetry ``packages`` entries with ``to`` or glob
156
+ patterns, and a pdm ``package-dir`` pattern, are refused likewise.
151
157
 
152
158
  Set `DEBUG_PROPOSAL_REJECTIONS=1` to print the reason for each rejected pair.
153
159
 
@@ -0,0 +1,66 @@
1
+ # Quick start
2
+
3
+ Towel finds repeated Python code and extracts each group of duplicates into one
4
+ helper function, rewriting the duplicates as calls. It verifies every extraction
5
+ and leaves the naming to you. Refactoring changes your code, so the workflow is:
6
+ preview, apply to a copy, review the diff, and run your tests.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install code-towel
12
+ towel --version
13
+ ```
14
+
15
+ The PyPI package is `code-towel` and the command is `towel`. Do not install
16
+ `towel`: that name belongs to a different, unrelated project, so `pip install
17
+ towel` and `uvx towel` will not get this tool. Python 3.11–3.13 on macOS or
18
+ Linux, no runtime dependencies.
19
+
20
+ ## 1. See what it would change (read-only)
21
+
22
+ ```bash
23
+ towel preview path/to/project
24
+ ```
25
+
26
+ For each opportunity, `preview` prints the extracted helper and, per call site, the original block (`-`) next to the generated call (`+`), so you can see exactly what would change before applying anything.
27
+
28
+ ## 2. Refactor into a fresh copy
29
+
30
+ Never refactor in place on your first run — write to a new directory and diff it.
31
+
32
+ ```bash
33
+ towel dry path/to/project path/to/cleaned --non-interactive
34
+ diff -ru path/to/project path/to/cleaned | less
35
+ ```
36
+
37
+ The output compiles and, for every proposal, the generated helper is verified to
38
+ reproduce the exact code it replaced. Helpers get placeholder names like
39
+ `__extracted_func_3`.
40
+
41
+ ## 3. Give the helpers real names (optional, LLM-assisted)
42
+
43
+ ```bash
44
+ towel rename-helpers path/to/cleaned --list --json > helpers.json
45
+ # Have a coding assistant read helpers.json and write renames.json,
46
+ # then apply the batch (it aborts whole if any name is unsafe):
47
+ towel rename-helpers path/to/cleaned --rename-file renames.json --dry-run
48
+ towel rename-helpers path/to/cleaned --rename-file renames.json
49
+ ```
50
+
51
+ See the [README](../README.md#naming-the-helpers-with-an-llm) for the full
52
+ naming workflow.
53
+
54
+ ## 4. Review and test
55
+
56
+ ```bash
57
+ # adopt the cleaned copy however you version-control it, then:
58
+ pytest # or your project's own test command
59
+ ```
60
+
61
+ ## Next
62
+
63
+ - [README](../README.md) — full CLI usage, requirements, and timing
64
+ - [Known limitations](KNOWN_LIMITATIONS.md) — what is verified, rejected, and outside the model
65
+ - [Python API guide](USAGE_GUIDE.md) — using `UnificationRefactorEngine` directly
66
+ - [Architecture](ARCHITECTURE.md) — how it works