code2llm 0.5.107__tar.gz → 0.5.109__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 (128) hide show
  1. {code2llm-0.5.107 → code2llm-0.5.109}/PKG-INFO +3 -2
  2. {code2llm-0.5.107 → code2llm-0.5.109}/README.md +1 -1
  3. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/__init__.py +1 -1
  4. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/cli_commands.py +66 -36
  5. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/cli_exports/orchestrator.py +3 -0
  6. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/yaml_exporter.py +88 -37
  7. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/nlp/__init__.py +1 -1
  8. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm.egg-info/PKG-INFO +3 -2
  9. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm.egg-info/requires.txt +1 -0
  10. {code2llm-0.5.107 → code2llm-0.5.109}/pyproject.toml +6 -1
  11. {code2llm-0.5.107 → code2llm-0.5.109}/setup.py +1 -1
  12. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_edge_cases.py +6 -5
  13. {code2llm-0.5.107 → code2llm-0.5.109}/LICENSE +0 -0
  14. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/__main__.py +0 -0
  15. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/__init__.py +0 -0
  16. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/call_graph.py +0 -0
  17. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/cfg.py +0 -0
  18. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/coupling.py +0 -0
  19. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/data_analysis.py +0 -0
  20. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/dfg.py +0 -0
  21. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/pipeline_detector.py +0 -0
  22. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/side_effects.py +0 -0
  23. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/smells.py +0 -0
  24. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/type_inference.py +0 -0
  25. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/utils/__init__.py +0 -0
  26. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/analysis/utils/ast_helpers.py +0 -0
  27. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/api.py +0 -0
  28. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/cli.py +0 -0
  29. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/cli_analysis.py +0 -0
  30. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/cli_exports/__init__.py +0 -0
  31. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/cli_exports/code2logic.py +0 -0
  32. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/cli_exports/formats.py +0 -0
  33. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/cli_exports/prompt.py +0 -0
  34. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/cli_parser.py +0 -0
  35. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/__init__.py +0 -0
  36. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/analyzer.py +0 -0
  37. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/ast_registry.py +0 -0
  38. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/config.py +0 -0
  39. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/export_pipeline.py +0 -0
  40. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/file_analyzer.py +0 -0
  41. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/file_cache.py +0 -0
  42. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/file_filter.py +0 -0
  43. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/gitignore.py +0 -0
  44. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/incremental.py +0 -0
  45. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/__init__.py +0 -0
  46. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/base.py +0 -0
  47. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/cpp.py +0 -0
  48. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/csharp.py +0 -0
  49. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/generic.py +0 -0
  50. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/go_lang.py +0 -0
  51. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/java.py +0 -0
  52. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/php.py +0 -0
  53. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/ruby.py +0 -0
  54. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/rust.py +0 -0
  55. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/ts_extractors.py +0 -0
  56. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/ts_parser.py +0 -0
  57. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/lang/typescript.py +0 -0
  58. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/large_repo.py +0 -0
  59. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/models.py +0 -0
  60. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/refactoring.py +0 -0
  61. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/repo_files.py +0 -0
  62. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/streaming/__init__.py +0 -0
  63. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/streaming/cache.py +0 -0
  64. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/streaming/incremental.py +0 -0
  65. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/streaming/prioritizer.py +0 -0
  66. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/streaming/scanner.py +0 -0
  67. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/streaming/strategies.py +0 -0
  68. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/streaming_analyzer.py +0 -0
  69. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/core/toon_size_manager.py +0 -0
  70. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/__init__.py +0 -0
  71. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/article_view.py +0 -0
  72. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/base.py +0 -0
  73. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/context_exporter.py +0 -0
  74. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/context_view.py +0 -0
  75. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/evolution_exporter.py +0 -0
  76. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/flow_constants.py +0 -0
  77. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/flow_exporter.py +0 -0
  78. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/flow_renderer.py +0 -0
  79. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/html_dashboard.py +0 -0
  80. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/index_generator.py +0 -0
  81. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/json_exporter.py +0 -0
  82. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/llm_exporter.py +0 -0
  83. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/map_exporter.py +0 -0
  84. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/mermaid_exporter.py +0 -0
  85. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/mermaid_flow_helpers.py +0 -0
  86. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/project_yaml_exporter.py +0 -0
  87. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/readme_exporter.py +0 -0
  88. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/report_generators.py +0 -0
  89. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/toon/__init__.py +0 -0
  90. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/toon/helpers.py +0 -0
  91. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/toon/metrics.py +0 -0
  92. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/toon/module_detail.py +0 -0
  93. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/toon/renderer.py +0 -0
  94. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/toon.py +0 -0
  95. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/toon_view.py +0 -0
  96. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/exporters/validate_project.py +0 -0
  97. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/generators/__init__.py +0 -0
  98. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/generators/llm_flow.py +0 -0
  99. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/generators/llm_task.py +0 -0
  100. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/generators/mermaid.py +0 -0
  101. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/nlp/config.py +0 -0
  102. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/nlp/entity_resolution.py +0 -0
  103. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/nlp/intent_matching.py +0 -0
  104. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/nlp/normalization.py +0 -0
  105. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/nlp/pipeline.py +0 -0
  106. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/patterns/__init__.py +0 -0
  107. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/patterns/detector.py +0 -0
  108. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/refactor/__init__.py +0 -0
  109. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm/refactor/prompt_engine.py +0 -0
  110. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm.egg-info/SOURCES.txt +0 -0
  111. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm.egg-info/dependency_links.txt +0 -0
  112. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm.egg-info/entry_points.txt +0 -0
  113. {code2llm-0.5.107 → code2llm-0.5.109}/code2llm.egg-info/top_level.txt +0 -0
  114. {code2llm-0.5.107 → code2llm-0.5.109}/setup.cfg +0 -0
  115. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_advanced_analysis.py +0 -0
  116. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_analyzer.py +0 -0
  117. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_deep_analysis.py +0 -0
  118. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_flow_exporter.py +0 -0
  119. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_format_quality.py +0 -0
  120. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_multilanguage_e2e.py +0 -0
  121. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_nlp_pipeline.py +0 -0
  122. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_nonpython_cc_calls.py +0 -0
  123. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_pipeline_detector.py +0 -0
  124. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_project_toon_export.py +0 -0
  125. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_prompt_engine.py +0 -0
  126. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_prompt_txt.py +0 -0
  127. {code2llm-0.5.107 → code2llm-0.5.109}/tests/test_refactoring_engine.py +0 -0
  128. {code2llm-0.5.107 → code2llm-0.5.109}/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.107
