redup 0.4.28__tar.gz → 0.4.30__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 (118) hide show
  1. {redup-0.4.28/src/redup.egg-info → redup-0.4.30}/PKG-INFO +29 -42
  2. {redup-0.4.28 → redup-0.4.30}/README.md +6 -6
  3. {redup-0.4.28 → redup-0.4.30}/pyproject.toml +25 -36
  4. {redup-0.4.28 → redup-0.4.30}/src/redup/__init__.py +1 -1
  5. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/config_builder.py +14 -0
  6. redup-0.4.30/src/redup/cli_app/intract_commands.py +104 -0
  7. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/main.py +58 -0
  8. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/scan_commands.py +56 -3
  9. {redup-0.4.28 → redup-0.4.30}/src/redup/core/models.py +9 -1
  10. {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline/__init__.py +3 -0
  11. {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline/duplicate_finder.py +19 -0
  12. {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline/groups.py +4 -0
  13. redup-0.4.30/src/redup/integrations/intract/__init__.py +4 -0
  14. redup-0.4.30/src/redup/integrations/intract/adapter.py +87 -0
  15. redup-0.4.30/src/redup/integrations/intract/policy.py +80 -0
  16. {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/handlers.py +9 -0
  17. {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/schemas.py +14 -0
  18. {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/json_reporter.py +6 -2
  19. {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/markdown_reporter.py +9 -1
  20. {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/toon_reporter.py +6 -1
  21. {redup-0.4.28 → redup-0.4.30/src/redup.egg-info}/PKG-INFO +29 -42
  22. {redup-0.4.28 → redup-0.4.30}/src/redup.egg-info/SOURCES.txt +5 -0
  23. redup-0.4.30/src/redup.egg-info/requires.txt +76 -0
  24. redup-0.4.30/tests/test_intent_integration.py +121 -0
  25. {redup-0.4.28 → redup-0.4.30}/tests/test_models.py +6 -0
  26. redup-0.4.28/src/redup.egg-info/requires.txt +0 -88
  27. {redup-0.4.28 → redup-0.4.30}/LICENSE +0 -0
  28. {redup-0.4.28 → redup-0.4.30}/setup.cfg +0 -0
  29. {redup-0.4.28 → redup-0.4.30}/src/redup/__main__.py +0 -0
  30. {redup-0.4.28 → redup-0.4.30}/src/redup/analysis_logic.py +0 -0
  31. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/__init__.py +0 -0
  32. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/compare_command.py +0 -0
  33. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/fuzzy_similarity.py +0 -0
  34. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/output_writer.py +0 -0
  35. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/quality_commands.py +0 -0
  36. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/scan_helpers.py +0 -0
  37. {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/tasks_command.py +0 -0
  38. {redup-0.4.28 → redup-0.4.30}/src/redup/config.py +0 -0
  39. {redup-0.4.28 → redup-0.4.30}/src/redup/config_handler.py +0 -0
  40. {redup-0.4.28 → redup-0.4.30}/src/redup/core/__init__.py +0 -0
  41. {redup-0.4.28 → redup-0.4.30}/src/redup/core/cache.py +0 -0
  42. {redup-0.4.28 → redup-0.4.30}/src/redup/core/community.py +0 -0
  43. {redup-0.4.28 → redup-0.4.30}/src/redup/core/comparator.py +0 -0
  44. {redup-0.4.28 → redup-0.4.30}/src/redup/core/config.py +0 -0
  45. {redup-0.4.28 → redup-0.4.30}/src/redup/core/decision.py +0 -0
  46. {redup-0.4.28 → redup-0.4.30}/src/redup/core/differ.py +0 -0
  47. {redup-0.4.28 → redup-0.4.30}/src/redup/core/fuzzy_similarity.py +0 -0
  48. {redup-0.4.28 → redup-0.4.30}/src/redup/core/grouper.py +0 -0
  49. {redup-0.4.28 → redup-0.4.30}/src/redup/core/hash_cache.py +0 -0
  50. {redup-0.4.28 → redup-0.4.30}/src/redup/core/hasher.py +0 -0
  51. {redup-0.4.28 → redup-0.4.30}/src/redup/core/lazy_grouper.py +0 -0
  52. {redup-0.4.28 → redup-0.4.30}/src/redup/core/lsh_matcher.py +0 -0
  53. {redup-0.4.28 → redup-0.4.30}/src/redup/core/matcher.py +0 -0
  54. {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline/phases.py +0 -0
  55. {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline_utils.py +0 -0
  56. {redup-0.4.28 → redup-0.4.30}/src/redup/core/planner.py +0 -0
  57. {redup-0.4.28 → redup-0.4.30}/src/redup/core/python_parser.py +0 -0
  58. {redup-0.4.28 → redup-0.4.30}/src/redup/core/refactor_advisor.py +0 -0
  59. {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner/__init__.py +0 -0
  60. {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner.py +0 -0
  61. {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_cache.py +0 -0
  62. {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_filters.py +0 -0
  63. {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_loader.py +0 -0
  64. {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_models.py +0 -0
  65. {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_types.py +0 -0
  66. {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_utils.py +0 -0
  67. {redup-0.4.28 → redup-0.4.30}/src/redup/core/semantic.py +0 -0
  68. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/__init__.py +0 -0
  69. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/config.py +0 -0
  70. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/dispatcher.py +0 -0
  71. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/__init__.py +0 -0
  72. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/base.py +0 -0
  73. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/c_family.py +0 -0
  74. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/dotnet.py +0 -0
  75. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/markup.py +0 -0
  76. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/php.py +0 -0
  77. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/query.py +0 -0
  78. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/ruby.py +0 -0
  79. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/shell.py +0 -0
  80. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/stylesheet.py +0 -0
  81. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/web.py +0 -0
  82. {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/main.py +0 -0
  83. {redup-0.4.28 → redup-0.4.30}/src/redup/core/universal_fuzzy.py +0 -0
  84. {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/__init__.py +0 -0
  85. {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/diff_helpers.py +0 -0
  86. {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/duplicate_finders.py +0 -0
  87. {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/function_extractor.py +0 -0
  88. {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/hash_utils.py +0 -0
  89. {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/language_dispatcher.py +0 -0
  90. {redup-0.4.28 → redup-0.4.30}/src/redup/integrations/__init__.py +0 -0
  91. {redup-0.4.28 → redup-0.4.30}/src/redup/integrations/planfile_integration.py +0 -0
  92. {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/__init__.py +0 -0
  93. {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/server.py +0 -0
  94. {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/utils.py +0 -0
  95. {redup-0.4.28 → redup-0.4.30}/src/redup/mcp_server.py +0 -0
  96. {redup-0.4.28 → redup-0.4.30}/src/redup/mcp_server_clean.py +0 -0
  97. {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/__init__.py +0 -0
  98. {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/code2llm_reporter.py +0 -0
  99. {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/enhanced_reporter.py +0 -0
  100. {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/yaml_reporter.py +0 -0
  101. {redup-0.4.28 → redup-0.4.30}/src/redup/reporters.py +0 -0
  102. {redup-0.4.28 → redup-0.4.30}/src/redup/utils.py +0 -0
  103. {redup-0.4.28 → redup-0.4.30}/src/redup.egg-info/dependency_links.txt +0 -0
  104. {redup-0.4.28 → redup-0.4.30}/src/redup.egg-info/entry_points.txt +0 -0
  105. {redup-0.4.28 → redup-0.4.30}/src/redup.egg-info/top_level.txt +0 -0
  106. {redup-0.4.28 → redup-0.4.30}/tests/test_cli_import_compat.py +0 -0
  107. {redup-0.4.28 → redup-0.4.30}/tests/test_compare.py +0 -0
  108. {redup-0.4.28 → redup-0.4.30}/tests/test_e2e.py +0 -0
  109. {redup-0.4.28 → redup-0.4.30}/tests/test_hasher.py +0 -0
  110. {redup-0.4.28 → redup-0.4.30}/tests/test_matcher.py +0 -0
  111. {redup-0.4.28 → redup-0.4.30}/tests/test_mcp_server.py +0 -0
  112. {redup-0.4.28 → redup-0.4.30}/tests/test_pipeline.py +0 -0
  113. {redup-0.4.28 → redup-0.4.30}/tests/test_planfile_integration.py +0 -0
  114. {redup-0.4.28 → redup-0.4.30}/tests/test_planner.py +0 -0
  115. {redup-0.4.28 → redup-0.4.30}/tests/test_quality_commands.py +0 -0
  116. {redup-0.4.28 → redup-0.4.30}/tests/test_reporters.py +0 -0
  117. {redup-0.4.28 → redup-0.4.30}/tests/test_scanner.py +0 -0
  118. {redup-0.4.28 → redup-0.4.30}/tests/test_ts_extractor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.4.28
3
+ Version: 0.4.30
4
4
  Summary: Code duplication analyzer and refactoring planner for LLMs
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Software Development :: Quality Assurance
20
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Classifier: Typing :: Typed
22
- Requires-Python: >=3.10
22
+ Requires-Python: <3.14,>=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: pyyaml>=6.0
@@ -37,10 +37,11 @@ Requires-Dist: redup[fuzzy]; extra == "all"
37
37
  Requires-Dist: redup[ast]; extra == "all"
38
38
  Requires-Dist: redup[lsh]; extra == "all"
39
39
  Requires-Dist: redup[semantic]; extra == "all"
40
+ Requires-Dist: redup[intent]; extra == "all"
40
41
  Requires-Dist: redup[compare]; extra == "all"
41
42
  Requires-Dist: redup[tasks]; extra == "all"
42
43
  Provides-Extra: tasks
43
- Requires-Dist: planfile>=0.4.0; extra == "tasks"
44
+ Requires-Dist: planfile>=0.1.0; extra == "tasks"
44
45
  Provides-Extra: compare
45
46
  Requires-Dist: networkx>=3.0; extra == "compare"
46
47
  Provides-Extra: llm
@@ -53,43 +54,29 @@ Provides-Extra: fuzzy
53
54
  Requires-Dist: rapidfuzz>=3.0; extra == "fuzzy"
54
55
  Provides-Extra: ast
55
56
  Requires-Dist: tree-sitter>=0.21; extra == "ast"
56
- Requires-Dist: tree-sitter-javascript>=0.20; extra == "ast"
57
- Requires-Dist: tree-sitter-typescript>=0.20; extra == "ast"
58
- Requires-Dist: tree-sitter-go>=0.20; extra == "ast"
59
- Requires-Dist: tree-sitter-rust>=0.20; extra == "ast"
60
- Requires-Dist: tree-sitter-java>=0.20; extra == "ast"
61
- Requires-Dist: tree-sitter-c>=0.20; extra == "ast"
62
- Requires-Dist: tree-sitter-cpp>=0.20; extra == "ast"
63
- Requires-Dist: tree-sitter-html>=0.20; extra == "ast"
64
- Requires-Dist: tree-sitter-css>=0.20; extra == "ast"
65
- Requires-Dist: tree-sitter-sql>=0.20; extra == "ast"
66
- Requires-Dist: tree-sitter-json>=0.20; extra == "ast"
67
- Requires-Dist: tree-sitter-yaml>=0.20; extra == "ast"
68
- Requires-Dist: tree-sitter-toml>=0.20; extra == "ast"
69
- Requires-Dist: tree-sitter-xml>=0.20; extra == "ast"
70
- Requires-Dist: tree-sitter-markdown>=0.20; extra == "ast"
71
- Requires-Dist: tree-sitter-bash>=0.20; extra == "ast"
72
- Requires-Dist: tree-sitter-ruby>=0.20; extra == "ast"
73
- Requires-Dist: tree-sitter-php>=0.20; extra == "ast"
74
- Requires-Dist: tree-sitter-embedded-template>=0.20; extra == "ast"
75
- Requires-Dist: tree-sitter-regex>=0.20; extra == "ast"
76
- Requires-Dist: tree-sitter-scala>=0.20; extra == "ast"
77
- Requires-Dist: tree-sitter-kotlin>=0.20; extra == "ast"
78
- Requires-Dist: tree-sitter-swift>=0.20; extra == "ast"
79
- Requires-Dist: tree-sitter-objc>=0.20; extra == "ast"
80
- Requires-Dist: tree-sitter-c-sharp>=0.20; extra == "ast"
81
- Requires-Dist: tree-sitter-lua>=0.20; extra == "ast"
82
- Requires-Dist: tree-sitter-graphql>=0.20; extra == "ast"
83
- Requires-Dist: tree-sitter-dockerfile>=0.20; extra == "ast"
84
- Requires-Dist: tree-sitter-make>=0.20; extra == "ast"
85
- Requires-Dist: tree-sitter-vim>=0.20; extra == "ast"
86
- Requires-Dist: tree-sitter-nginx>=0.20; extra == "ast"
87
- Requires-Dist: tree-sitter-svelte>=0.20; extra == "ast"
88
- Requires-Dist: tree-sitter-vue>=0.20; extra == "ast"
57
+ Requires-Dist: tree-sitter-javascript>=0.21.0; extra == "ast"
58
+ Requires-Dist: tree-sitter-typescript>=0.21.0; extra == "ast"
59
+ Requires-Dist: tree-sitter-go>=0.21.0; extra == "ast"
60
+ Requires-Dist: tree-sitter-rust>=0.21.0; extra == "ast"
61
+ Requires-Dist: tree-sitter-java>=0.21.0; extra == "ast"
62
+ Requires-Dist: tree-sitter-c>=0.21.0; extra == "ast"
63
+ Requires-Dist: tree-sitter-cpp>=0.21.0; extra == "ast"
64
+ Requires-Dist: tree-sitter-c-sharp>=0.21.0; extra == "ast"
65
+ Requires-Dist: tree-sitter-html>=0.19.0; extra == "ast"
66
+ Requires-Dist: tree-sitter-css>=0.19.0; extra == "ast"
67
+ Requires-Dist: tree-sitter-sql>=0.3.0; extra == "ast"
68
+ Requires-Dist: tree-sitter-json>=0.21.0; extra == "ast"
69
+ Requires-Dist: tree-sitter-yaml>=0.7.0; extra == "ast"
70
+ Requires-Dist: tree-sitter-bash>=0.21.0; extra == "ast"
71
+ Requires-Dist: tree-sitter-ruby>=0.21.0; extra == "ast"
72
+ Requires-Dist: tree-sitter-php>=0.21.0; extra == "ast"
73
+ Requires-Dist: tree-sitter-dockerfile>=0.2.0; extra == "ast"
89
74
  Provides-Extra: lsh
90
75
  Requires-Dist: datasketch>=1.6; extra == "lsh"
91
76
  Provides-Extra: semantic
92
77
  Requires-Dist: sentence-transformers>=2.0; extra == "semantic"
78
+ Provides-Extra: intent
79
+ Requires-Dist: intract>=0.5.6; extra == "intent"
93
80
  Provides-Extra: dev
94
81
  Requires-Dist: pytest>=7.0; extra == "dev"
95
82
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
@@ -107,18 +94,18 @@ Dynamic: license-file
107
94
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
108
95
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
109
96
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
110
- [![Version](https://img.shields.io/badge/version-0.4.28-green.svg)](https://pypi.org/project/redup/)
97
+ [![Version](https://img.shields.io/badge/version-0.4.30-green.svg)](https://pypi.org/project/redup/)
111
98
 
112
99
 
113
100
  ## AI Cost Tracking
114
101
 
115
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.4.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
116
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$33.81-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-24.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
102
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.4.30-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
103
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$31.52-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
117
104
 
118
- - 🤖 **LLM usage:** $33.8130 (71 commits)
119
- - 👤 **Human dev:** ~$2459 (24.6h @ $100/h, 30min dedup)
105
+ - 🤖 **LLM usage:** $31.5200 (74 commits)
106
+ - 👤 **Human dev:** ~$2609 (26.1h @ $100/h, 30min dedup)
120
107
 
121
- Generated on 2026-05-19 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
108
+ Generated on 2026-05-31 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
122
109
 
123
110
  ---
124
111
 
@@ -5,18 +5,18 @@
5
5
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
6
6
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
7
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
8
- [![Version](https://img.shields.io/badge/version-0.4.28-green.svg)](https://pypi.org/project/redup/)
8
+ [![Version](https://img.shields.io/badge/version-0.4.30-green.svg)](https://pypi.org/project/redup/)
9
9
 
10
10
 
11
11
  ## AI Cost Tracking
12
12
 
13
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.4.28-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
14
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$33.81-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-24.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
13
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.4.30-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
14
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$31.52-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.1h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
15
15
 
16
- - 🤖 **LLM usage:** $33.8130 (71 commits)
17
- - 👤 **Human dev:** ~$2459 (24.6h @ $100/h, 30min dedup)
16
+ - 🤖 **LLM usage:** $31.5200 (74 commits)
17
+ - 👤 **Human dev:** ~$2609 (26.1h @ $100/h, 30min dedup)
18
18
 
19
- Generated on 2026-05-19 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
19
+ Generated on 2026-05-31 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
20
20
 
21
21
  ---
22
22
 
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redup"
7
- version = "0.4.28"
7
+ version = "0.4.30"
8
8
  description = "Code duplication analyzer and refactoring planner for LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
11
- requires-python = ">=3.10"
11
+ requires-python = ">=3.10,<3.14"
12
12
  authors = [
13
13
  { name = "Tom Sapletta", email = "tom@sapletta.com" },
14
14
  ]
@@ -44,52 +44,38 @@ all = [
44
44
  "redup[ast]",
45
45
  "redup[lsh]",
46
46
  "redup[semantic]",
47
+ "redup[intent]",
47
48
  "redup[compare]",
48
49
  "redup[tasks]",
49
50
  ]
50
- tasks = ["planfile>=0.4.0"]
51
+ tasks = ["planfile>=0.1.0"]
51
52
  compare = ["networkx>=3.0"]
52
53
  llm = ["litellm>=1.0"]
53
54
  fast = ["xxhash>=3.0", "libcst>=1.0", "pybloom-live>=4.0"]
54
55
  fuzzy = ["rapidfuzz>=3.0"]
55
56
  ast = [
56
57
  "tree-sitter>=0.21",
57
- "tree-sitter-javascript>=0.20",
58
- "tree-sitter-typescript>=0.20",
59
- "tree-sitter-go>=0.20",
60
- "tree-sitter-rust>=0.20",
61
- "tree-sitter-java>=0.20",
62
- "tree-sitter-c>=0.20",
63
- "tree-sitter-cpp>=0.20",
64
- "tree-sitter-html>=0.20",
65
- "tree-sitter-css>=0.20",
66
- "tree-sitter-sql>=0.20",
67
- "tree-sitter-json>=0.20",
68
- "tree-sitter-yaml>=0.20",
69
- "tree-sitter-toml>=0.20",
70
- "tree-sitter-xml>=0.20",
71
- "tree-sitter-markdown>=0.20",
72
- "tree-sitter-bash>=0.20",
73
- "tree-sitter-ruby>=0.20",
74
- "tree-sitter-php>=0.20",
75
- "tree-sitter-embedded-template>=0.20",
76
- "tree-sitter-regex>=0.20",
77
- "tree-sitter-scala>=0.20",
78
- "tree-sitter-kotlin>=0.20",
79
- "tree-sitter-swift>=0.20",
80
- "tree-sitter-objc>=0.20",
81
- "tree-sitter-c-sharp>=0.20",
82
- "tree-sitter-lua>=0.20",
83
- "tree-sitter-graphql>=0.20",
84
- "tree-sitter-dockerfile>=0.20",
85
- "tree-sitter-make>=0.20",
86
- "tree-sitter-vim>=0.20",
87
- "tree-sitter-nginx>=0.20",
88
- "tree-sitter-svelte>=0.20",
89
- "tree-sitter-vue>=0.20",
58
+ "tree-sitter-javascript>=0.21.0",
59
+ "tree-sitter-typescript>=0.21.0",
60
+ "tree-sitter-go>=0.21.0",
61
+ "tree-sitter-rust>=0.21.0",
62
+ "tree-sitter-java>=0.21.0",
63
+ "tree-sitter-c>=0.21.0",
64
+ "tree-sitter-cpp>=0.21.0",
65
+ "tree-sitter-c-sharp>=0.21.0",
66
+ "tree-sitter-html>=0.19.0",
67
+ "tree-sitter-css>=0.19.0",
68
+ "tree-sitter-sql>=0.3.0",
69
+ "tree-sitter-json>=0.21.0",
70
+ "tree-sitter-yaml>=0.7.0",
71
+ "tree-sitter-bash>=0.21.0",
72
+ "tree-sitter-ruby>=0.21.0",
73
+ "tree-sitter-php>=0.21.0",
74
+ "tree-sitter-dockerfile>=0.2.0",
90
75
  ]
91
76
  lsh = ["datasketch>=1.6"]
92
77
  semantic = ["sentence-transformers>=2.0"]
78
+ intent = ["intract>=0.5.6"]
93
79
  dev = [
94
80
  "pytest>=7.0",
95
81
  "pytest-cov>=4.0",
@@ -113,6 +99,9 @@ Documentation = "https://github.com/semcod/redup#readme"
113
99
  [tool.setuptools.packages.find]
114
100
  where = ["src"]
115
101
 
102
+ [tool.uv.sources]
103
+ intract = { path = "../intract", editable = true }
104
+
116
105
  [tool.ruff]
117
106
  target-version = "py310"
118
107
  line-length = 100
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.4.28"
5
+ __version__ = "0.4.30"
6
6
 
7
7
  # Click compatibility shim for older typer versions
8
8
  # This must run before any typer imports
@@ -29,6 +29,11 @@ def build_config_with_file_support(
29
29
  functions_only: bool = False,
30
30
  fuzzy: bool = False,
31
31
  fuzzy_threshold: float = 0.8,
32
+ intent: bool = False,
33
+ intent_threshold: float = 0.84,
34
+ intent_manifest: str | None = None,
35
+ intent_fail_on: str | None = None,
36
+ intent_warn_on: str | None = None,
32
37
  target_files: list[str] | None = None,
33
38
  ) -> ScanConfig:
34
39
  """Build scan configuration with advanced options."""
@@ -65,4 +70,13 @@ def build_config_with_file_support(
65
70
  scan_config.fuzzy_enabled = fuzzy
66
71
  scan_config.fuzzy_threshold = fuzzy_threshold
67
72
 
73
+ scan_config.intent_enabled = intent
74
+ scan_config.intent_threshold = intent_threshold
75
+ if intent_manifest:
76
+ scan_config.intent_manifest_path = Path(intent_manifest)
77
+ if intent_fail_on:
78
+ scan_config.intent_fail_on = intent_fail_on
79
+ if intent_warn_on:
80
+ scan_config.intent_warn_on = intent_warn_on
81
+
68
82
  return scan_config
@@ -0,0 +1,104 @@
1
+ """reDUP intract CLI command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+
11
+ from redup.core.models import ScanConfig
12
+ from redup.integrations.intract.policy import run_intract_policy_check
13
+
14
+
15
+ def intract_command(
16
+ path: Path = typer.Argument(Path("."), help="Project root to validate."),
17
+ manifest: Optional[Path] = typer.Option(None, "--manifest", help="Path to intract.yaml / intent.yaml."),
18
+ intent: bool = typer.Option(True, "--intent/--no-intent", help="Include intent duplicate groups in policy."),
19
+ intent_threshold: float = typer.Option(0.84, "--intent-threshold", help="Intent duplicate threshold."),
20
+ fail_on: Optional[str] = typer.Option(
21
+ None,
22
+ "--fail-on",
23
+ help="Comma-separated fail tokens (violation,intent_duplicate,missing_required_p1,...).",
24
+ ),
25
+ warn_on: Optional[str] = typer.Option(
26
+ None,
27
+ "--warn-on",
28
+ help="Comma-separated warn tokens (partial,unknown,intent_duplicate,...).",
29
+ ),
30
+ output_format: str = typer.Option("text", "--format", "-f", help="Output format: text|json"),
31
+ exit_on_fail: bool = typer.Option(True, "--exit/--no-exit", help="Exit non-zero on policy failure."),
32
+ ) -> None:
33
+ """Validate Intract contracts and optional intent duplicate policy for a project."""
34
+ from intract.integrations.redup import parse_policy_tokens
35
+
36
+ config = ScanConfig(
37
+ root=path,
38
+ intent_enabled=intent,
39
+ intent_threshold=intent_threshold,
40
+ intent_manifest_path=manifest,
41
+ )
42
+ if fail_on:
43
+ config.intent_fail_on = fail_on
44
+ if warn_on:
45
+ config.intent_warn_on = warn_on
46
+
47
+ blocks = None
48
+ if intent:
49
+ from redup.core.pipeline.phases import process_blocks, scan_phase
50
+
51
+ scanned_files, _stats = scan_phase(config)
52
+ blocks = process_blocks(scanned_files, config.functions_only)
53
+
54
+ try:
55
+ result = run_intract_policy_check(
56
+ path,
57
+ config,
58
+ blocks=blocks,
59
+ fail_on=parse_policy_tokens(fail_on) or None,
60
+ warn_on=parse_policy_tokens(warn_on) or None,
61
+ )
62
+ except ImportError:
63
+ typer.echo(
64
+ "Error: Intract is not installed. Install with: pip install 'redup[intent]'",
65
+ err=True,
66
+ )
67
+ raise typer.Exit(1) from None
68
+
69
+ if output_format == "json":
70
+ typer.echo(json.dumps(result.to_dict(), ensure_ascii=False, indent=2))
71
+ else:
72
+ lines = [
73
+ "REDUP INTRACT CHECK",
74
+ "",
75
+ f"Validation status: {result.validation_status}",
76
+ f"Intent duplicate groups: {result.intent_duplicate_groups}",
77
+ ]
78
+ if result.reasons:
79
+ lines.extend(["", "FAIL REASONS:"])
80
+ lines.extend(f"- {item}" for item in result.reasons)
81
+ if result.warnings:
82
+ lines.extend(["", "WARNINGS:"])
83
+ lines.extend(f"- {item}" for item in result.warnings)
84
+ typer.echo("\n".join(lines))
85
+
86
+ if exit_on_fail and result.should_fail:
87
+ raise typer.Exit(1)
88
+
89
+
90
+ def apply_scan_intent_policy(path: Path, config: ScanConfig, dup_map) -> "RedupPolicyResult":
91
+ from intract.integrations.redup import validate_for_redup
92
+ from redup.integrations.intract.adapter import _resolve_manifest_path
93
+ from redup.integrations.intract.policy import intent_groups_from_dup_map, resolve_intract_policy
94
+
95
+ resolved_fail_on, resolved_warn_on = resolve_intract_policy(config)
96
+ manifest = _resolve_manifest_path(config)
97
+ intent_groups = intent_groups_from_dup_map(dup_map)
98
+ return validate_for_redup(
99
+ path,
100
+ manifest=manifest,
101
+ intent_groups=intent_groups,
102
+ fail_on=resolved_fail_on,
103
+ warn_on=resolved_warn_on,
104
+ )
@@ -21,6 +21,7 @@ from redup.cli_app.scan_commands import ( # noqa: E402
21
21
  info_command,
22
22
  scan_command,
23
23
  )
24
+ from redup.cli_app.intract_commands import intract_command # noqa: E402
24
25
 
25
26
  app = typer.Typer(
26
27
  name="redup",
@@ -129,6 +130,31 @@ def scan(
129
130
  "--fuzzy-threshold",
130
131
  help="Fuzzy similarity threshold (0.0-1.0).",
131
132
  ),
133
+ intent: bool = typer.Option(
134
+ False,
135
+ "--intent",
136
+ help="Enable Intract intent duplicate detection (requires reDUP[intent]).",
137
+ ),
138
+ intent_threshold: float = typer.Option(
139
+ 0.84,
140
+ "--intent-threshold",
141
+ help="Intent contract similarity threshold (0.0-1.0).",
142
+ ),
143
+ intent_manifest: str | None = typer.Option(
144
+ None,
145
+ "--intent-manifest",
146
+ help="Optional intent.yaml / intract.yaml manifest path.",
147
+ ),
148
+ intent_fail_on: str | None = typer.Option(
149
+ None,
150
+ "--intent-fail-on",
151
+ help="Comma-separated Intract policy fail tokens for --intent scans.",
152
+ ),
153
+ intent_warn_on: str | None = typer.Option(
154
+ None,
155
+ "--intent-warn-on",
156
+ help="Comma-separated Intract policy warn tokens for --intent scans.",
157
+ ),
132
158
  ) -> None:
133
159
  """Scan a project for code duplicates."""
134
160
  return scan_command(
@@ -150,6 +176,38 @@ def scan(
150
176
  include_untracked,
151
177
  fuzzy,
152
178
  fuzzy_threshold,
179
+ intent,
180
+ intent_threshold,
181
+ intent_manifest,
182
+ intent_fail_on,
183
+ intent_warn_on,
184
+ )
185
+
186
+
187
+ @app.command()
188
+ def intract(
189
+ path: Path = typer.Argument(DEFAULT_PATH, help="Project root to validate."),
190
+ manifest: Path | None = typer.Option(None, "--manifest", help="Path to intract.yaml / intent.yaml."),
191
+ intent: bool = typer.Option(True, "--intent/--no-intent", help="Include intent duplicate groups."),
192
+ intent_threshold: float = typer.Option(0.84, "--intent-threshold"),
193
+ fail_on: str | None = typer.Option(
194
+ "violation,missing_required_p1,invalid_manifest,intent_duplicate",
195
+ "--fail-on",
196
+ ),
197
+ warn_on: str | None = typer.Option("partial,unknown", "--warn-on"),
198
+ format: str = typer.Option("text", "--format", "-f", help="text|json"),
199
+ exit_on_fail: bool = typer.Option(True, "--exit/--no-exit"),
200
+ ) -> None:
201
+ """Validate Intract contracts and intent duplicate policy."""
202
+ return intract_command(
203
+ path,
204
+ manifest,
205
+ intent,
206
+ intent_threshold,
207
+ fail_on,
208
+ warn_on,
209
+ format,
210
+ exit_on_fail,
153
211
  )
154
212
 
155
213
 
@@ -132,6 +132,31 @@ def scan_command(
132
132
  fuzzy_threshold: float = typer.Option(
133
133
  0.8, "--fuzzy-threshold", help="Fuzzy similarity threshold (0.0-1.0)."
134
134
  ),
135
+ intent: bool = typer.Option(
136
+ False,
137
+ "--intent",
138
+ help="Enable Intract intent duplicate detection (requires reDUP[intent]).",
139
+ ),
140
+ intent_threshold: float = typer.Option(
141
+ 0.84,
142
+ "--intent-threshold",
143
+ help="Intent contract similarity threshold (0.0-1.0).",
144
+ ),
145
+ intent_manifest: str | None = typer.Option(
146
+ None,
147
+ "--intent-manifest",
148
+ help="Optional intent.yaml / intract.yaml manifest path.",
149
+ ),
150
+ intent_fail_on: str | None = typer.Option(
151
+ None,
152
+ "--intent-fail-on",
153
+ help="Comma-separated Intract policy fail tokens for --intent scans.",
154
+ ),
155
+ intent_warn_on: str | None = typer.Option(
156
+ None,
157
+ "--intent-warn-on",
158
+ help="Comma-separated Intract policy warn tokens for --intent scans.",
159
+ ),
135
160
  ) -> None:
136
161
  """Scan a project for code duplicates."""
137
162
 
@@ -157,6 +182,7 @@ def scan_command(
157
182
  max_cache_mb,
158
183
  changed_only,
159
184
  fuzzy,
185
+ intent,
160
186
  ]
161
187
  ):
162
188
  config = build_config_with_file_support(
@@ -173,6 +199,11 @@ def scan_command(
173
199
  functions_only,
174
200
  fuzzy,
175
201
  fuzzy_threshold,
202
+ intent,
203
+ intent_threshold,
204
+ intent_manifest,
205
+ intent_fail_on,
206
+ intent_warn_on,
176
207
  target_files,
177
208
  )
178
209
  else:
@@ -201,6 +232,26 @@ def scan_command(
201
232
 
202
233
  write_results(dup_map, format, output, path)
203
234
 
235
+ if intent:
236
+ from redup.cli_app.intract_commands import apply_scan_intent_policy
237
+
238
+ try:
239
+ policy = apply_scan_intent_policy(path, config, dup_map)
240
+ except ImportError:
241
+ typer.echo("WARN: Intract policy check skipped (install redup[intent])", err=True)
242
+ return
243
+
244
+ if policy.warnings:
245
+ typer.echo("\nIntract warnings:")
246
+ for item in policy.warnings:
247
+ typer.echo(f" - {item}")
248
+
249
+ if policy.should_fail:
250
+ typer.echo("\nIntract policy failed:", err=True)
251
+ for item in policy.reasons:
252
+ typer.echo(f" - {item}", err=True)
253
+ raise typer.Exit(1)
254
+
204
255
 
205
256
  def diff_command(before: Path, after: Path) -> None:
206
257
  """Compare two reDUP analysis results."""
@@ -249,9 +300,11 @@ def check_command(
249
300
 
250
301
  # Show top duplicate groups
251
302
  if dup_map.groups:
252
- typer.echo(f"\n🎯 Top {min(max_groups, len(dup_map.groups))} duplicate groups:")
253
- for i, group in enumerate(dup_map.sorted_by_impact()[:max_groups]):
254
- if group.saved_lines_potential <= max_saved_lines:
303
+ _max_groups = max_groups if max_groups is not None else 10
304
+ _max_saved = max_saved_lines if max_saved_lines is not None else 100
305
+ typer.echo(f"\n🎯 Top {min(_max_groups, len(dup_map.groups))} duplicate groups:")
306
+ for i, group in enumerate(dup_map.sorted_by_impact()[:_max_groups]):
307
+ if group.saved_lines_potential <= _max_saved:
255
308
  break
256
309
  typer.echo(
257
310
  f" {i + 1}. {group.id}: {group.occurrences} occurrences, {group.saved_lines_potential} lines recoverable"
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from dataclasses import dataclass, field
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
 
9
10
 
10
11
  class DuplicateType(str, Enum):
@@ -14,7 +15,8 @@ class DuplicateType(str, Enum):
14
15
  STRUCTURAL = "structural"
15
16
  FUZZY = "fuzzy"
16
17
  NEAR_DUPLICATE = "near_duplicate"
17
- SEMANTIC = "semantic" # ← NOWY
18
+ SEMANTIC = "semantic"
19
+ INTENT = "intent"
18
20
 
19
21
 
20
22
  class RefactorAction(str, Enum):
@@ -183,6 +185,11 @@ class ScanConfig:
183
185
  functions_only: bool = True
184
186
  fuzzy_enabled: bool = False
185
187
  fuzzy_threshold: float = 0.8
188
+ intent_enabled: bool = False
189
+ intent_threshold: float = 0.84
190
+ intent_manifest_path: Path | None = None
191
+ intent_fail_on: str = "violation,missing_required_p1,invalid_manifest"
192
+ intent_warn_on: str = "partial,unknown"
186
193
  # LSH configuration
187
194
  lsh_enabled: bool = True
188
195
  lsh_min_lines: int = 50
@@ -219,6 +226,7 @@ class DuplicateGroup:
219
226
  similarity_score: float = 1.0
220
227
  normalized_hash: str = ""
221
228
  normalized_name: str | None = None
229
+ metadata: dict[str, Any] = field(default_factory=dict)
222
230
 
223
231
  @property
224
232
  def occurrences(self) -> int:
@@ -13,6 +13,7 @@ from redup.core.pipeline.duplicate_finder import (
13
13
  find_duplicates_phase_lazy,
14
14
  find_duplicates_phase_optimized,
15
15
  find_exact_groups,
16
+ find_intent_groups,
16
17
  find_near_duplicate_groups,
17
18
  find_semantic_groups,
18
19
  find_structural_groups,
@@ -46,6 +47,7 @@ _match_results_to_blocks = match_results_to_blocks
46
47
  _calculate_similarity = calculate_similarity
47
48
  _find_near_duplicate_groups = find_near_duplicate_groups
48
49
  _find_semantic_groups = find_semantic_groups
50
+ _find_intent_groups = find_intent_groups
49
51
  _blocks_to_group = blocks_to_group
50
52
 
51
53
 
@@ -269,6 +271,7 @@ __all__ = [
269
271
  "find_structural_groups",
270
272
  "find_near_duplicate_groups",
271
273
  "find_semantic_groups",
274
+ "find_intent_groups",
272
275
  # Groups
273
276
  "blocks_to_group",
274
277
  "deduplicate_groups",
@@ -26,6 +26,8 @@ def _finalize_duplicate_groups(
26
26
  ) -> list[DuplicateGroup]:
27
27
  """Attach near duplicates, sort by impact, and report timing."""
28
28
  groups.extend(find_near_duplicate_groups(all_blocks, config))
29
+ if getattr(config, "intent_enabled", False):
30
+ groups.extend(find_intent_groups(all_blocks, config))
29
31
  groups.sort(key=lambda g: g.impact_score, reverse=True)
30
32
 
31
33
  processing_time = (time.time() - start_time) * 1000
@@ -214,6 +216,23 @@ def find_semantic_groups(blocks: list[CodeBlock], threshold: float = 0.80) -> li
214
216
  return groups
215
217
 
216
218
 
219
+ def find_intent_groups(blocks: list[CodeBlock], config: ScanConfig) -> list[DuplicateGroup]:
220
+ """Find duplicate intent contracts via Intract."""
221
+ if not getattr(config, "intent_enabled", False):
222
+ return []
223
+
224
+ try:
225
+ from redup.integrations.intract.adapter import detect_intent_duplicates
226
+ except ImportError:
227
+ return []
228
+
229
+ try:
230
+ return detect_intent_duplicates(blocks, config)
231
+ except RuntimeError as exc:
232
+ print(f"⚠️ {exc}")
233
+ return []
234
+
235
+
217
236
  def find_duplicates_phase_optimized(
218
237
  all_blocks: list[CodeBlock], config: ScanConfig
219
238
  ) -> list[DuplicateGroup]:
@@ -64,6 +64,10 @@ def deduplicate_groups(groups: list[DuplicateGroup]) -> list[DuplicateGroup]:
64
64
  if group.occurrences < 2:
65
65
  continue
66
66
 
67
+ if group.duplicate_type == DuplicateType.INTENT:
68
+ kept.append(group)
69
+ continue
70
+
67
71
  # Check if this group's fragments are already covered
68
72
  locations = {(f.file, f.line_start, f.line_end) for f in group.fragments}
69
73
  new_locations = locations - seen_locations
@@ -0,0 +1,4 @@
1
+ from .adapter import detect_intent_duplicates
2
+ from .policy import run_intract_policy_check
3
+
4
+ __all__ = ["detect_intent_duplicates", "run_intract_policy_check"]