redup 0.2.4__tar.gz → 0.3.2__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 (41) hide show
  1. {redup-0.2.4 → redup-0.3.2}/PKG-INFO +76 -6
  2. {redup-0.2.4 → redup-0.3.2}/README.md +57 -5
  3. {redup-0.2.4 → redup-0.3.2}/pyproject.toml +19 -1
  4. {redup-0.2.4 → redup-0.3.2}/src/redup/__init__.py +1 -1
  5. {redup-0.2.4 → redup-0.3.2}/src/redup/cli_app/main.py +83 -50
  6. {redup-0.2.4 → redup-0.3.2}/src/redup/core/config.py +3 -3
  7. redup-0.3.2/src/redup/core/parallel_scanner.py +185 -0
  8. {redup-0.2.4 → redup-0.3.2}/src/redup/core/pipeline.py +60 -0
  9. {redup-0.2.4 → redup-0.3.2}/src/redup/core/ts_extractor.py +151 -11
  10. {redup-0.2.4 → redup-0.3.2}/src/redup/reporters/json_reporter.py +20 -13
  11. {redup-0.2.4 → redup-0.3.2}/src/redup.egg-info/PKG-INFO +76 -6
  12. {redup-0.2.4 → redup-0.3.2}/src/redup.egg-info/SOURCES.txt +1 -0
  13. redup-0.3.2/src/redup.egg-info/requires.txt +49 -0
  14. redup-0.2.4/src/redup.egg-info/requires.txt +0 -31
  15. {redup-0.2.4 → redup-0.3.2}/LICENSE +0 -0
  16. {redup-0.2.4 → redup-0.3.2}/setup.cfg +0 -0
  17. {redup-0.2.4 → redup-0.3.2}/src/redup/__main__.py +0 -0
  18. {redup-0.2.4 → redup-0.3.2}/src/redup/cli_app/__init__.py +0 -0
  19. {redup-0.2.4 → redup-0.3.2}/src/redup/core/__init__.py +0 -0
  20. {redup-0.2.4 → redup-0.3.2}/src/redup/core/differ.py +0 -0
  21. {redup-0.2.4 → redup-0.3.2}/src/redup/core/hasher.py +0 -0
  22. {redup-0.2.4 → redup-0.3.2}/src/redup/core/lsh_matcher.py +0 -0
  23. {redup-0.2.4 → redup-0.3.2}/src/redup/core/matcher.py +0 -0
  24. {redup-0.2.4 → redup-0.3.2}/src/redup/core/models.py +0 -0
  25. {redup-0.2.4 → redup-0.3.2}/src/redup/core/planner.py +0 -0
  26. {redup-0.2.4 → redup-0.3.2}/src/redup/core/scanner.py +0 -0
  27. {redup-0.2.4 → redup-0.3.2}/src/redup/reporters/__init__.py +0 -0
  28. {redup-0.2.4 → redup-0.3.2}/src/redup/reporters/markdown_reporter.py +0 -0
  29. {redup-0.2.4 → redup-0.3.2}/src/redup/reporters/toon_reporter.py +0 -0
  30. {redup-0.2.4 → redup-0.3.2}/src/redup/reporters/yaml_reporter.py +0 -0
  31. {redup-0.2.4 → redup-0.3.2}/src/redup.egg-info/dependency_links.txt +0 -0
  32. {redup-0.2.4 → redup-0.3.2}/src/redup.egg-info/entry_points.txt +0 -0
  33. {redup-0.2.4 → redup-0.3.2}/src/redup.egg-info/top_level.txt +0 -0
  34. {redup-0.2.4 → redup-0.3.2}/tests/test_e2e.py +0 -0
  35. {redup-0.2.4 → redup-0.3.2}/tests/test_hasher.py +0 -0
  36. {redup-0.2.4 → redup-0.3.2}/tests/test_matcher.py +0 -0
  37. {redup-0.2.4 → redup-0.3.2}/tests/test_models.py +0 -0
  38. {redup-0.2.4 → redup-0.3.2}/tests/test_pipeline.py +0 -0
  39. {redup-0.2.4 → redup-0.3.2}/tests/test_planner.py +0 -0
  40. {redup-0.2.4 → redup-0.3.2}/tests/test_reporters.py +0 -0
  41. {redup-0.2.4 → redup-0.3.2}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redup