3
+ Version: 0.5.109
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
@@ -50,6 +50,7 @@ Requires-Dist: tree-sitter-ruby>=0.21
50
50
  Provides-Extra: dev
51
51
  Requires-Dist: pytest>=6.2; extra == "dev"
52
52
  Requires-Dist: pytest-cov>=2.12; extra == "dev"
53
+ Requires-Dist: pytest-xdist>=3.0; extra == "dev"
53
54
  Requires-Dist: black>=21.0; extra == "dev"
54
55
  Requires-Dist: flake8>=3.9; extra == "dev"
55
56
  Requires-Dist: mypy>=0.910; extra == "dev"
@@ -66,7 +67,7 @@ Dynamic: requires-python
66
67
 
67
68
  ## AI Cost Tracking
68
69
 
69
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.107-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
70
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.109-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
70
71
  ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-53.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
71
72
 
72
73
  - 🤖 **LLM usage:** $7.5000 (154 commits)
@@ -3,7 +3,7 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.107-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.109-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
7
  ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-53.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
9
  - 🤖 **LLM usage:** $7.5000 (154 commits)
@@ -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.107"
11
+ __version__ = "0.5.109"
12
12
  __author__ = "STTS Project"
13
13
 
14
14
  # Core analysis components (lightweight, always needed)
@@ -109,71 +109,101 @@ def print_start_info(args, source_path: Path, output_dir: Path) -> None:
109
109
 
110
110
  def validate_chunked_output(output_dir: Path, args) -> bool:
111
111
  """Validate generated chunked output.
112
-
112
+
113
113
  Checks:
114
114
  1. All chunks have required files (analysis.toon, context.md, evolution.toon.yaml)
115
115
  2. Files are not empty
116
116
  3. Report summary
117
-
117
+
118
118
  Returns True if valid, False otherwise.
119
119
  """
120
120
  if not output_dir.exists():
121
121
  print(f"✗ Output directory does not exist: {output_dir}", file=sys.stderr)
