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.
- {redup-0.4.28/src/redup.egg-info → redup-0.4.30}/PKG-INFO +29 -42
- {redup-0.4.28 → redup-0.4.30}/README.md +6 -6
- {redup-0.4.28 → redup-0.4.30}/pyproject.toml +25 -36
- {redup-0.4.28 → redup-0.4.30}/src/redup/__init__.py +1 -1
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/config_builder.py +14 -0
- redup-0.4.30/src/redup/cli_app/intract_commands.py +104 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/main.py +58 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/scan_commands.py +56 -3
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/models.py +9 -1
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline/__init__.py +3 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline/duplicate_finder.py +19 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline/groups.py +4 -0
- redup-0.4.30/src/redup/integrations/intract/__init__.py +4 -0
- redup-0.4.30/src/redup/integrations/intract/adapter.py +87 -0
- redup-0.4.30/src/redup/integrations/intract/policy.py +80 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/handlers.py +9 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/schemas.py +14 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/json_reporter.py +6 -2
- {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/markdown_reporter.py +9 -1
- {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/toon_reporter.py +6 -1
- {redup-0.4.28 → redup-0.4.30/src/redup.egg-info}/PKG-INFO +29 -42
- {redup-0.4.28 → redup-0.4.30}/src/redup.egg-info/SOURCES.txt +5 -0
- redup-0.4.30/src/redup.egg-info/requires.txt +76 -0
- redup-0.4.30/tests/test_intent_integration.py +121 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_models.py +6 -0
- redup-0.4.28/src/redup.egg-info/requires.txt +0 -88
- {redup-0.4.28 → redup-0.4.30}/LICENSE +0 -0
- {redup-0.4.28 → redup-0.4.30}/setup.cfg +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/__main__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/analysis_logic.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/__init__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/compare_command.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/fuzzy_similarity.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/output_writer.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/quality_commands.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/scan_helpers.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/cli_app/tasks_command.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/config.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/config_handler.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/__init__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/cache.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/community.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/comparator.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/config.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/decision.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/differ.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/fuzzy_similarity.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/grouper.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/hash_cache.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/hasher.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/lazy_grouper.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/lsh_matcher.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/matcher.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline/phases.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/pipeline_utils.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/planner.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/python_parser.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/refactor_advisor.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner/__init__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_cache.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_filters.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_loader.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_models.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_types.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/scanner_utils.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/semantic.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/__init__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/config.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/dispatcher.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/__init__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/base.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/c_family.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/dotnet.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/markup.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/php.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/query.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/ruby.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/shell.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/stylesheet.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/extractors/web.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/ts_extractor/main.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/universal_fuzzy.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/__init__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/diff_helpers.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/duplicate_finders.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/function_extractor.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/hash_utils.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/core/utils/language_dispatcher.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/integrations/__init__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/integrations/planfile_integration.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/__init__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/server.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/mcp/utils.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/mcp_server.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/mcp_server_clean.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/__init__.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/code2llm_reporter.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/enhanced_reporter.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/reporters/yaml_reporter.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/reporters.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup/utils.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup.egg-info/dependency_links.txt +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup.egg-info/entry_points.txt +0 -0
- {redup-0.4.28 → redup-0.4.30}/src/redup.egg-info/top_level.txt +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_cli_import_compat.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_compare.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_e2e.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_hasher.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_matcher.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_mcp_server.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_pipeline.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_planfile_integration.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_planner.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_quality_commands.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_reporters.py +0 -0
- {redup-0.4.28 → redup-0.4.30}/tests/test_scanner.py +0 -0
- {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.
|
|
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:
|
|
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.
|
|
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.
|
|
57
|
-
Requires-Dist: tree-sitter-typescript>=0.
|
|
58
|
-
Requires-Dist: tree-sitter-go>=0.
|
|
59
|
-
Requires-Dist: tree-sitter-rust>=0.
|
|
60
|
-
Requires-Dist: tree-sitter-java>=0.
|
|
61
|
-
Requires-Dist: tree-sitter-c>=0.
|
|
62
|
-
Requires-Dist: tree-sitter-cpp>=0.
|
|
63
|
-
Requires-Dist: tree-sitter-
|
|
64
|
-
Requires-Dist: tree-sitter-
|
|
65
|
-
Requires-Dist: tree-sitter-
|
|
66
|
-
Requires-Dist: tree-sitter-
|
|
67
|
-
Requires-Dist: tree-sitter-
|
|
68
|
-
Requires-Dist: tree-sitter-
|
|
69
|
-
Requires-Dist: tree-sitter-
|
|
70
|
-
Requires-Dist: tree-sitter-
|
|
71
|
-
Requires-Dist: tree-sitter-
|
|
72
|
-
Requires-Dist: tree-sitter-
|
|
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
|
[](https://pypi.org/project/redup/)
|
|
108
95
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
109
96
|
[](https://python.org)
|
|
110
|
-
[](https://pypi.org/project/redup/)
|
|
111
98
|
|
|
112
99
|
|
|
113
100
|
## AI Cost Tracking
|
|
114
101
|
|
|
115
|
-
    
|
|
103
|
+
  
|
|
117
104
|
|
|
118
|
-
- 🤖 **LLM usage:** $
|
|
119
|
-
- 👤 **Human dev:** ~$
|
|
105
|
+
- 🤖 **LLM usage:** $31.5200 (74 commits)
|
|
106
|
+
- 👤 **Human dev:** ~$2609 (26.1h @ $100/h, 30min dedup)
|
|
120
107
|
|
|
121
|
-
Generated on 2026-05-
|
|
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
|
[](https://pypi.org/project/redup/)
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[](https://python.org)
|
|
8
|
-
[](https://pypi.org/project/redup/)
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
## AI Cost Tracking
|
|
12
12
|
|
|
13
|
-
    
|
|
14
|
+
  
|
|
15
15
|
|
|
16
|
-
- 🤖 **LLM usage:** $
|
|
17
|
-
- 👤 **Human dev:** ~$
|
|
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
|
+
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.
|
|
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.
|
|
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.
|
|
58
|
-
"tree-sitter-typescript>=0.
|
|
59
|
-
"tree-sitter-go>=0.
|
|
60
|
-
"tree-sitter-rust>=0.
|
|
61
|
-
"tree-sitter-java>=0.
|
|
62
|
-
"tree-sitter-c>=0.
|
|
63
|
-
"tree-sitter-cpp>=0.
|
|
64
|
-
"tree-sitter-
|
|
65
|
-
"tree-sitter-
|
|
66
|
-
"tree-sitter-
|
|
67
|
-
"tree-sitter-
|
|
68
|
-
"tree-sitter-
|
|
69
|
-
"tree-sitter-
|
|
70
|
-
"tree-sitter-
|
|
71
|
-
"tree-sitter-
|
|
72
|
-
"tree-sitter-
|
|
73
|
-
"tree-sitter-
|
|
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
|
|
@@ -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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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"
|
|
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
|