3
- Version: 0.2.4
3
+ Version: 0.3.2
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
@@ -41,6 +41,24 @@ Requires-Dist: tree-sitter-rust>=0.20; extra == "ast"
41
41
  Requires-Dist: tree-sitter-java>=0.20; extra == "ast"
42
42
  Requires-Dist: tree-sitter-c>=0.20; extra == "ast"
43
43
  Requires-Dist: tree-sitter-cpp>=0.20; extra == "ast"
44
+ Requires-Dist: tree-sitter-html>=0.20; extra == "ast"
45
+ Requires-Dist: tree-sitter-css>=0.20; extra == "ast"
46
+ Requires-Dist: tree-sitter-sql>=0.20; extra == "ast"
47
+ Requires-Dist: tree-sitter-json>=0.20; extra == "ast"
48
+ Requires-Dist: tree-sitter-yaml>=0.20; extra == "ast"
49
+ Requires-Dist: tree-sitter-toml>=0.20; extra == "ast"
50
+ Requires-Dist: tree-sitter-xml>=0.20; extra == "ast"
51
+ Requires-Dist: tree-sitter-markdown>=0.20; extra == "ast"
52
+ Requires-Dist: tree-sitter-bash>=0.20; extra == "ast"
53
+ Requires-Dist: tree-sitter-ruby>=0.20; extra == "ast"
54
+ Requires-Dist: tree-sitter-php>=0.20; extra == "ast"
55
+ Requires-Dist: tree-sitter-embedded-template>=0.20; extra == "ast"
56
+ Requires-Dist: tree-sitter-regex>=0.20; extra == "ast"
57
+ Requires-Dist: tree-sitter-scala>=0.20; extra == "ast"
58
+ Requires-Dist: tree-sitter-kotlin>=0.20; extra == "ast"
59
+ Requires-Dist: tree-sitter-swift>=0.20; extra == "ast"
60
+ Requires-Dist: tree-sitter-objc>=0.20; extra == "ast"
61
+ Requires-Dist: tree-sitter-c-sharp>=0.20; extra == "ast"
44
62
  Provides-Extra: lsh
45
63
  Requires-Dist: datasketch>=1.6; extra == "lsh"
46
64
  Provides-Extra: dev
