code2llm 0.5.6__tar.gz → 0.5.7__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.
- {code2llm-0.5.6 → code2llm-0.5.7}/PKG-INFO +15 -1
- {code2llm-0.5.6 → code2llm-0.5.7}/README.md +14 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/__init__.py +1 -1
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/data_analysis.py +34 -13
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/cli.py +77 -45
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/context_exporter.py +29 -12
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/evolution_exporter.py +61 -28
- code2llm-0.5.7/code2llm/exporters/flow_constants.py +29 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/flow_exporter.py +11 -190
- code2llm-0.5.7/code2llm/exporters/flow_renderer.py +188 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/generators/llm_flow.py +37 -16
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/generators/llm_task.py +45 -24
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/__init__.py +1 -1
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/PKG-INFO +15 -1
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/SOURCES.txt +2 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/pyproject.toml +1 -1
- {code2llm-0.5.6 → code2llm-0.5.7}/LICENSE +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/__main__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/__init__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/call_graph.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/cfg.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/coupling.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/dfg.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/pipeline_detector.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/side_effects.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/smells.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/type_inference.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/__init__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/analyzer.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/config.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/__init__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/file_analyzer.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/file_cache.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/file_filter.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/refactoring.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/models.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/__init__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/cache.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/incremental.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/prioritizer.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/scanner.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/strategies.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming_analyzer.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/__init__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/base.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/json_exporter.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/llm_exporter.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/map_exporter.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/mermaid_exporter.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/readme_exporter.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/__init__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/helpers.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/metrics.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/module_detail.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/renderer.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/yaml_exporter.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/generators/__init__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/generators/mermaid.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/config.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/entity_resolution.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/intent_matching.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/normalization.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/pipeline.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/patterns/__init__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/patterns/detector.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/refactor/__init__.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/refactor/prompt_engine.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/dependency_links.txt +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/entry_points.txt +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/requires.txt +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/top_level.txt +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/setup.cfg +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/setup.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_advanced_analysis.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_analyzer.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_deep_analysis.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_edge_cases.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_flow_exporter.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_format_quality.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_nlp_pipeline.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_pipeline_detector.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_prompt_engine.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_refactoring_engine.py +0 -0
- {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_toon_v2.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code2llm
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.7
|
|
4
4
|
Summary: High-performance Python code flow analysis with optimized TOON format - CFG, DFG, call graphs, and intelligent code queries
|
|
5
5
|
Home-page: https://github.com/wronai/stts
|
|
6
6
|
Author: STTS Project
|
|
@@ -95,6 +95,12 @@ code2llm ./ -f all
|
|
|
95
95
|
code2llm ./ -f context
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
Przykład z projektu toonic, z użyciem modelu Kimi K2.5 w VScode Windsurf:
|
|
99
|
+

|
|
100
|
+
```bash
|
|
101
|
+
code2llm ./ -f toon,evolution -o ./project
|
|
102
|
+
```
|
|
103
|
+
|
|
98
104
|
### Performance Options
|
|
99
105
|
```bash
|
|
100
106
|
# Fast analysis for large projects
|
|
@@ -355,3 +361,11 @@ code2llm ./ -f yaml --separate-orphans
|
|
|
355
361
|
**Modules**: 76
|
|
356
362
|
|
|
357
363
|
For more information about code2llm, visit: https://github.com/tom-sapletta/code2llm
|
|
364
|
+
|
|
365
|
+
## License
|
|
366
|
+
|
|
367
|
+
Apache License 2.0 - see [LICENSE](LICENSE) for details.
|
|
368
|
+
|
|
369
|
+
## Author
|
|
370
|
+
|
|
371
|
+
Created by **Tom Sapletta** - [tom@sapletta.com](mailto:tom@sapletta.com)
|
|
@@ -46,6 +46,12 @@ code2llm ./ -f all
|
|
|
46
46
|
code2llm ./ -f context
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Przykład z projektu toonic, z użyciem modelu Kimi K2.5 w VScode Windsurf:
|
|
50
|
+

|
|
51
|
+
```bash
|
|
52
|
+
code2llm ./ -f toon,evolution -o ./project
|
|
53
|
+
```
|
|
54
|
+
|
|
49
55
|
### Performance Options
|
|
50
56
|
```bash
|
|
51
57
|
# Fast analysis for large projects
|
|
@@ -306,3 +312,11 @@ code2llm ./ -f yaml --separate-orphans
|
|
|
306
312
|
**Modules**: 76
|
|
307
313
|
|
|
308
314
|
For more information about code2llm, visit: https://github.com/tom-sapletta/code2llm
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
Apache License 2.0 - see [LICENSE](LICENSE) for details.
|
|
319
|
+
|
|
320
|
+
## Author
|
|
321
|
+
|
|
322
|
+
Created by **Tom Sapletta** - [tom@sapletta.com](mailto:tom@sapletta.com)
|
|
@@ -130,9 +130,8 @@ class DataAnalyzer:
|
|
|
130
130
|
if len(flows) >= 20: break
|
|
131
131
|
return flows
|
|
132
132
|
|
|
133
|
-
def
|
|
134
|
-
"""
|
|
135
|
-
data_types = {}
|
|
133
|
+
def _detect_types_from_name(self, func_name: str, doc: str) -> list:
|
|
134
|
+
"""Detect data types from function name and docstring."""
|
|
136
135
|
type_indicators = {
|
|
137
136
|
'list': ['list', 'array', 'items', 'elements', 'collection', 'sequence'],
|
|
138
137
|
'dict': ['dict', 'map', 'mapping', 'key_value', 'record', 'object'],
|
|
@@ -143,23 +142,45 @@ class DataAnalyzer:
|
|
|
143
142
|
'tuple': ['tuple', 'pair'],
|
|
144
143
|
'set': ['set', 'unique'],
|
|
145
144
|
}
|
|
145
|
+
name_lower = func_name.lower()
|
|
146
|
+
return [t for t, inds in type_indicators.items() if any(ind in name_lower or ind in doc for ind in inds)]
|
|
147
|
+
|
|
148
|
+
def _create_type_entry(self, type_key: str, detected: list, params: list, returns: list) -> dict:
|
|
149
|
+
"""Create a new data type entry."""
|
|
150
|
+
return {
|
|
151
|
+
'type_name': type_key,
|
|
152
|
+
'detected_types': list(set(detected)),
|
|
153
|
+
'parameter_types': list(set(params)),
|
|
154
|
+
'return_types': list(set(returns)),
|
|
155
|
+
'functions': [],
|
|
156
|
+
'usage_count': 0,
|
|
157
|
+
'cross_module_usage': 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def _update_type_stats(self, entry: dict, func_name: str, func_module: str, calls: list) -> None:
|
|
161
|
+
"""Update type entry with function info and check cross-module usage."""
|
|
162
|
+
entry['functions'].append(func_name)
|
|
163
|
+
entry['usage_count'] += 1
|
|
164
|
+
for called in list(calls)[:10]:
|
|
165
|
+
called_module = called.rsplit('.', 1)[0] if '.' in called else 'root'
|
|
166
|
+
if called_module != func_module:
|
|
167
|
+
entry['cross_module_usage'] += 1
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
def _analyze_data_types(self, result: AnalysisResult) -> list:
|
|
171
|
+
"""Analyze data types and usage."""
|
|
172
|
+
data_types = {}
|
|
146
173
|
for func_name, func in result.functions.items():
|
|
147
|
-
name_lower = func.name.lower()
|
|
148
174
|
doc = func.docstring.lower() if func.docstring else ''
|
|
149
|
-
detected =
|
|
175
|
+
detected = self._detect_types_from_name(func.name, doc)
|
|
150
176
|
params = self._infer_parameter_types(func)
|
|
151
177
|
returns = self._infer_return_types(func)
|
|
152
178
|
if detected or params or returns:
|
|
153
179
|
type_key = ",".join(sorted(set(detected + params + returns)))
|
|
154
180
|
if type_key not in data_types:
|
|
155
|
-
data_types[type_key] =
|
|
156
|
-
|
|
157
|
-
data_types[type_key]
|
|
158
|
-
mod = func_name.rsplit('.', 1)[0] if '.' in func_name else 'root'
|
|
159
|
-
for called in list(func.calls)[:10]:
|
|
160
|
-
if (called.rsplit('.', 1)[0] if '.' in called else 'root') != mod:
|
|
161
|
-
data_types[type_key]['cross_module_usage'] += 1
|
|
162
|
-
break
|
|
181
|
+
data_types[type_key] = self._create_type_entry(type_key, detected, params, returns)
|
|
182
|
+
func_module = func_name.rsplit('.', 1)[0] if '.' in func_name else 'root'
|
|
183
|
+
self._update_type_stats(data_types[type_key], func_name, func_module, func.calls)
|
|
163
184
|
return sorted(data_types.values(), key=lambda x: x['usage_count'], reverse=True)
|
|
164
185
|
|
|
165
186
|
def _infer_parameter_types(self, func) -> list:
|
|
@@ -8,6 +8,7 @@ Analyze control flow, data flow, and call graphs of Python codebases.
|
|
|
8
8
|
import argparse
|
|
9
9
|
import sys
|
|
10
10
|
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
11
12
|
|
|
12
13
|
from .core.config import Config, ANALYSIS_MODES
|
|
13
14
|
from .core.analyzer import ProjectAnalyzer
|
|
@@ -203,20 +204,18 @@ Strategy Options (--strategy):
|
|
|
203
204
|
return parser
|
|
204
205
|
|
|
205
206
|
|
|
206
|
-
def
|
|
207
|
-
"""
|
|
208
|
-
# Handle special sub-commands first
|
|
207
|
+
def _handle_special_commands() -> Optional[int]:
|
|
208
|
+
"""Handle special sub-commands (llm-flow, llm-context)."""
|
|
209
209
|
if len(sys.argv) > 1 and sys.argv[1] == 'llm-flow':
|
|
210
210
|
from .generators.llm_flow import main as llm_flow_main
|
|
211
211
|
return llm_flow_main(sys.argv[2:])
|
|
212
|
-
|
|
213
212
|
if len(sys.argv) > 1 and sys.argv[1] == 'llm-context':
|
|
214
213
|
return generate_llm_context(sys.argv[2:])
|
|
214
|
+
return None
|
|
215
215
|
|
|
216
|
-
# Parse arguments
|
|
217
|
-
parser = create_parser()
|
|
218
|
-
args = parser.parse_args()
|
|
219
216
|
|
|
217
|
+
def _validate_and_setup(args) -> tuple[Path, Path]:
|
|
218
|
+
"""Validate source path and setup output directory."""
|
|
220
219
|
if not args.source:
|
|
221
220
|
print("Error: missing required argument: source", file=sys.stderr)
|
|
222
221
|
print("Usage: code2llm <source> [options]", file=sys.stderr)
|
|
@@ -230,12 +229,31 @@ def main():
|
|
|
230
229
|
|
|
231
230
|
output_dir = Path(args.output)
|
|
232
231
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
return source_path, output_dir
|
|
233
|
+
|
|
233
234
|
|
|
235
|
+
def _print_start_info(args, source_path: Path, output_dir: Path) -> None:
|
|
236
|
+
"""Print analysis start information if verbose."""
|
|
234
237
|
if args.verbose:
|
|
235
238
|
print(f"Analyzing: {source_path}")
|
|
236
239
|
print(f"Mode: {args.mode}")
|
|
237
240
|
print(f"Output: {output_dir}")
|
|
238
241
|
|
|
242
|
+
|
|
243
|
+
def main():
|
|
244
|
+
"""Main CLI entry point."""
|
|
245
|
+
# Handle special sub-commands first
|
|
246
|
+
special_result = _handle_special_commands()
|
|
247
|
+
if special_result is not None:
|
|
248
|
+
return special_result
|
|
249
|
+
|
|
250
|
+
# Parse arguments
|
|
251
|
+
parser = create_parser()
|
|
252
|
+
args = parser.parse_args()
|
|
253
|
+
|
|
254
|
+
source_path, output_dir = _validate_and_setup(args)
|
|
255
|
+
_print_start_info(args, source_path, output_dir)
|
|
256
|
+
|
|
239
257
|
# Analyze → Export
|
|
240
258
|
result = _run_analysis(args, source_path, output_dir)
|
|
241
259
|
_run_exports(args, result, output_dir)
|
|
@@ -321,56 +339,70 @@ def _run_streaming_analysis(args, config, source_path: Path):
|
|
|
321
339
|
return analyzer.analyze_project(str(source_path))
|
|
322
340
|
|
|
323
341
|
|
|
342
|
+
def _export_evolution(args, result, output_dir: Path):
|
|
343
|
+
"""Export evolution.toon format."""
|
|
344
|
+
if 'evolution' not in [f.strip() for f in args.format.split(',')] and 'all' not in [f.strip() for f in args.format.split(',')]:
|
|
345
|
+
return
|
|
346
|
+
exporter = EvolutionExporter()
|
|
347
|
+
filepath = output_dir / 'evolution.toon'
|
|
348
|
+
exporter.export(result, str(filepath))
|
|
349
|
+
if args.verbose:
|
|
350
|
+
print(f" - EVOLUTION (refactoring queue): {filepath}")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _export_data_structures(args, result, output_dir: Path):
|
|
354
|
+
"""Export data structures YAML."""
|
|
355
|
+
if not args.data_structures:
|
|
356
|
+
return
|
|
357
|
+
exporter = YAMLExporter()
|
|
358
|
+
struct_path = output_dir / 'data_structures.yaml'
|
|
359
|
+
exporter.export_data_structures(result, str(struct_path), compact=True)
|
|
360
|
+
if args.verbose:
|
|
361
|
+
print(f" - Data structures: {struct_path}")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _export_context_fallback(args, result, output_dir: Path, formats: list):
|
|
365
|
+
"""Export context.md if not in formats."""
|
|
366
|
+
if 'context' in formats or 'all' in formats:
|
|
367
|
+
return
|
|
368
|
+
exporter = ContextExporter()
|
|
369
|
+
filepath = output_dir / 'context.md'
|
|
370
|
+
exporter.export(result, str(filepath))
|
|
371
|
+
if args.verbose:
|
|
372
|
+
print(f" - CONTEXT (LLM narrative): {filepath}")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _export_readme(args, result, output_dir: Path):
|
|
376
|
+
"""Export README.md documentation."""
|
|
377
|
+
if not args.readme or args.no_readme:
|
|
378
|
+
return
|
|
379
|
+
exporter = READMEExporter()
|
|
380
|
+
filepath = output_dir / 'README.md'
|
|
381
|
+
exporter.export(result, str(filepath))
|
|
382
|
+
if args.verbose:
|
|
383
|
+
print(f" - README (documentation): {filepath}")
|
|
384
|
+
|
|
385
|
+
|
|
324
386
|
def _run_exports(args, result, output_dir: Path):
|
|
325
387
|
"""Export analysis results in requested formats."""
|
|
326
388
|
formats = [f.strip() for f in args.format.split(',')]
|
|
327
|
-
|
|
328
389
|
if 'all' in formats:
|
|
329
390
|
formats = ['toon', 'map', 'flow', 'context', 'yaml', 'json', 'mermaid', 'evolution']
|
|
330
391
|
|
|
331
392
|
try:
|
|
332
|
-
# Simple format exports
|
|
333
393
|
_export_simple_formats(args, result, output_dir, formats)
|
|
334
|
-
|
|
335
|
-
# Mermaid (complex — 3 files + PNG)
|
|
394
|
+
|
|
336
395
|
if 'mermaid' in formats:
|
|
337
396
|
_export_mermaid(args, result, output_dir)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
exporter.export(result, str(filepath))
|
|
344
|
-
if args.verbose:
|
|
345
|
-
print(f" - EVOLUTION (refactoring queue): {filepath}")
|
|
346
|
-
|
|
347
|
-
# Data structures (optional flag)
|
|
348
|
-
if args.data_structures:
|
|
349
|
-
exporter = YAMLExporter()
|
|
350
|
-
struct_path = output_dir / 'data_structures.yaml'
|
|
351
|
-
exporter.export_data_structures(result, str(struct_path), compact=True)
|
|
352
|
-
if args.verbose:
|
|
353
|
-
print(f" - Data structures: {struct_path}")
|
|
354
|
-
|
|
355
|
-
# Backward compat: always generate context.md
|
|
356
|
-
if 'context' not in formats:
|
|
357
|
-
exporter = ContextExporter()
|
|
358
|
-
filepath = output_dir / 'context.md'
|
|
359
|
-
exporter.export(result, str(filepath))
|
|
360
|
-
if args.verbose:
|
|
361
|
-
print(f" - CONTEXT (LLM narrative): {filepath}")
|
|
362
|
-
|
|
363
|
-
# AI-driven refactoring prompts
|
|
397
|
+
|
|
398
|
+
_export_evolution(args, result, output_dir)
|
|
399
|
+
_export_data_structures(args, result, output_dir)
|
|
400
|
+
_export_context_fallback(args, result, output_dir, formats)
|
|
401
|
+
|
|
364
402
|
if args.refactor:
|
|
365
403
|
_export_refactor_prompts(args, result, output_dir)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if args.readme and not args.no_readme:
|
|
369
|
-
exporter = READMEExporter()
|
|
370
|
-
filepath = output_dir / 'README.md'
|
|
371
|
-
exporter.export(result, str(filepath))
|
|
372
|
-
if args.verbose:
|
|
373
|
-
print(f" - README (documentation): {filepath}")
|
|
404
|
+
|
|
405
|
+
_export_readme(args, result, output_dir)
|
|
374
406
|
|
|
375
407
|
except Exception as e:
|
|
376
408
|
print(f"Error during export: {e}", file=sys.stderr)
|
|
@@ -177,31 +177,48 @@ class ContextExporter(Exporter):
|
|
|
177
177
|
lines.extend(["```", ""])
|
|
178
178
|
return lines
|
|
179
179
|
|
|
180
|
+
def _group_calls_by_module(self, calls: list, func_name: str) -> dict:
|
|
181
|
+
"""Group function calls by their module."""
|
|
182
|
+
module = func_name.rsplit('.', 1)[0] if '.' in func_name else 'root'
|
|
183
|
+
calls_by_module = {}
|
|
184
|
+
for called in calls[:5]:
|
|
185
|
+
mod = called.rsplit('.', 1)[0] if '.' in called else 'root'
|
|
186
|
+
if mod not in calls_by_module:
|
|
187
|
+
calls_by_module[mod] = []
|
|
188
|
+
calls_by_module[mod].append(called)
|
|
189
|
+
return calls_by_module, module
|
|
190
|
+
|
|
191
|
+
def _format_sub_flow(self, sub_flow: str, called: str, is_cross_module: bool) -> list:
|
|
192
|
+
"""Format sub-flow lines with proper indentation."""
|
|
193
|
+
lines = []
|
|
194
|
+
cross = " →" if is_cross_module else ""
|
|
195
|
+
lines.append(f" └─{cross}> {called.split('.')[-1]}")
|
|
196
|
+
for sub in sub_flow.split('\n')[1:][:3]:
|
|
197
|
+
lines.append(" " + sub)
|
|
198
|
+
return lines
|
|
199
|
+
|
|
180
200
|
def _trace_flow(self, func_name: str, func, result: AnalysisResult, depth: int, visited: set = None) -> str:
|
|
181
201
|
"""Trace execution flow from a function with cycle detection."""
|
|
182
|
-
if visited is None:
|
|
183
|
-
|
|
202
|
+
if visited is None:
|
|
203
|
+
visited = set()
|
|
204
|
+
if func_name in visited or depth <= 0:
|
|
205
|
+
return func_name.split('.')[-1]
|
|
184
206
|
|
|
185
207
|
visited.add(func_name)
|
|
186
208
|
short_name = func_name.split('.')[-1]
|
|
187
209
|
module = func_name.rsplit('.', 1)[0] if '.' in func_name else 'root'
|
|
188
210
|
lines = [f"{short_name} [{module}]"]
|
|
189
211
|
|
|
190
|
-
calls_by_module =
|
|
191
|
-
for called in func.calls[:5]:
|
|
192
|
-
mod = called.rsplit('.', 1)[0] if '.' in called else 'root'
|
|
193
|
-
if mod not in calls_by_module: calls_by_module[mod] = []
|
|
194
|
-
calls_by_module[mod].append(called)
|
|
212
|
+
calls_by_module, func_module = self._group_calls_by_module(func.calls, func_name)
|
|
195
213
|
|
|
196
214
|
shown = 0
|
|
197
|
-
for mod, calls in sorted(calls_by_module.items(), key=lambda x: x[0] !=
|
|
215
|
+
for mod, calls in sorted(calls_by_module.items(), key=lambda x: x[0] != func_module):
|
|
198
216
|
for called in calls[:2]:
|
|
199
|
-
if shown >= 3:
|
|
217
|
+
if shown >= 3:
|
|
218
|
+
break
|
|
200
219
|
called_func = result.functions.get(called)
|
|
201
220
|
if called_func and called not in visited:
|
|
202
221
|
sub_flow = self._trace_flow(called, called_func, result, depth - 1, visited.copy())
|
|
203
|
-
|
|
204
|
-
lines.append(f" └─{cross}> {called.split('.')[-1]}")
|
|
205
|
-
for sub in sub_flow.split('\n')[1:][:3]: lines.append(" " + sub)
|
|
222
|
+
lines.extend(self._format_sub_flow(sub_flow, called, mod != func_module))
|
|
206
223
|
shown += 1
|
|
207
224
|
return '\n'.join(lines)
|
|
@@ -103,24 +103,38 @@ class EvolutionExporter(Exporter):
|
|
|
103
103
|
})
|
|
104
104
|
return sorted(func_data, key=lambda x: x["impact"], reverse=True)
|
|
105
105
|
|
|
106
|
-
def
|
|
107
|
-
"""
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
106
|
+
def _scan_file_sizes(self, project_path: Optional[Path]) -> Dict[str, int]:
|
|
107
|
+
"""Scan Python files and return line counts."""
|
|
108
|
+
file_lines: Dict[str, int] = {}
|
|
109
|
+
if not project_path or not project_path.is_dir():
|
|
110
|
+
return file_lines
|
|
111
|
+
|
|
112
|
+
for py in project_path.rglob("*.py"):
|
|
113
|
+
fpath = str(py)
|
|
114
|
+
if self._is_excluded(fpath):
|
|
115
|
+
continue
|
|
116
|
+
try:
|
|
117
|
+
lc = len(py.read_text(encoding="utf-8", errors="ignore").splitlines())
|
|
118
|
+
file_lines[fpath] = lc
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
return file_lines
|
|
122
|
+
|
|
123
|
+
def _aggregate_file_stats(
|
|
124
|
+
self,
|
|
125
|
+
result: AnalysisResult,
|
|
126
|
+
file_lines: Dict[str, int]
|
|
127
|
+
) -> Dict[str, Dict]:
|
|
128
|
+
"""Aggregate function and class data per file."""
|
|
129
|
+
file_stats: Dict[str, Dict] = defaultdict(
|
|
130
|
+
lambda: {"lines": 0, "funcs": 0, "classes": set(), "max_cc": 0}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Initialize with line counts
|
|
134
|
+
for fpath, lc in file_lines.items():
|
|
135
|
+
file_stats[fpath]["lines"] = lc
|
|
136
|
+
|
|
137
|
+
# Aggregate function data
|
|
124
138
|
for qname, fi in result.functions.items():
|
|
125
139
|
if self._is_excluded(fi.file):
|
|
126
140
|
continue
|
|
@@ -129,28 +143,47 @@ class EvolutionExporter(Exporter):
|
|
|
129
143
|
fs["max_cc"] = max(fs["max_cc"], fi.complexity.get("cyclomatic_complexity", 0))
|
|
130
144
|
if fi.class_name:
|
|
131
145
|
fs["classes"].add(fi.class_name)
|
|
146
|
+
|
|
147
|
+
# Aggregate class data
|
|
132
148
|
for qname, ci in result.classes.items():
|
|
133
149
|
if not self._is_excluded(ci.file):
|
|
134
150
|
file_stats[ci.file]["classes"].add(ci.name)
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
|
|
152
|
+
return file_stats
|
|
153
|
+
|
|
154
|
+
def _make_relative_path(self, fpath: str, project_path: Optional[Path]) -> str:
|
|
155
|
+
"""Convert absolute path to relative path."""
|
|
156
|
+
if not project_path:
|
|
157
|
+
return fpath
|
|
158
|
+
try:
|
|
159
|
+
return str(Path(fpath).relative_to(project_path))
|
|
160
|
+
except ValueError:
|
|
161
|
+
return fpath
|
|
162
|
+
|
|
163
|
+
def _filter_god_modules(self, file_stats: Dict[str, Dict], project_path: Optional[Path]) -> List[Dict]:
|
|
164
|
+
"""Filter files to god modules (≥500 lines)."""
|
|
137
165
|
god_modules = []
|
|
138
166
|
for fpath, stats in file_stats.items():
|
|
139
167
|
if stats["lines"] >= GOD_MODULE_LINES:
|
|
140
|
-
rel = fpath
|
|
141
|
-
if pp:
|
|
142
|
-
try:
|
|
143
|
-
rel = str(Path(fpath).relative_to(pp))
|
|
144
|
-
except ValueError:
|
|
145
|
-
pass
|
|
168
|
+
rel = self._make_relative_path(fpath, project_path)
|
|
146
169
|
god_modules.append({
|
|
147
|
-
"file": rel,
|
|
148
|
-
"
|
|
170
|
+
"file": rel,
|
|
171
|
+
"lines": stats["lines"],
|
|
172
|
+
"funcs": stats["funcs"],
|
|
173
|
+
"classes": len(stats["classes"]),
|
|
149
174
|
"max_cc": stats["max_cc"],
|
|
150
175
|
})
|
|
151
176
|
god_modules.sort(key=lambda x: x["lines"], reverse=True)
|
|
152
177
|
return god_modules
|
|
153
178
|
|
|
179
|
+
def _compute_god_modules(self, result: AnalysisResult) -> List[Dict]:
|
|
180
|
+
"""Identify god modules (≥500 lines) from project files."""
|
|
181
|
+
pp = Path(result.project_path) if result.project_path else None
|
|
182
|
+
|
|
183
|
+
file_lines = self._scan_file_sizes(pp)
|
|
184
|
+
file_stats = self._aggregate_file_stats(result, file_lines)
|
|
185
|
+
return self._filter_god_modules(file_stats, pp)
|
|
186
|
+
|
|
154
187
|
def _compute_hub_types(self, result: AnalysisResult) -> List[Dict]:
|
|
155
188
|
"""Identify hub types consumed by many functions."""
|
|
156
189
|
type_consumers: Dict[str, int] = defaultdict(int)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Stałe dla FlowExporter.
|
|
2
|
+
|
|
3
|
+
Zawiera progi, wzorce wykluczeń i rekomendacje dotyczące podziału typów hub.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Progi dla wykrywania problemów
|
|
7
|
+
CC_HIGH = 15
|
|
8
|
+
FAN_OUT_THRESHOLD = 10
|
|
9
|
+
HUB_TYPE_THRESHOLD = 10
|
|
10
|
+
|
|
11
|
+
# Wzorce do wykluczenia (venv, cache, etc.)
|
|
12
|
+
EXCLUDE_PATTERNS = {
|
|
13
|
+
'venv', '.venv', 'env', '.env', 'publish-env', 'test-env',
|
|
14
|
+
'site-packages', 'node_modules', '__pycache__', '.git',
|
|
15
|
+
'dist', 'build', 'egg-info', '.tox', '.mypy_cache',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Rekomendacje podziału typów hub: typ -> sugerowane pod-interfejsy
|
|
19
|
+
HUB_SPLIT_RECOMMENDATIONS = {
|
|
20
|
+
"AnalysisResult": [
|
|
21
|
+
"StructureResult (modules, classes, functions)",
|
|
22
|
+
"MetricsResult (complexity, coupling)",
|
|
23
|
+
"FlowResult (call_graph, cfg, dfg)",
|
|
24
|
+
],
|
|
25
|
+
"dict": ["replace with typed alternatives (dataclass/TypedDict)"],
|
|
26
|
+
"str": [], # primitive, expected to be ubiquitous
|
|
27
|
+
"list": [],
|
|
28
|
+
"Any": [],
|
|
29
|
+
}
|