code2llm 0.5.126__tar.gz → 0.5.127__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 (193) hide show
  1. code2llm-0.5.127/MANIFEST.in +4 -0
  2. {code2llm-0.5.126 → code2llm-0.5.127}/PKG-INFO +2 -2
  3. {code2llm-0.5.126 → code2llm-0.5.127}/README.md +1 -1
  4. code2llm-0.5.127/VERSION +1 -0
  5. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/__init__.py +1 -1
  6. code2llm-0.5.127/code2llm/exporters/evolution/__init__.py +78 -0
  7. code2llm-0.5.127/code2llm/exporters/evolution/computation.py +167 -0
  8. code2llm-0.5.127/code2llm/exporters/evolution/constants.py +25 -0
  9. code2llm-0.5.127/code2llm/exporters/evolution/exclusion.py +17 -0
  10. code2llm-0.5.127/code2llm/exporters/evolution/render.py +195 -0
  11. code2llm-0.5.127/code2llm/exporters/evolution/yaml_export.py +103 -0
  12. code2llm-0.5.127/code2llm/exporters/evolution_exporter.py +74 -0
  13. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/nlp/__init__.py +1 -1
  14. code2llm-0.5.127/code2llm/parsers/toon_parser.py +147 -0
  15. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm.egg-info/PKG-INFO +2 -2
  16. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm.egg-info/SOURCES.txt +9 -0
  17. {code2llm-0.5.126 → code2llm-0.5.127}/pyproject.toml +1 -1
  18. code2llm-0.5.126/code2llm/exporters/evolution_exporter.py +0 -473
  19. {code2llm-0.5.126 → code2llm-0.5.127}/LICENSE +0 -0
  20. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/__main__.py +0 -0
  21. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/__init__.py +0 -0
  22. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/call_graph.py +0 -0
  23. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/cfg.py +0 -0
  24. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/coupling.py +0 -0
  25. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/data_analysis.py +0 -0
  26. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/dfg.py +0 -0
  27. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/pipeline_classifier.py +0 -0
  28. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/pipeline_detector.py +0 -0
  29. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/pipeline_resolver.py +0 -0
  30. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/side_effects.py +0 -0
  31. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/smells.py +0 -0
  32. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/type_inference.py +0 -0
  33. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/utils/__init__.py +0 -0
  34. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/analysis/utils/ast_helpers.py +0 -0
  35. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/api.py +0 -0
  36. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli.py +0 -0
  37. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_analysis.py +0 -0
  38. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_commands.py +0 -0
  39. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_exports/__init__.py +0 -0
  40. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_exports/code2logic.py +0 -0
  41. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_exports/formats.py +0 -0
  42. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_exports/orchestrator.py +0 -0
  43. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_exports/orchestrator_chunked.py +0 -0
  44. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_exports/orchestrator_constants.py +0 -0
  45. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_exports/orchestrator_handlers.py +0 -0
  46. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_exports/prompt.py +0 -0
  47. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/cli_parser.py +0 -0
  48. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/__init__.py +0 -0
  49. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/analyzer.py +0 -0
  50. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/ast_registry.py +0 -0
  51. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/config.py +0 -0
  52. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/export_pipeline.py +0 -0
  53. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/file_analyzer.py +0 -0
  54. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/file_cache.py +0 -0
  55. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/file_filter.py +0 -0
  56. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/gitignore.py +0 -0
  57. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/incremental.py +0 -0
  58. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/__init__.py +0 -0
  59. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/base.py +0 -0
  60. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/cpp.py +0 -0
  61. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/csharp.py +0 -0
  62. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/generic.py +0 -0
  63. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/go_lang.py +0 -0
  64. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/java.py +0 -0
  65. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/php.py +0 -0
  66. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/ruby.py +0 -0
  67. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/rust.py +0 -0
  68. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/ts_extractors.py +0 -0
  69. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/ts_parser.py +0 -0
  70. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/lang/typescript.py +0 -0
  71. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/large_repo.py +0 -0
  72. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/models.py +0 -0
  73. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/persistent_cache.py +0 -0
  74. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/refactoring.py +0 -0
  75. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/repo_files.py +0 -0
  76. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/streaming/__init__.py +0 -0
  77. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/streaming/cache.py +0 -0
  78. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/streaming/incremental.py +0 -0
  79. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/streaming/prioritizer.py +0 -0
  80. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/streaming/scanner.py +0 -0
  81. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/streaming/strategies.py +0 -0
  82. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/streaming_analyzer.py +0 -0
  83. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/core/toon_size_manager.py +0 -0
  84. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/__init__.py +0 -0
  85. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/article_view.py +0 -0
  86. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/base.py +0 -0
  87. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/context_exporter.py +0 -0
  88. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/context_view.py +0 -0
  89. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/dashboard_data.py +0 -0
  90. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/dashboard_renderer.py +0 -0
  91. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/flow_constants.py +0 -0
  92. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/flow_exporter.py +0 -0
  93. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/flow_renderer.py +0 -0
  94. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/html_dashboard.py +0 -0
  95. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/index_generator/__init__.py +0 -0
  96. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/index_generator/renderer.py +0 -0
  97. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/index_generator/scanner.py +0 -0
  98. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/index_generator.py +0 -0
  99. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/json_exporter.py +0 -0
  100. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/llm_exporter.py +0 -0
  101. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/map/__init__.py +0 -0
  102. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/map/alerts.py +0 -0
  103. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/map/details.py +0 -0
  104. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/map/header.py +0 -0
  105. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/map/module_list.py +0 -0
  106. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/map/utils.py +0 -0
  107. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/map/yaml_export.py +0 -0
  108. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/map_exporter.py +0 -0
  109. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid/__init__.py +0 -0
  110. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid/calls.py +0 -0
  111. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid/classic.py +0 -0
  112. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid/compact.py +0 -0
  113. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid/flow_compact.py +0 -0
  114. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid/flow_detailed.py +0 -0
  115. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid/flow_full.py +0 -0
  116. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid/utils.py +0 -0
  117. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid_exporter.py +0 -0
  118. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/mermaid_flow_helpers.py +0 -0
  119. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/project_yaml/__init__.py +0 -0
  120. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/project_yaml/constants.py +0 -0
  121. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/project_yaml/core.py +0 -0
  122. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/project_yaml/evolution.py +0 -0
  123. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/project_yaml/health.py +0 -0
  124. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/project_yaml/hotspots.py +0 -0
  125. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/project_yaml/modules.py +0 -0
  126. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/project_yaml_exporter.py +0 -0
  127. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/readme/__init__.py +0 -0
  128. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/readme/content.py +0 -0
  129. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/readme/files.py +0 -0
  130. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/readme/insights.py +0 -0
  131. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/readme/sections.py +0 -0
  132. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/readme_exporter.py +0 -0
  133. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/report_generators.py +0 -0
  134. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon/__init__.py +0 -0
  135. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon/helpers.py +0 -0
  136. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon/metrics.py +0 -0
  137. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon/metrics_core.py +0 -0
  138. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon/metrics_duplicates.py +0 -0
  139. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon/metrics_health.py +0 -0
  140. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon/module_detail.py +0 -0
  141. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon/renderer.py +0 -0
  142. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon.py +0 -0
  143. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/toon_view.py +0 -0
  144. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/validate_project.py +0 -0
  145. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/exporters/yaml_exporter.py +0 -0
  146. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/__init__.py +0 -0
  147. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/_utils.py +0 -0
  148. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/llm_flow/__init__.py +0 -0
  149. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/llm_flow/analysis.py +0 -0
  150. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/llm_flow/cli.py +0 -0
  151. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/llm_flow/generator.py +0 -0
  152. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/llm_flow/nodes.py +0 -0
  153. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/llm_flow/parsing.py +0 -0
  154. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/llm_flow/utils.py +0 -0
  155. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/llm_flow.py +0 -0
  156. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/llm_task.py +0 -0
  157. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/mermaid/__init__.py +0 -0
  158. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/mermaid/fix.py +0 -0
  159. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/mermaid/png.py +0 -0
  160. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/mermaid/validation.py +0 -0
  161. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/generators/mermaid.py +0 -0
  162. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/nlp/config.py +0 -0
  163. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/nlp/entity_resolution.py +0 -0
  164. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/nlp/intent_matching.py +0 -0
  165. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/nlp/normalization.py +0 -0
  166. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/nlp/pipeline.py +0 -0
  167. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/patterns/__init__.py +0 -0
  168. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/patterns/detector.py +0 -0
  169. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/refactor/__init__.py +0 -0
  170. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm/refactor/prompt_engine.py +0 -0
  171. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm.egg-info/dependency_links.txt +0 -0
  172. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm.egg-info/entry_points.txt +0 -0
  173. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm.egg-info/requires.txt +0 -0
  174. {code2llm-0.5.126 → code2llm-0.5.127}/code2llm.egg-info/top_level.txt +0 -0
  175. {code2llm-0.5.126 → code2llm-0.5.127}/setup.cfg +0 -0
  176. {code2llm-0.5.126 → code2llm-0.5.127}/setup.py +0 -0
  177. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_advanced_analysis.py +0 -0
  178. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_analyzer.py +0 -0
  179. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_calls_toon_export.py +0 -0
  180. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_deep_analysis.py +0 -0
  181. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_edge_cases.py +0 -0
  182. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_flow_exporter.py +0 -0
  183. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_format_quality.py +0 -0
  184. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_multilanguage_e2e.py +0 -0
  185. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_nlp_pipeline.py +0 -0
  186. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_nonpython_cc_calls.py +0 -0
  187. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_persistent_cache.py +0 -0
  188. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_pipeline_detector.py +0 -0
  189. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_project_toon_export.py +0 -0
  190. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_prompt_engine.py +0 -0
  191. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_prompt_txt.py +0 -0
  192. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_refactoring_engine.py +0 -0
  193. {code2llm-0.5.126 → code2llm-0.5.127}/tests/test_toon_v2.py +0 -0