@@ -57,7 +75,7 @@ Dynamic: license-file
57
75
  [![PyPI](https://img.shields.io/pypi/v/redup)](https://pypi.org/project/redup/)
58
76
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
59
77
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://python.org)
60
- [![Version](https://img.shields.io/badge/version-0.2.4-green.svg)](https://pypi.org/project/redup/)
78
+ [![Version](https://img.shields.io/badge/version-0.3.2-green.svg)](https://pypi.org/project/redup/)
61
79
 
62
80
  reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
63
81
 
@@ -65,14 +83,18 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
65
83
 
66
84
  - **Exact duplicate detection** via SHA-256 block hashing
67
85
  - **Structural clone detection** — same AST shape, different variable names
86
+ - **LSH near-duplicate detection** for large code blocks (>50 lines)
87
+ - **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C++
88
+ - **Parallel scanning** for large projects (2x+ performance improvement)
68
89
  - **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
69
- - **Function-level analysis** using Python AST extraction
90
+ - **Function-level analysis** using Python AST and tree-sitter extraction
70
91
  - **Impact scoring** — prioritizes duplicates by `saved_lines × similarity`
71
92
  - **Refactoring planner** — generates concrete extract/inline suggestions
72
- - **Three output formats**: JSON (tooling), YAML (humans), TOON (LLMs)
73
- - **CLI** with `typer` + `rich` for interactive use
93
+ - **Multiple output formats**: JSON, YAML, TOON, Markdown
94
+ - **Configuration system** TOML files and environment variables
95
+ - **CLI commands**: `scan`, `diff`, `check`, `config`, `info`
96
+ - **CI integration** with configurable quality gates
74
97
  - **Clean output** — no syntax warnings from external libraries
75
- - **Optimized performance** — reduced complexity and improved maintainability
76
98
 
77
99
  ## Installation
78
100
 
@@ -100,6 +122,23 @@ redup scan .
100
122
  # Scan with JSON output saved to file
101
123
  redup scan ./src --format json --output ./reports/
102
124
 
125
+ # Parallel scanning for large projects
126
+ redup scan . --parallel --max-workers 4
127
+
128
+ # Multi-language scanning
129
+ redup scan . --ext ".py,.js,.ts,.go"
130
+
131
+ # CI gate with thresholds
132
+ redup check . --max-groups 10 --max-lines 100
133
+
134
+ # Compare two scans
135
+ redup diff before.json after.json
136
+
137
+ # Initialize configuration
138
+ redup config --init
139
+ ```
140
+
141
+ ```bash
103
142
  # Scan with all formats
104
143
  redup scan . --format all --output ./redup_output/
105
144
 
@@ -113,6 +152,37 @@ redup scan . --min-lines 5 --min-sim 0.9
113
152
  redup info
114
153
  ```
115
154
 
155
+ ### Configuration
156
+
157
+ Create a `redup.toml` file:
158
+
159
+ ```toml
160
+ [scan]
161
+ extensions = ".py,.js,.ts,.go,.rs,.java"
162
+ min_lines = 3
163
+ min_similarity = 0.85
164
+ include_tests = false
165
+
166
+ [lsh]
167
+ enabled = true
168
+ min_lines = 50
169
+ threshold = 0.8
170
+
171
+ [check]
172
+ max_groups = 10
173
+ max_lines = 100
174
+
175
+ [output]
176
+ format = "toon"
177
+ output = "redup_output"
178
+
179
+ [reporting]
180
+ include_snippets = true
181
+ generate_suggestions = true
182
+ ```
183
+
184
+ Or use `[tool.redup]` in `pyproject.toml`. Environment variables with `REDUP_` prefix override file settings.
185
+
116
186
  ### Python API
117
187
 
118
188
  ```python
@@ -5,7 +5,7 @@
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.2.4-green.svg)](https://pypi.org/project/redup/)
8
+ [![Version](https://img.shields.io/badge/version-0.3.2-green.svg)](https://pypi.org/project/redup/)
9
9
 
10
10
  reDUP scans codebases for duplicated functions, blocks, and structural patterns — then builds a prioritized refactoring map that LLMs can consume to eliminate redundancy systematically.
11
11
 
@@ -13,14 +13,18 @@ reDUP scans codebases for duplicated functions, blocks, and structural patterns
13
13
 
14
14
  - **Exact duplicate detection** via SHA-256 block hashing
15
15
  - **Structural clone detection** — same AST shape, different variable names
16
+ - **LSH near-duplicate detection** for large code blocks (>50 lines)
17
+ - **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C/C++
18
+ - **Parallel scanning** for large projects (2x+ performance improvement)
16
19
  - **Fuzzy near-duplicate matching** via SequenceMatcher / rapidfuzz
17
- - **Function-level analysis** using Python AST extraction
20
+ - **Function-level analysis** using Python AST and tree-sitter extraction
18
21
  - **Impact scoring** — prioritizes duplicates by `saved_lines × similarity`
19
22
  - **Refactoring planner** — generates concrete extract/inline suggestions
20
- - **Three output formats**: JSON (tooling), YAML (humans), TOON (LLMs)
21
- - **CLI** with `typer` + `rich` for interactive use
23
+ - **Multiple output formats**: JSON, YAML, TOON, Markdown
24
+ - **Configuration system** TOML files and environment variables
25
+ - **CLI commands**: `scan`, `diff`, `check`, `config`, `info`
26
+ - **CI integration** with configurable quality gates
22
27
  - **Clean output** — no syntax warnings from external libraries
23
- - **Optimized performance** — reduced complexity and improved maintainability
24
28
 
25
29
  ## Installation
26
30
 
@@ -48,6 +52,23 @@ redup scan .
48
52
  # Scan with JSON output saved to file
49
53
  redup scan ./src --format json --output ./reports/
50
54
 
55
+ # Parallel scanning for large projects
56
+ redup scan . --parallel --max-workers 4
57
+
58
+ # Multi-language scanning
59
+ redup scan . --ext ".py,.js,.ts,.go"
60
+
61
+ # CI gate with thresholds
62
+ redup check . --max-groups 10 --max-lines 100
63
+
64
+ # Compare two scans
65
+ redup diff before.json after.json
66
+
67
+ # Initialize configuration
68
+ redup config --init
69
+ ```
70
+
71
+ ```bash
51
72
  # Scan with all formats
52
73
  redup scan . --format all --output ./redup_output/
53
74
 
@@ -61,6 +82,37 @@ redup scan . --min-lines 5 --min-sim 0.9
61
82
  redup info
62
83
  ```
63
84
 
85
+ ### Configuration
86
+
87
+ Create a `redup.toml` file:
88
+
89
+ ```toml
90
+ [scan]
91
+ extensions = ".py,.js,.ts,.go,.rs,.java"
92
+ min_lines = 3
93
+ min_similarity = 0.85
94
+ include_tests = false
95
+
96
+ [lsh]
97
+ enabled = true
98
+ min_lines = 50
99
+ threshold = 0.8
100
+
101
+ [check]
102
+ max_groups = 10
103
+ max_lines = 100
104
+
105
+ [output]
106
+ format = "toon"
107
+ output = "redup_output"
108
+
109
+ [reporting]
110
+ include_snippets = true
111
+ generate_suggestions = true
112
+ ```
113
+
114
+ Or use `[tool.redup]` in `pyproject.toml`. Environment variables with `REDUP_` prefix override file settings.
115
+
64
116
  ### Python API
65
117
 
66
118
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redup"
7
- version = "0.2.4"
7
+ version = "0.3.2"
8
8
  description = "Code duplication analyzer and refactoring planner for LLMs"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -48,6 +48,24 @@ ast = [
48
48
  "tree-sitter-java>=0.20",
49
49
  "tree-sitter-c>=0.20",
50
50
  "tree-sitter-cpp>=0.20",
51
+ "tree-sitter-html>=0.20",
52
+ "tree-sitter-css>=0.20",
53
+ "tree-sitter-sql>=0.20",
54
+ "tree-sitter-json>=0.20",
55
+ "tree-sitter-yaml>=0.20",
56
+ "tree-sitter-toml>=0.20",
57
+ "tree-sitter-xml>=0.20",
58
+ "tree-sitter-markdown>=0.20",
59
+ "tree-sitter-bash>=0.20",
60
+ "tree-sitter-ruby>=0.20",
61
+ "tree-sitter-php>=0.20",
62
+ "tree-sitter-embedded-template>=0.20",
63
+ "tree-sitter-regex>=0.20",
64
+ "tree-sitter-scala>=0.20",
65
+ "tree-sitter-kotlin>=0.20",
66
+ "tree-sitter-swift>=0.20",
67
+ "tree-sitter-objc>=0.20",
68
+ "tree-sitter-c-sharp>=0.20",
51
69
  ]
52
70
  lsh = ["datasketch>=1.6"]
53
71
  dev = [
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.2.4"
5
+ __version__ = "0.3.2"
6
6
 
7
7
  from redup.core.models import (
8
8
  DuplicateFragment,
@@ -15,7 +15,7 @@ warnings.filterwarnings("ignore", category=SyntaxWarning)
15
15
  import redup # noqa: E402
16
16
  from redup.core.config import config_to_scan_config, create_sample_redup_toml, load_config # noqa: E402
17
17
  from redup.core.models import DuplicationMap, ScanConfig # noqa: E402
18
- from redup.core.pipeline import analyze # noqa: E402
18
+ from redup.core.pipeline import analyze, analyze_parallel # noqa: E402
19
19
  from redup.reporters.json_reporter import to_json # noqa: E402
20
20
  from redup.reporters.markdown_reporter import to_markdown # noqa: E402
21
21
  from redup.reporters.toon_reporter import to_toon # noqa: E402
@@ -28,7 +28,7 @@ app = typer.Typer(
28
28
  )
29
29
 
30
30
 
31
- OutputFormat = Literal["json", "yaml", "toon", "all"]
31
+ OutputFormat = Literal["json", "yaml", "toon", "markdown", "all"]
32
32
 
33
33
 
34
34
  DEFAULT_PATH = Path(".")
@@ -92,14 +92,12 @@ def _build_config_with_file_support(
92
92
 
93
93
  def _print_scan_header(
94
94
  path: Path,
95
- ext_list: list[str],
96
- min_lines: int,
97
- min_similarity: float,
95
+ config: ScanConfig,
98
96
  ) -> None:
99
97
  """Print scan configuration header."""
100
98
  typer.echo(f"reDUP v{redup.__version__} — scanning {path.resolve()}")
101
- typer.echo(f" extensions: {', '.join(ext_list)}")
102
- typer.echo(f" min_lines: {min_lines}, min_similarity: {min_similarity}")
99
+ typer.echo(f" extensions: {', '.join(config.extensions)}")
100
+ typer.echo(f" min_lines: {config.min_block_lines}, min_similarity: {config.min_similarity}")
103
101
  typer.echo("")
104
102
 
105
103
 
@@ -136,11 +134,15 @@ def _write_results(
136
134
  ]
137
135
  for _fmt, renderer, suffix in renderers:
138
136
  content = renderer(dup_map)
137
+ if suffix == "json":
138
+ include_snippets = load_config().get("reporting", {}).get("include_snippets", False)
139
+ content = to_json(dup_map, include_snippets=include_snippets)
139
140
  target = output_dir / f"duplication.{suffix}"
140
141
  target.write_text(content, encoding="utf-8")
141
142
  typer.echo(f" → {target}")
142
143
  elif format == "json":
143
- _write_output(to_json(dup_map), output_dir, "json")
144
+ include_snippets = load_config().get("reporting", {}).get("include_snippets", False)
145
+ _write_output(to_json(dup_map, include_snippets=include_snippets), output_dir, "json")
144
146
  elif format == "yaml":
145
147
  _write_output(to_yaml(dup_map), output_dir, "yaml")
146
148
  elif format == "toon":
@@ -168,38 +170,52 @@ def scan(
168
170
  "--output", "-o",
169
171
  help="Output directory or file path. Defaults to stdout.",
170
172
  ),
171
- extensions: str = typer.Option(
172
- ".py",
173
+ extensions: str | None = typer.Option(
174
+ None,
173
175
  "--ext", "-e",
174
- help="Comma-separated file extensions to scan.",
176
+ help="Comma-separated file extensions to scan. Overrides config.",
175
177
  ),
176
- min_lines: int = typer.Option(
177
- 3,
178
+ min_lines: int | None = typer.Option(
179
+ None,
178
180
  "--min-lines",
179
- help="Minimum block size (lines) to consider as duplicate.",
181
+ help="Minimum block size (lines). Overrides config.",
180
182
  ),
181
- min_similarity: float = typer.Option(
182
- 0.85,
183
+ min_similarity: float | None = typer.Option(
184
+ None,
183
185
  "--min-sim",
184
- help="Minimum similarity score (0.0-1.0) for fuzzy matches.",
186
+ help="Minimum similarity score (0.0-1.0). Overrides config.",
185
187
  ),
186
- include_tests: bool = typer.Option(
187
- False,
188
+ include_tests: bool | None = typer.Option(
189
+ None,
188
190
  "--include-tests",
189
- help="Include test files in analysis.",
191
+ help="Include test files. Overrides config.",
190
192
  ),
191
193
  functions_only: bool = typer.Option(
192
194
  False,
193
195
  "--functions-only",
194
196
  help="Only analyze function-level duplicates (faster).",
195
197
  ),
198
+ parallel: bool = typer.Option(
199
+ False,
200
+ "--parallel",
201
+ help="Use parallel scanning for large projects.",
202
+ ),
203
+ max_workers: int = typer.Option(
204
+ None,
205
+ "--max-workers",
206
+ help="Number of parallel workers (default: CPU count).",
207
+ ),
196
208
  ) -> None:
197
209
  """Scan a project for code duplicates and generate a refactoring map."""
198
- config = _build_config(path, extensions, min_lines, min_similarity, include_tests)
210
+ config = _build_config_with_file_support(path, extensions, min_lines, min_similarity, include_tests)
199
211
 
200
- _print_scan_header(path, config.extensions, min_lines, min_similarity)
212
+ _print_scan_header(path, config)
201
213
 
202
- dup_map = analyze(config=config, function_level_only=functions_only)
214
+ # Choose analysis method
215
+ if parallel:
216
+ dup_map = analyze_parallel(config=config, function_level_only=functions_only, max_workers=max_workers)
217
+ else:
218
+ dup_map = analyze(config=config, function_level_only=functions_only)
203
219
 
204
220
  _print_scan_summary(dup_map)
205
221
  _write_results(dup_map, format, output, path)
@@ -252,61 +268,78 @@ def check(
252
268
  dir_okay=True,
253
269
  file_okay=False,
254
270
  ),
255
- max_groups: int = typer.Option(
256
- 10,
271
+ max_groups: int | None = typer.Option(
272
+ None,
257
273
  "--max-groups",
258
- help="Maximum allowed duplicate groups (default: 10).",
274
+ help="Maximum allowed duplicate groups. Overrides config.",
259
275
  ),
260
- max_saved_lines: int = typer.Option(
261
- 100,
276
+ max_saved_lines: int | None = typer.Option(
277
+ None,
262
278
  "--max-lines",
263
- help="Maximum allowed recoverable lines (default: 100).",
279
+ help="Maximum allowed recoverable lines. Overrides config.",
264
280
  ),
265
- extensions: str = typer.Option(
266
- ".py",
281
+ extensions: str | None = typer.Option(
282
+ None,
267
283
  "--ext", "-e",
268
- help="Comma-separated file extensions to scan.",
284
+ help="Comma-separated file extensions to scan. Overrides config.",
269
285
  ),
270
- min_lines: int = typer.Option(
271
- 3,
286
+ min_lines: int | None = typer.Option(
287
+ None,
272
288
  "--min-lines",
273
- help="Minimum block size (lines) to consider as duplicate.",
289
+ help="Minimum block size (lines). Overrides config.",
274
290
  ),
275
- min_similarity: float = typer.Option(
276
- 0.85,
291
+ min_similarity: float | None = typer.Option(
292
+ None,
277
293
  "--min-sim",
278
- help="Minimum similarity score (0.0-1.0) for fuzzy matches.",
294
+ help="Minimum similarity score (0.0-1.0). Overrides config.",
279
295
  ),
280
- include_tests: bool = typer.Option(
281
- False,
296
+ include_tests: bool | None = typer.Option(
297
+ None,
282
298
  "--include-tests",
283
- help="Include test files in analysis.",
299
+ help="Include test files. Overrides config.",
284
300
  ),
285
301
  ) -> None:
286
302
  """Check project for duplicates and exit with non-zero code if thresholds exceeded."""
287
- config = _build_config(path, extensions, min_lines, min_similarity, include_tests)
303
+ file_config = load_config()
304
+
305
+ # Override with CLI arguments
306
+ if extensions is not None:
307
+ file_config["extensions"] = extensions
308
+ if min_lines is not None:
309
+ file_config["min_lines"] = min_lines
310
+ if min_similarity is not None:
311
+ file_config["min_similarity"] = min_similarity
312
+ if include_tests is not None:
313
+ file_config["include_tests"] = include_tests
314
+
315
+ config = config_to_scan_config(file_config, path)
316
+
317
+ # Get thresholds from config or CLI
318
+ check_config = file_config.get("check", {})
319
+ threshold_groups = max_groups if max_groups is not None else check_config.get("max_groups", 10)
320
+ threshold_lines = max_saved_lines if max_saved_lines is not None else check_config.get("max_lines", 100)
288
321
 
289
322
  dup_map = analyze(config=config, function_level_only=True)
290
323
 
291
324
  # Check thresholds
292
- groups_exceeded = dup_map.total_groups > max_groups
293
- lines_exceeded = dup_map.total_saved_lines > max_saved_lines
325
+ groups_exceeded = dup_map.total_groups > threshold_groups
326
+ lines_exceeded = dup_map.total_saved_lines > threshold_lines
294
327
 
295
328
  if groups_exceeded or lines_exceeded:
296
329
  typer.echo(f"❌ reDUP check FAILED - thresholds exceeded:", err=True)
297
- typer.echo(f" Duplicate groups: {dup_map.total_groups} (max: {max_groups})", err=True)
298
- typer.echo(f" Recoverable lines: {dup_map.total_saved_lines} (max: {max_saved_lines})", err=True)
330
+ typer.echo(f" Duplicate groups: {dup_map.total_groups} (max: {threshold_groups})", err=True)
331
+ typer.echo(f" Recoverable lines: {dup_map.total_saved_lines} (max: {threshold_lines})", err=True)
299
332
 
300
333
  if groups_exceeded:
301
- typer.echo(f" ❌ Too many duplicate groups ({dup_map.total_groups} > {max_groups})", err=True)
334
+ typer.echo(f" ❌ Too many duplicate groups ({dup_map.total_groups} > {threshold_groups})", err=True)
302
335
  if lines_exceeded:
303
- typer.echo(f" ❌ Too many duplicate lines ({dup_map.total_saved_lines} > {max_saved_lines})", err=True)
336
+ typer.echo(f" ❌ Too many duplicate lines ({dup_map.total_saved_lines} > {threshold_lines})", err=True)
304
337
 
305
338
  raise typer.Exit(1)
306
339
  else:
307
340
  typer.echo(f"✅ reDUP check PASSED")
308
- typer.echo(f" Duplicate groups: {dup_map.total_groups}/{max_groups}")
309
- typer.echo(f" Recoverable lines: {dup_map.total_saved_lines}/{max_saved_lines}")
341
+ typer.echo(f" Duplicate groups: {dup_map.total_groups}/{threshold_groups}")
342
+ typer.echo(f" Recoverable lines: {dup_map.total_saved_lines}/{threshold_lines}")
310
343
  # Don't exit - just return normally (exit code 0)
311
344
  return
312
345
 
@@ -103,9 +103,9 @@ def config_to_scan_config(config: dict[str, Any], path: Path) -> ScanConfig:
103
103
  return ScanConfig(
104
104
  root=path,
105
105
  extensions=ext_list,
106
- min_block_lines=scan_config.get("min_lines", 3),
107
- min_similarity=scan_config.get("min_similarity", 0.85),
108
- include_tests=scan_config.get("include_tests", False),
106
+ min_block_lines=config.get("min_lines", scan_config.get("min_lines", 3)),
107
+ min_similarity=config.get("min_similarity", scan_config.get("min_similarity", 0.85)),
108
+ include_tests=config.get("include_tests", scan_config.get("include_tests", False)),
109
109
  lsh_enabled=lsh_config.get("enabled", True),
110
110
  lsh_min_lines=lsh_config.get("min_lines", 50),
111
111
  lsh_threshold=lsh_config.get("threshold", 0.8),