122
122
  return False
123
-
124
- # Find all chunk directories
125
- chunk_dirs = [d for d in output_dir.iterdir() if d.is_dir()]
126
-
123
+
124
+ chunk_dirs = _get_chunk_dirs(output_dir)
127
125
  if not chunk_dirs:
128
126
  print(f"✗ No chunk directories found in: {output_dir}", file=sys.stderr)
129
127
  return False
130
-
128
+
131
129
  required_files = ['analysis.toon.yaml', 'context.md', 'evolution.toon.yaml']
130
+ issues, valid_chunks = _validate_chunks(chunk_dirs, required_files)
131
+
132
+ _print_validation_summary(chunk_dirs, valid_chunks, issues)
133
+ return len(issues) == 0
134
+
135
+
136
+ def _get_chunk_dirs(output_dir: Path) -> list[Path]:
137
+ """Find all chunk directories in output directory."""
138
+ return [d for d in output_dir.iterdir() if d.is_dir()]
139
+
140
+
141
+ def _validate_chunks(chunk_dirs: list[Path], required_files: list[str]) -> tuple:
142
+ """Validate all chunks and return issues and valid chunks."""
132
143
  issues = []
133
144
  valid_chunks = []
134
-
135
- print(f"\n🔍 Validating {len(chunk_dirs)} chunks in: {output_dir}")
145
+
146
+ print(f"\n🔍 Validating {len(chunk_dirs)} chunks")
136
147
  print("-" * 50)
137
-
148
+
138
149
  for chunk_dir in sorted(chunk_dirs):
139
- chunk_name = chunk_dir.name
140
- chunk_issues = []
141
-
142
- for req_file in required_files:
143
- file_path = chunk_dir / req_file
144
- if not file_path.exists():
145
- chunk_issues.append(f" missing {req_file}")
146
- elif file_path.stat().st_size == 0:
147
- chunk_issues.append(f" empty {req_file}")
148
-
150
+ chunk_issues = _validate_single_chunk(chunk_dir, required_files)
151
+
149
152
  if chunk_issues:
150
- issues.append((chunk_name, chunk_issues))
151
- print(f"✗ {chunk_name}")
152
- for issue in chunk_issues:
153
- print(f" {issue}")
153
+ issues.append((chunk_dir.name, chunk_issues))
154
+ _print_chunk_errors(chunk_dir.name, chunk_issues)
154
155
  else:
155
- # Get file sizes
156
- sizes = []
157
- for req_file in required_files:
158
- size = (chunk_dir / req_file).stat().st_size
159
- sizes.append(f"{req_file}:{size//1024}KB" if size > 1024 else f"{req_file}:{size}B")
160
- valid_chunks.append(chunk_name)
161
- print(f"✓ {chunk_name} ({', '.join(sizes)})")
162
-
156
+ sizes = _get_file_sizes(chunk_dir, required_files)
157
+ valid_chunks.append(chunk_dir.name)
158
+ print(f"✓ {chunk_dir.name} ({sizes})")
159
+
163
160
  print("-" * 50)
161
+ return issues, valid_chunks
162
+
163
+
164
+ def _validate_single_chunk(chunk_dir: Path, required_files: list[str]) -> list[str]:
165
+ """Validate a single chunk directory. Returns list of issues."""
166
+ chunk_issues = []
167
+
168
+ for req_file in required_files:
169
+ file_path = chunk_dir / req_file
170
+ if not file_path.exists():
171
+ chunk_issues.append(f" missing {req_file}")
172
+ elif file_path.stat().st_size == 0:
173
+ chunk_issues.append(f" empty {req_file}")
174
+
175
+ return chunk_issues
176
+
177
+
178
+ def _get_file_sizes(chunk_dir: Path, required_files: list[str]) -> str:
179
+ """Get formatted file sizes for a chunk."""
180
+ sizes = []
181
+ for req_file in required_files:
182
+ size = (chunk_dir / req_file).stat().st_size
183
+ sizes.append(f"{req_file}:{size//1024}KB" if size > 1024 else f"{req_file}:{size}B")
184
+ return ", ".join(sizes)
185
+
186
+
187
+ def _print_chunk_errors(chunk_name: str, chunk_issues: list[str]) -> None:
188
+ """Print errors for a chunk."""
189
+ print(f"✗ {chunk_name}")
190
+ for issue in chunk_issues:
191
+ print(f" {issue}")
192
+
193
+
194
+ def _print_validation_summary(chunk_dirs: list, valid_chunks: list, issues: list) -> None:
195
+ """Print validation summary report."""
164
196
  print(f"\n📊 Validation Summary:")