@@ -0,0 +1,4 @@
1
+ include VERSION
2
+ include README.md
3
+ include LICENSE
4
+ recursive-include code2llm *.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2llm
3
- Version: 0.5.126
3
+ Version: 0.5.127
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
@@ -67,7 +67,7 @@ Dynamic: requires-python
67
67
 
68
68
  ## AI Cost Tracking
69
69
 
70
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.126-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.127-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
71
71
  ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-57.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
72
72
 
73
73
  - 🤖 **LLM usage:** $7.5000 (166 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.126-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.127-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-57.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
9
  - 🤖 **LLM usage:** $7.5000 (166 commits)
@@ -0,0 +1 @@
1
+ 0.5.127
@@ -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.126"
11
+ __version__ = "0.5.127"
12
12
  __author__ = "STTS Project"
13
13
 
14
14
  # Core analysis components (lightweight, always needed)
@@ -0,0 +1,78 @@
1
+ """Evolution exporter package — prioritized refactoring queue for iterative improvement.
2
+
3
+ This package provides:
4
+ - constants: Thresholds and exclusion patterns
5
+ - exclusion: Path filtering logic
6
+ - computation: Metrics calculation (god modules, hub types, etc.)
7
+ - render: Text output generation for evolution.toon
8
+ - yaml_export: Structured YAML output for evolution.toon.yaml
9
+
10
+ All public names are re-exported here for backward compatibility
11
+ with the original evolution_exporter.py module structure.
12
+ """
13
+
14
+ # Constants
15
+ from .constants import (
16
+ CC_SPLIT_THRESHOLD,
17
+ FAN_OUT_THRESHOLD,
18
+ GOD_MODULE_LINES,
19
+ HUB_TYPE_THRESHOLD,
20
+ EXCLUDE_PATTERNS,
21
+ )
22
+
23
+ # Exclusion
24
+ from .exclusion import is_excluded
25
+
26
+ # Computation
27
+ from .computation import (
28
+ compute_func_data,
29
+ scan_file_sizes,
30
+ aggregate_file_stats,
31
+ make_relative_path,
32
+ filter_god_modules,
33
+ compute_god_modules,
34
+ compute_hub_types,
35
+ build_context,
36
+ )
37
+
38
+ # Render
39
+ from .render import (
40
+ render_header,
41
+ render_next,
42
+ render_risks,
43
+ render_metrics_target,
44
+ render_patterns,
45
+ render_history,
46
+ )
47
+
48
+ # YAML Export
49
+ from .yaml_export import export_to_yaml
50
+
51
+ __all__ = [
52
+ # Constants
53
+ 'CC_SPLIT_THRESHOLD',
54
+ 'FAN_OUT_THRESHOLD',
55
+ 'GOD_MODULE_LINES',
56
+ 'HUB_TYPE_THRESHOLD',
57
+ 'EXCLUDE_PATTERNS',
58
+ # Exclusion
59
+ 'is_excluded',
60
+ # Computation
61
+ 'compute_func_data',
62
+ 'scan_file_sizes',
63
+ 'aggregate_file_stats',
64
+ 'make_relative_path',
65
+ 'filter_god_modules',
66
+ 'compute_god_modules',
67
+ 'compute_hub_types',
68
+ 'build_context',
69
+ # Render
70
+ 'render_header',
71
+ 'render_next',
72
+ 'render_risks',
73
+ 'render_metrics_target',
74
+ 'render_patterns',
75
+ 'render_history',
76
+ # YAML Export
77
+ 'export_to_yaml',
78
+ ]
@@ -0,0 +1,167 @@
1
+ """Evolution exporter computation — metrics calculation for god modules, hub types, etc."""
2
+
3
+ from collections import defaultdict
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from code2llm.core.models import AnalysisResult, FunctionInfo
8
+
9
+ from .constants import GOD_MODULE_LINES, HUB_TYPE_THRESHOLD, CC_SPLIT_THRESHOLD
10
+ from .exclusion import is_excluded
11
+
12
+
13
+ def compute_func_data(result: AnalysisResult) -> List[Dict]:
14
+ """Compute per-function metrics, excluding venv."""
15
+ func_data = []
16
+ for qname, fi in result.functions.items():
17
+ if is_excluded(fi.file):
18
+ continue
19
+ cc = fi.complexity.get("cyclomatic_complexity", 0)
20
+ fan_out = len(set(fi.calls))
21
+ fan_in = len(set(fi.called_by))
22
+ func_data.append({
23
+ "qname": qname, "name": fi.name,
24
+ "class_name": fi.class_name, "cc": cc,
25
+ "fan_out": fan_out, "fan_in": fan_in,
26
+ "impact": cc * max(fan_out, 1),
27
+ "file": fi.file, "module": fi.module,
28
+ })
29
+ return sorted(func_data, key=lambda x: x["impact"], reverse=True)
30
+
31
+
32
+ def scan_file_sizes(project_path: Optional[Path]) -> Dict[str, int]:
33
+ """Scan Python files and return line counts."""
34
+ file_lines: Dict[str, int] = {}
35
+ if not project_path or not project_path.is_dir():
36
+ return file_lines
37
+
38
+ for py in project_path.rglob("*.py"):
39
+ fpath = str(py)
40
+ if is_excluded(fpath):
41
+ continue
42
+ try:
43
+ lc = len(py.read_text(encoding="utf-8", errors="ignore").splitlines())
44
+ file_lines[fpath] = lc
45
+ except Exception:
46
+ pass
47
+ return file_lines
48
+
49
+
50
+ def aggregate_file_stats(
51
+ result: AnalysisResult,
52
+ file_lines: Dict[str, int]
53
+ ) -> Dict[str, Dict]:
54
+ """Aggregate function and class data per file."""
55
+ file_stats: Dict[str, Dict] = defaultdict(
56
+ lambda: {"lines": 0, "funcs": 0, "classes": set(), "max_cc": 0}
57
+ )
58
+
59
+ # Initialize with line counts
60
+ for fpath, lc in file_lines.items():
61
+ file_stats[fpath]["lines"] = lc
62
+
63
+ # Aggregate function data
64
+ for qname, fi in result.functions.items():
65
+ if is_excluded(fi.file):
66
+ continue
67
+ fs = file_stats[fi.file]
68
+ fs["funcs"] += 1
69
+ fs["max_cc"] = max(fs["max_cc"], fi.complexity.get("cyclomatic_complexity", 0))
70
+ if fi.class_name:
71
+ fs["classes"].add(fi.class_name)
72
+
73
+ # Aggregate class data
74
+ for qname, ci in result.classes.items():
75
+ if not is_excluded(ci.file):
76
+ file_stats[ci.file]["classes"].add(ci.name)
77
+
78
+ return file_stats
79
+
80
+
81
+ def make_relative_path(fpath: str, project_path: Optional[Path]) -> str:
82
+ """Convert absolute path to relative path."""
83
+ if not project_path:
84
+ return fpath
85
+ try:
86
+ return str(Path(fpath).relative_to(project_path))
87
+ except ValueError:
88
+ return fpath
89
+
90
+
91
+ def filter_god_modules(file_stats: Dict[str, Dict], project_path: Optional[Path]) -> List[Dict]:
92
+ """Filter files to god modules (≥500 lines)."""
93
+ god_modules = []
94
+ for fpath, stats in file_stats.items():
95
+ if stats["lines"] >= GOD_MODULE_LINES:
96
+ rel = make_relative_path(fpath, project_path)
97
+ god_modules.append({
98
+ "file": rel,
99
+ "lines": stats["lines"],
100
+ "funcs": stats["funcs"],
101
+ "classes": len(stats["classes"]),
102
+ "max_cc": stats["max_cc"],
103
+ })
104
+ god_modules.sort(key=lambda x: x["lines"], reverse=True)
105
+ return god_modules
106
+
107
+
108
+ def compute_god_modules(result: AnalysisResult) -> List[Dict]:
109
+ """Identify god modules (≥500 lines) from project files."""
110
+ pp = Path(result.project_path) if result.project_path else None
111
+
112
+ file_lines = scan_file_sizes(pp)
113
+ file_stats = aggregate_file_stats(result, file_lines)
114
+ return filter_god_modules(file_stats, pp)
115
+
116
+
117
+ def compute_hub_types(result: AnalysisResult) -> List[Dict]:
118
+ """Identify hub types consumed by many functions."""
119
+ type_consumers: Dict[str, int] = defaultdict(int)
120
+ type_producers: Dict[str, int] = defaultdict(int)
121
+ for qname, fi in result.functions.items():
122
+ ret = fi.complexity.get("return_type", "")
123
+ if ret:
124
+ type_producers[ret] += 1
125
+ for arg_type in fi.complexity.get("arg_types", []):
126
+ if arg_type:
127
+ type_consumers[arg_type] += 1
128
+ hub_types = [
129
+ {"type": t, "consumers": c, "producers": type_producers.get(t, 0)}
130
+ for t, c in type_consumers.items()
131
+ if c >= HUB_TYPE_THRESHOLD
132
+ ]
133
+ hub_types.sort(key=lambda x: x["consumers"], reverse=True)
134
+ return hub_types
135
+
136
+
137
+ def build_context(result: AnalysisResult) -> Dict[str, Any]:
138
+ """Build context dict with all computed metrics."""
139
+ ctx = {
140
+ "result": result,
141
+ }
142
+ ctx["funcs"] = compute_func_data(result)
143
+ ctx["god_modules"] = compute_god_modules(result)
144
+ ctx["hub_types"] = compute_hub_types(result)
145
+
146
+ # Overall metrics
147
+ all_cc = [f["cc"] for f in ctx["funcs"]]
148
+ ctx["avg_cc"] = round(sum(all_cc) / len(all_cc), 1) if all_cc else 0.0
149
+ ctx["max_cc"] = max(all_cc) if all_cc else 0
150
+ ctx["total_funcs"] = len(all_cc)
151
+ ctx["total_files"] = len(set(f["file"] for f in ctx["funcs"])) or 1
152
+ ctx["high_cc_count"] = len([c for c in all_cc if c >= CC_SPLIT_THRESHOLD])
153
+ ctx["critical_count"] = len([c for c in all_cc if c >= 10])
154
+
155
+ return ctx
156
+
157
+
158
+ __all__ = [
159
+ 'compute_func_data',
160
+ 'scan_file_sizes',
161
+ 'aggregate_file_stats',
162
+ 'make_relative_path',
163
+ 'filter_god_modules',
164
+ 'compute_god_modules',
165
+ 'compute_hub_types',
166
+ 'build_context',
167
+ ]
@@ -0,0 +1,25 @@
1
+ """Evolution exporter constants — thresholds and configuration."""
2
+
3
+ # Thresholds
4
+ CC_SPLIT_THRESHOLD = 15
5
+ FAN_OUT_THRESHOLD = 10
6
+ GOD_MODULE_LINES = 500
7
+ HUB_TYPE_THRESHOLD = 10
8
+
9
+
10
+ # Exclude patterns (mirrors ToonExporter)
11
+ EXCLUDE_PATTERNS = {
12
+ 'venv', '.venv', 'env', '.env', 'publish-env', 'test-env',
13
+ 'site-packages', 'node_modules', '__pycache__', '.git',
14
+ 'dist', 'build', 'egg-info', '.tox', '.mypy_cache',
15
+ 'examples', 'benchmarks', 'tests', 'scripts', 'demo_langs',
16
+ }
17
+
18
+
19
+ __all__ = [
20
+ 'CC_SPLIT_THRESHOLD',
21
+ 'FAN_OUT_THRESHOLD',
22
+ 'GOD_MODULE_LINES',
23
+ 'HUB_TYPE_THRESHOLD',
24
+ 'EXCLUDE_PATTERNS',
25
+ ]
@@ -0,0 +1,17 @@
1
+ """Evolution exporter exclusion logic — path filtering."""
2
+
3
+ from .constants import EXCLUDE_PATTERNS
4
+
5
+
6
+ def is_excluded(path: str) -> bool:
7
+ """Check if path should be excluded (venv, site-packages, etc.)."""
8
+ path_lower = path.lower().replace('\\', '/')
9
+ for pattern in EXCLUDE_PATTERNS:
10
+ if f'/{pattern}/' in path_lower or path_lower.startswith(f'{pattern}/'):
11
+ return True
12
+ if pattern in path_lower.split('/'):
13
+ return True
14
+ return False
15
+
16
+
17
+ __all__ = ['is_excluded']
@@ -0,0 +1,195 @@
1
+ """Evolution exporter render — text output generation for evolution.toon."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List
6
+
7
+ from .constants import CC_SPLIT_THRESHOLD
8
+
9
+
10
+ def render_header(ctx: Dict[str, Any]) -> List[str]:
11
+ """Render header line."""
12
+ return [
13
+ f"# code2llm/evolution | {ctx['total_funcs']} func"
14
+ f" | {ctx['total_files']}f | {datetime.now().strftime('%Y-%m-%d')}",
15
+ ]
16
+
17
+
18
+ def render_next(ctx: Dict[str, Any]) -> List[str]:
19
+ """Render NEXT — ranked refactoring queue."""
20
+ actions: List[Dict[str, Any]] = []
21
+
22
+ # 1. God modules → split
23
+ for gm in ctx["god_modules"][:3]:
24
+ actions.append({
25
+ "priority": "!!",
26
+ "action": "SPLIT",
27
+ "target": gm["file"],
28
+ "why": f"{gm['lines']}L, {gm['classes']} classes, max CC={gm['max_cc']}",
29
+ "effort": "~4h",
30
+ "impact_score": gm["lines"] * gm["max_cc"],
31
+ })
32
+
33
+ # 2. High CC functions → split
34
+ for f in ctx["funcs"][:20]:
35
+ if f["cc"] >= CC_SPLIT_THRESHOLD:
36
+ display = f["name"]
37
+ if f["class_name"]:
38
+ display = f"{f['class_name']}.{f['name']}"
39
+ actions.append({
40
+ "priority": "!!" if f["cc"] >= 25 else "!",
41
+ "action": "SPLIT-FUNC",
42
+ "target": f"{display} CC={f['cc']} fan={f['fan_out']}",
43
+ "why": f"CC={f['cc']} exceeds {CC_SPLIT_THRESHOLD}",
44
+ "effort": "~1h",
45
+ "impact_score": f["impact"],
46
+ })
47
+
48
+ # 3. Hub types → interface segregation
49
+ for ht in ctx["hub_types"][:3]:
50
+ if ht["consumers"] >= 20:
51
+ actions.append({
52
+ "priority": "!",
53
+ "action": "INTERFACE-SPLIT",
54
+ "target": f"{ht['type']} consumed:{ht['consumers']}",
55
+ "why": f"Hub type with {ht['consumers']} consumers → split interface",
56
+ "effort": "~6h",
57
+ "impact_score": ht["consumers"] * 10,
58
+ })
59
+
60
+ # Sort by impact and limit
61
+ actions.sort(key=lambda x: x["impact_score"], reverse=True)
62
+ actions = actions[:10]
63
+
64
+ if not actions:
65
+ return ["NEXT[0]: no refactoring needed"]
66
+
67
+ lines = [f"NEXT[{len(actions)}] (ranked by impact):"]
68
+ for i, a in enumerate(actions, 1):
69
+ lines.append(
70
+ f" [{i}] {a['priority']:2s} {a['action']:15s} {a['target']}"
71
+ )
72
+ lines.append(
73
+ f" WHY: {a['why']}"
74
+ )
75
+ lines.append(
76
+ f" EFFORT: {a['effort']} IMPACT: {a['impact_score']}"
77
+ )
78
+ lines.append("")
79
+
80
+ return lines
81
+
82
+
83
+ def render_risks(ctx: Dict[str, Any]) -> List[str]:
84
+ """Render RISKS — potential breaking changes."""
85
+ risks: List[str] = []
86
+
87
+ # God module splits may break imports
88
+ for gm in ctx["god_modules"][:3]:
89
+ risks.append(
90
+ f"⚠ Splitting {gm['file']} may break {gm['funcs']} import paths"
91
+ )
92
+
93
+ # Hub type splits change public API
94
+ for ht in ctx["hub_types"][:2]:
95
+ if ht["consumers"] >= 20:
96
+ risks.append(
97
+ f"⚠ Splitting {ht['type']} changes API for {ht['consumers']} consumers"
98
+ )
99
+
100
+ if not risks:
101
+ return ["RISKS[0]: none"]
102
+
103
+ lines = [f"RISKS[{len(risks)}]:"]
104
+ for r in risks:
105
+ lines.append(f" {r}")
106
+ return lines
107
+
108
+
109
+ def render_metrics_target(ctx: Dict[str, Any]) -> List[str]:
110
+ """Render METRICS-TARGET — baseline vs goals."""
111
+ avg = ctx["avg_cc"]
112
+ max_cc = ctx["max_cc"]
113
+ gods = len(ctx["god_modules"])
114
+ hubs = len(ctx["hub_types"])
115
+ high = ctx["high_cc_count"]
116
+
117
+ # Compute targets (halve the worst metrics)
118
+ target_avg = round(min(avg * 0.7, 5.0), 1)
119
+ target_max = min(max_cc // 2, 20)
120
+ target_gods = 0
121
+ target_high = max(high // 2, 0)
122
+
123
+ lines = [
124
+ "METRICS-TARGET:",
125
+ f" CC̄: {avg} → ≤{target_avg}",
126
+ f" max-CC: {max_cc} → ≤{target_max}",
127
+ f" god-modules: {gods} → {target_gods}",
128
+ f" high-CC(≥{CC_SPLIT_THRESHOLD}): {high} → ≤{target_high}",
129
+ f" hub-types: {hubs} → ≤{max(hubs - 2, 0)}",
130
+ ]
131
+ return lines
132
+
133
+
134
+ def render_patterns(ctx: Dict[str, Any]) -> List[str]:
135
+ """Render PATTERNS — shared language parser extraction patterns."""
136
+ lines = [
137
+ "PATTERNS (language parser shared logic):",
138
+ " _extract_declarations() in base.py — unified extraction for:",
139
+ " - TypeScript: interfaces, types, classes, functions, arrow funcs",
140
+ " - PHP: namespaces, traits, classes, functions, includes",
141
+ " - Ruby: modules, classes, methods, requires",
142
+ " - C++: classes, structs, functions, #includes",
143
+ " - C#: classes, interfaces, methods, usings",
144
+ " - Java: classes, interfaces, methods, imports",
145
+ " - Go: packages, functions, structs",
146
+ " - Rust: modules, functions, traits, use statements",
147
+ "",
148
+ " Shared regex patterns per language:",
149
+ " - import: language-specific import/require/using patterns",
150
+ " - class: class/struct/trait declarations with inheritance",
151
+ " - function: function/method signatures with visibility",
152
+ " - brace_tracking: for C-family languages ({ })",
153
+ " - end_keyword_tracking: for Ruby (module/class/def...end)",
154
+ "",
155
+ " Benefits:",
156
+ " - Consistent extraction logic across all languages",
157
+ " - Reduced code duplication (~70% reduction in parser LOC)",
158
+ " - Easier maintenance: fix once, apply everywhere",
159
+ " - Standardized FunctionInfo/ClassInfo models",
160
+ ]
161
+ return lines
162
+
163
+
164
+ def render_history(ctx: Dict[str, Any], output_path: str) -> List[str]:
165
+ """Render HISTORY — load previous evolution.toon.yaml if exists."""
166
+ lines = ["HISTORY:"]
167
+
168
+ prev_path = Path(output_path)
169
+ if prev_path.exists():
170
+ try:
171
+ prev_content = prev_path.read_text(encoding="utf-8")
172
+ # Extract previous metrics line
173
+ for line in prev_content.splitlines():
174
+ if line.strip().startswith("CC̄:"):
175
+ prev_avg = line.split("→")[0].strip().split()[-1]
176
+ lines.append(f" prev CC̄={prev_avg} → now CC̄={ctx['avg_cc']}")
177
+ break
178
+ else:
179
+ lines.append(f" previous evolution.toon.yaml found but no metrics parsed")
180
+ except Exception:
181
+ lines.append(f" (could not read previous evolution.toon.yaml)")
182
+ else:
183
+ lines.append(f" (first run — no previous data)")
184
+
185
+ return lines
186
+
187
+
188
+ __all__ = [
189
+ 'render_header',
190
+ 'render_next',
191
+ 'render_risks',
192
+ 'render_metrics_target',
193
+ 'render_patterns',
194
+ 'render_history',
195
+ ]
@@ -0,0 +1,103 @@
1
+ """Evolution exporter YAML output — evolution.toon.yaml structured format."""
2
+
3
+ import yaml
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List
6
+
7
+ from code2llm.core.models import AnalysisResult
8
+
9
+ from .constants import CC_SPLIT_THRESHOLD
10
+ from .computation import build_context
11
+
12
+
13
+ def export_to_yaml(result: AnalysisResult, output_path: str) -> None:
14
+ """Generate evolution.toon.yaml (structured YAML)."""
15
+ ctx = build_context(result)
16
+
17
+ # Build refactoring actions
18
+ actions = []
19
+ for gm in ctx["god_modules"][:3]:
20
+ actions.append({
21
+ "priority": "high",
22
+ "action": "SPLIT",
23
+ "target": gm["file"],
24
+ "reason": f"{gm['lines']}L, {gm['classes']} classes, max CC={gm['max_cc']}",
25
+ "effort": "~4h",
26
+ })
27
+
28
+ for f in ctx["funcs"][:20]:
29
+ if f["cc"] >= CC_SPLIT_THRESHOLD:
30
+ actions.append({
31
+ "priority": "critical" if f["cc"] >= 25 else "high",
32
+ "action": "SPLIT-FUNC",
33
+ "target": f"{f['class_name']}.{f['name']}" if f["class_name"] else f["name"],
34
+ "cc": f["cc"],
35
+ "fan_out": f["fan_out"],
36
+ "reason": f"CC={f['cc']} exceeds {CC_SPLIT_THRESHOLD}",
37
+ "effort": "~1h",
38
+ })
39
+
40
+ for ht in ctx["hub_types"][:3]:
41
+ if ht["consumers"] >= 20:
42
+ actions.append({
43
+ "priority": "medium",
44
+ "action": "INTERFACE-SPLIT",
45
+ "target": ht["type"],
46
+ "consumers": ht["consumers"],
47
+ "reason": f"Hub type with {ht['consumers']} consumers",
48
+ "effort": "~6h",
49
+ })
50
+
51
+ actions.sort(key=lambda x: x.get("priority", "") == "critical", reverse=True)
52
+
53
+ # Build risks
54
+ risks = []
55
+ for gm in ctx["god_modules"][:3]:
56
+ risks.append({
57
+ "type": "breaking_imports",
58
+ "target": gm["file"],
59
+ "impact": f"may break {gm['funcs']} import paths",
60
+ })
61
+ for ht in ctx["hub_types"][:2]:
62
+ if ht["consumers"] >= 20:
63
+ risks.append({
64
+ "type": "api_change",
65
+ "target": ht["type"],
66
+ "impact": f"changes API for {ht['consumers']} consumers",
67
+ })
68
+
69
+ from datetime import datetime
70
+ data = {
71
+ "format": "evolution-toon-yaml",
72
+ "timestamp": datetime.now().strftime("%Y-%m-%d"),
73
+ "stats": {
74
+ "total_funcs": ctx["total_funcs"],
75
+ "total_files": ctx["total_files"],
76
+ "avg_cc": ctx["avg_cc"],
77
+ "max_cc": ctx["max_cc"],
78
+ "high_cc_count": ctx["high_cc_count"],
79
+ "critical_count": ctx["critical_count"],
80
+ },
81
+ "refactoring": {
82
+ "action_count": len(actions),
83
+ "actions": actions[:10],
84
+ },
85
+ "risks": {
86
+ "count": len(risks),
87
+ "items": risks,
88
+ },
89
+ "metrics_target": {
90
+ "avg_cc": {"current": ctx["avg_cc"], "target": round(min(ctx["avg_cc"] * 0.7, 5.0), 1)},
91
+ "max_cc": {"current": ctx["max_cc"], "target": min(ctx["max_cc"] // 2, 20)},
92
+ "god_modules": {"current": len(ctx["god_modules"]), "target": 0},
93
+ "high_cc": {"current": ctx["high_cc_count"], "target": max(ctx["high_cc_count"] // 2, 0)},
94
+ "hub_types": {"current": len(ctx["hub_types"]), "target": max(len(ctx["hub_types"]) - 2, 0)},
95
+ },
96
+ }
97
+
98
+ Path(output_path).parent.mkdir(parents=True, exist_ok=True)
99
+ with open(output_path, "w", encoding="utf-8") as f:
100
+ yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
101
+
102
+
103
+ __all__ = ['export_to_yaml']