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.
Files changed (85) hide show
  1. {code2llm-0.5.6 → code2llm-0.5.7}/PKG-INFO +15 -1
  2. {code2llm-0.5.6 → code2llm-0.5.7}/README.md +14 -0
  3. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/__init__.py +1 -1
  4. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/data_analysis.py +34 -13
  5. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/cli.py +77 -45
  6. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/context_exporter.py +29 -12
  7. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/evolution_exporter.py +61 -28
  8. code2llm-0.5.7/code2llm/exporters/flow_constants.py +29 -0
  9. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/flow_exporter.py +11 -190
  10. code2llm-0.5.7/code2llm/exporters/flow_renderer.py +188 -0
  11. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/generators/llm_flow.py +37 -16
  12. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/generators/llm_task.py +45 -24
  13. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/__init__.py +1 -1
  14. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/PKG-INFO +15 -1
  15. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/SOURCES.txt +2 -0
  16. {code2llm-0.5.6 → code2llm-0.5.7}/pyproject.toml +1 -1
  17. {code2llm-0.5.6 → code2llm-0.5.7}/LICENSE +0 -0
  18. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/__main__.py +0 -0
  19. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/__init__.py +0 -0
  20. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/call_graph.py +0 -0
  21. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/cfg.py +0 -0
  22. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/coupling.py +0 -0
  23. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/dfg.py +0 -0
  24. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/pipeline_detector.py +0 -0
  25. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/side_effects.py +0 -0
  26. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/smells.py +0 -0
  27. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/analysis/type_inference.py +0 -0
  28. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/__init__.py +0 -0
  29. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/analyzer.py +0 -0
  30. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/config.py +0 -0
  31. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/__init__.py +0 -0
  32. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/file_analyzer.py +0 -0
  33. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/file_cache.py +0 -0
  34. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/file_filter.py +0 -0
  35. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/core/refactoring.py +0 -0
  36. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/models.py +0 -0
  37. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/__init__.py +0 -0
  38. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/cache.py +0 -0
  39. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/incremental.py +0 -0
  40. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/prioritizer.py +0 -0
  41. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/scanner.py +0 -0
  42. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming/strategies.py +0 -0
  43. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/core/streaming_analyzer.py +0 -0
  44. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/__init__.py +0 -0
  45. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/base.py +0 -0
  46. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/json_exporter.py +0 -0
  47. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/llm_exporter.py +0 -0
  48. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/map_exporter.py +0 -0
  49. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/mermaid_exporter.py +0 -0
  50. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/readme_exporter.py +0 -0
  51. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/__init__.py +0 -0
  52. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/helpers.py +0 -0
  53. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/metrics.py +0 -0
  54. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/module_detail.py +0 -0
  55. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon/renderer.py +0 -0
  56. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/toon.py +0 -0
  57. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/exporters/yaml_exporter.py +0 -0
  58. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/generators/__init__.py +0 -0
  59. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/generators/mermaid.py +0 -0
  60. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/config.py +0 -0
  61. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/entity_resolution.py +0 -0
  62. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/intent_matching.py +0 -0
  63. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/normalization.py +0 -0
  64. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/nlp/pipeline.py +0 -0
  65. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/patterns/__init__.py +0 -0
  66. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/patterns/detector.py +0 -0
  67. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/refactor/__init__.py +0 -0
  68. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm/refactor/prompt_engine.py +0 -0
  69. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/dependency_links.txt +0 -0
  70. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/entry_points.txt +0 -0
  71. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/requires.txt +0 -0
  72. {code2llm-0.5.6 → code2llm-0.5.7}/code2llm.egg-info/top_level.txt +0 -0
  73. {code2llm-0.5.6 → code2llm-0.5.7}/setup.cfg +0 -0
  74. {code2llm-0.5.6 → code2llm-0.5.7}/setup.py +0 -0
  75. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_advanced_analysis.py +0 -0
  76. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_analyzer.py +0 -0
  77. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_deep_analysis.py +0 -0
  78. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_edge_cases.py +0 -0
  79. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_flow_exporter.py +0 -0
  80. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_format_quality.py +0 -0
  81. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_nlp_pipeline.py +0 -0
  82. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_pipeline_detector.py +0 -0
  83. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_prompt_engine.py +0 -0
  84. {code2llm-0.5.6 → code2llm-0.5.7}/tests/test_refactoring_engine.py +0 -0
  85. {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.6
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
+ ![img_1.png](img_1.png)
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
+ ![img_1.png](img_1.png)
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)
@@ -8,7 +8,7 @@ Includes NLP Processing Pipeline for query normalization, intent matching,
8
8
  and entity resolution with multilingual support.
9
9
  """
10
10
 
11
- __version__ = "0.5.2"
11
+ __version__ = "0.5.7"
12
12
  __author__ = "STTS Project"
13
13
 
14
14
  # Core analysis components
@@ -130,9 +130,8 @@ class DataAnalyzer:
130
130
  if len(flows) >= 20: break
131
131
  return flows
132
132
 
133
- def _analyze_data_types(self, result: AnalysisResult) -> list:
134
- """Analyze data types and usage."""
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 = [t for t, inds in type_indicators.items() if any(ind in name_lower or ind in doc for ind in inds)]
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] = {'type_name': type_key, 'detected_types': list(set(detected)), 'parameter_types': list(set(params)), 'return_types': list(set(returns)), 'functions': [], 'usage_count': 0, 'cross_module_usage': 0}
156
- data_types[type_key]['functions'].append(func_name)
157
- data_types[type_key]['usage_count'] += 1
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 main():
207
- """Main CLI entry point."""
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
- # Evolution
340
- if 'evolution' in formats:
341
- exporter = EvolutionExporter()
342
- filepath = output_dir / 'evolution.toon'
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
- # README documentation (default enabled)
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: visited = set()
183
- if func_name in visited or depth <= 0: return func_name.split('.')[-1]
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] != module):
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: break
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
- cross = " →" if mod != module else ""
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 _compute_god_modules(self, result: AnalysisResult) -> List[Dict]:
107
- """Identify god modules (≥500 lines) from project files."""
108
- file_stats = defaultdict(lambda: {"lines": 0, "funcs": 0, "classes": set(), "max_cc": 0})
109
- pp = Path(result.project_path) if result.project_path else None
110
-
111
- # Scan file sizes
112
- if pp and pp.is_dir():
113
- for py in pp.rglob("*.py"):
114
- fpath = str(py)
115
- if self._is_excluded(fpath):
116
- continue
117
- try:
118
- lc = len(py.read_text(encoding="utf-8", errors="ignore").splitlines())
119
- file_stats[fpath]["lines"] = lc
120
- except Exception:
121
- pass
122
-
123
- # Aggregate function/class data
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
- # Filter to god modules
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, "lines": stats["lines"],
148
- "funcs": stats["funcs"], "classes": len(stats["classes"]),
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
+ }