165
197
  print(f" Total chunks: {len(chunk_dirs)}")
166
198
  print(f" Valid: {len(valid_chunks)}")
167
199
  print(f" Issues: {len(issues)}")
168
-
200
+
169
201
  if issues:
170
202
  print(f"\n⚠️ {len(issues)} chunk(s) have issues:")
171
- for chunk_name, chunk_issues in issues:
203
+ for chunk_name, _ in issues:
172
204
  print(f" - {chunk_name}")
173
- return False
174
205
  else:
175
206
  print(f"\n✅ All {len(valid_chunks)} chunks are valid!")
176
- return True
177
207
 
178
208
 
179
209
  def generate_llm_context(args_list):
@@ -103,6 +103,9 @@ def _export_chunked_results(
103
103
  _export_simple_formats(args, result, output_dir, ['toon', 'context'])
104
104
  _export_evolution(args, result, output_dir)
105
105
 
106
+ if 'calls' in formats:
107
+ _export_calls(args, result, output_dir)
108
+
106
109
  if 'all' in requested_formats:
107
110
  _export_project_toon(args, result, output_dir)
108
111
 
@@ -117,53 +117,107 @@ class YAMLExporter(Exporter):
117
117
  - modules: grouping of functions by module
118
118
  - stats: summary statistics
119
119
  """
120
- # Collect connected nodes and edges
120
+ connected, edges = self._collect_edges(result, max_calls_per_func, max_edges)
121
+ nodes = self._build_nodes(result, connected)
122
+ modules = self._group_by_module(result, connected)
123
+
124
+ calls_data = self._build_calls_data(result, nodes, edges, modules)
125
+
126
+ Path(output_path).parent.mkdir(parents=True, exist_ok=True)
127
+ with open(output_path, 'w', encoding='utf-8') as f:
128
+ yaml.dump(calls_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
129
+
130
+ def _collect_edges(self, result: AnalysisResult, max_calls: int, max_edges: int) -> Tuple[Set[str], List[Dict]]:
131
+ """Collect connected nodes and edges from function calls."""
121
132
  connected: Set[str] = set()
122
133
  edges: List[Dict] = []
123
134
  seen_pairs: Set[Tuple[str, str]] = set()
124
-
135
+
125
136
  for func_name, fi in result.functions.items():
126
- for callee in fi.calls[:max_calls_per_func]:
127
- resolved = self._resolve_callee(callee, result.functions)
128
- if resolved and resolved != func_name:
129
- connected.add(func_name)
130
- connected.add(resolved)
131
- pair = (func_name, resolved)
132
- if pair not in seen_pairs:
133
- seen_pairs.add(pair)
134
- edges.append({
135
- 'caller': func_name,
136
- 'callee': resolved,
137
- 'call_type': 'direct' if callee == resolved.split('.')[-1] else 'resolved'
138
- })
139
- if len(edges) >= max_edges:
140
- break
141
137
  if len(edges) >= max_edges:
142
138
  break
143
-
144
- # Build nodes data
139
+ self._process_function_calls(
140
+ func_name, fi, result.functions, max_calls, max_edges,
141
+ connected, edges, seen_pairs
142
+ )
143
+
144
+ return connected, edges
145
+
146
+ def _process_function_calls(
147
+ self, func_name: str, fi: FunctionInfo, functions: Dict[str, FunctionInfo],
148
+ max_calls: int, max_edges: int, connected: Set[str], edges: List[Dict],
149
+ seen_pairs: Set[Tuple[str, str]]
150
+ ) -> None:
151
+ """Process calls for a single function."""
152
+ for callee in fi.calls[:max_calls]:
153
+ if len(edges) >= max_edges:
154
+ break
155
+ resolved = self._resolve_callee(callee, functions)
156
+ if self._should_add_edge(func_name, resolved, seen_pairs):
157
+ connected.add(func_name)
158
+ connected.add(resolved)
159
+ seen_pairs.add((func_name, resolved))
160
+ edges.append(self._create_edge(func_name, resolved, callee))
161
+
162
+ @staticmethod
163
+ def _should_add_edge(func_name: str, resolved: Optional[str], seen_pairs: Set[Tuple[str, str]]) -> bool:
164
+ """Check if edge should be added (valid callee and not duplicate)."""
165
+ if not resolved or resolved == func_name:
166
+ return False
167
+ return (func_name, resolved) not in seen_pairs
168
+
169
+ @staticmethod
170
+ def _create_edge(caller: str, resolved: str, callee: str) -> Dict:
171
+ """Create edge dict with call type classification."""
172
+ return {
173
+ 'caller': caller,
174
+ 'callee': resolved,
175
+ 'call_type': 'direct' if callee == resolved.split('.')[-1] else 'resolved'
176
+ }
177
+
178
+ def _build_nodes(self, result: AnalysisResult, connected: Set[str]) -> Dict[str, Dict]:
179
+ """Build node data for all connected functions."""
145
180
  nodes: Dict[str, Dict] = {}
146
181
  for fn in connected:
147
182
  fi = result.functions.get(fn)
148
183
  if fi:
149
- cc = self._get_cc(fi)
150
- nodes[fn] = {
151
- 'name': fi.name,
152
- 'module': fi.module,
153
- 'line': fi.line,
154
- 'cyclomatic_complexity': cc,
155
- 'calls_out': len(fi.calls),
156
- 'calls_in': sum(1 for f in result.functions.values() if fn in [self._resolve_callee(c, result.functions) for c in f.calls]),
157
- }
158
-
159
- # Group by module
184
+ nodes[fn] = self._create_node(fi, fn, result)
185
+ return nodes
186
+
187
+ def _create_node(self, fi: FunctionInfo, fn: str, result: AnalysisResult) -> Dict:
188
+ """Create node dict with function metadata."""
189
+ return {
190
+ 'name': fi.name,
191
+ 'module': fi.module,
192
+ 'line': fi.line,
193
+ 'cyclomatic_complexity': self._get_cc(fi),
194
+ 'calls_out': len(fi.calls),
195
+ 'calls_in': self._count_calls_in(fn, result),
196
+ }
197
+
198
+ def _count_calls_in(self, fn: str, result: AnalysisResult) -> int:
199
+ """Count how many functions call the given function."""
200
+ count = 0
201
+ for f in result.functions.values():
202
+ for c in f.calls:
203
+ resolved = self._resolve_callee(c, result.functions)
204
+ if resolved == fn:
205
+ count += 1
206
+ break
207
+ return count
208
+
209
+ @staticmethod
210
+ def _group_by_module(result: AnalysisResult, connected: Set[str]) -> Dict[str, List[str]]:
211
+ """Group connected functions by their module."""
160
212
  modules: Dict[str, List[str]] = defaultdict(list)
161
213
  for fn in connected:
162
214
  fi = result.functions.get(fn)
163
215
  if fi:
164
216
  modules[fi.module].append(fn)
165
-
166
- # Build output structure
217
+ return {mod: sorted(funcs) for mod, funcs in sorted(modules.items())}
218
+
219
+ def _build_calls_data(self, result: AnalysisResult, nodes: Dict, edges: List, modules: Dict) -> Dict:
220
+ """Build the final calls.yaml data structure."""
167
221
  calls_data = {
168
222
  'project': result.project_path,
169
223
  'generated_from': 'code2llm call graph analysis',
@@ -174,16 +228,13 @@ class YAMLExporter(Exporter):
174
228
  },
175
229
  'nodes': nodes,
176
230
  'edges': edges,
177
- 'modules': {mod: sorted(funcs) for mod, funcs in sorted(modules.items())},
231
+ 'modules': modules,
178
232
  }
179
233
 
180
- # Add entry points if available
181
234
  if result.entry_points:
182
235
  calls_data['entry_points'] = sorted(result.entry_points)
183
236
 
184
- Path(output_path).parent.mkdir(parents=True, exist_ok=True)
185
- with open(output_path, 'w', encoding='utf-8') as f:
186
- yaml.dump(calls_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
237
+ return calls_data
187
238
 
188
239
  @staticmethod
189
240
  def _resolve_callee(callee: str, funcs: Dict[str, FunctionInfo]) -> Optional[str]:
@@ -4,7 +4,7 @@ Provides query normalization, intent matching, and entity resolution
4
4
  with multilingual support and fuzzy matching.
5
5
  """
6
6
 
7
- __version__ = "0.5.107"
7
+ __version__ = "0.5.109"
8
8
 
9
9
  from .pipeline import NLPPipeline
10
10
  from .normalization import QueryNormalizer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2llm
3
- Version: 0.5.107
3
+ Version: 0.5.109
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
@@ -50,6 +50,7 @@ Requires-Dist: tree-sitter-ruby>=0.21
50
50
  Provides-Extra: dev
51
51
  Requires-Dist: pytest>=6.2; extra == "dev"
52
52
  Requires-Dist: pytest-cov>=2.12; extra == "dev"
53
+ Requires-Dist: pytest-xdist>=3.0; extra == "dev"
53
54
  Requires-Dist: black>=21.0; extra == "dev"
54
55
  Requires-Dist: flake8>=3.9; extra == "dev"
55
56
  Requires-Dist: mypy>=0.910; extra == "dev"
@@ -66,7 +67,7 @@ Dynamic: requires-python
66
67
 
67
68
  ## AI Cost Tracking
68
69
 
69
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.107-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
70
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.109-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
70
71
  ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-53.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
71
72
 
72
73
  - 🤖 **LLM usage:** $7.5000 (154 commits)
@@ -24,6 +24,7 @@ tree-sitter-ruby>=0.21
24
24
  [dev]
25
25
  pytest>=6.2
26
26
  pytest-cov>=2.12
27
+ pytest-xdist>=3.0
27
28
  black>=21.0
28
29
  flake8>=3.9
29
30
  mypy>=0.910
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code2llm"
7
- version = "0.5.107"
7
+ version = "0.5.109"
8
8
  description = "High-performance Python code flow analysis with optimized TOON format - CFG, DFG, call graphs, and intelligent code queries"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -67,6 +67,7 @@ dependencies = [
67
67
  dev = [
68
68
  "pytest>=6.2",
69
69
  "pytest-cov>=2.12",
70
+ "pytest-xdist>=3.0",
70
71
  "black>=21.0",
71
72
  "flake8>=3.9",
72
73
  "mypy>=0.910",
@@ -94,6 +95,10 @@ ignore_missing_imports = true
94
95
  [tool.pytest.ini_options]
95
96
  testpaths = ["tests"]
96
97
  python_files = "test_*.py"
98
+ addopts = "-n auto" # Run tests in parallel using pytest-xdist
99
+ markers = [
100
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')"
101
+ ]
97
102
 
98
103
  [tool.pfix]
99
104
  # Self-healing Python configuration
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
4
4
  import os
5
5
 
6
6
  # Read version
7
- version = "0.5.106"
7
+ version = "0.5.108"
8
8
 
9
9
  # Read long description
10
10
  def read_readme():
@@ -50,22 +50,23 @@ class TestEdgeCases:
50
50
  finally:
51
51
  shutil.rmtree(tmp_dir)
52
52
 
53
+ @pytest.mark.slow
53
54
  def test_very_large_file(self):
54
55
  """Handle very large Python file."""
55
56
  tmp_dir = Path(tempfile.mkdtemp())
56
57
  try:
57
- # Create large file (1000 functions)
58
- lines = [f"def func_{i}(): pass" for i in range(1000)]
58
+ # Create large file (200 functions - reduced from 1000 for faster tests)
59
+ lines = [f"def func_{i}(): pass" for i in range(200)]
59
60
  (tmp_dir / "large.py").write_text('\n'.join(lines))
60
-
61
+
61
62
  config = Config(
62
63
  mode="static",
63
64
  filters=FilterConfig(exclude_patterns=[], min_function_lines=1)
64
65
  )
65
66
  analyzer = ProjectAnalyzer(config)
66
67
  result = analyzer.analyze_project(str(tmp_dir))
67
-
68
- assert result.get_function_count() >= 1000
68
+
69
+ assert result.get_function_count() >= 200
69
70
  finally:
70
71
  shutil.rmtree(tmp_dir)
71
72
 
File without changes
File without changes
File without changes
File without changes