code2llm 0.5.145__tar.gz → 0.5.146__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 (197) hide show
  1. {code2llm-0.5.145/code2llm.egg-info → code2llm-0.5.146}/PKG-INFO +5 -7
  2. {code2llm-0.5.145 → code2llm-0.5.146}/README.md +4 -6
  3. code2llm-0.5.146/VERSION +1 -0
  4. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/__init__.py +1 -1
  5. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_exports/formats.py +26 -6
  6. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_exports/orchestrator.py +52 -1
  7. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_exports/orchestrator_handlers.py +36 -11
  8. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_exports/prompt.py +6 -1
  9. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/project_yaml/core.py +1 -1
  10. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon/helpers.py +40 -9
  11. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon/metrics.py +1 -1
  12. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon/metrics_core.py +49 -57
  13. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/nlp/__init__.py +1 -1
  14. {code2llm-0.5.145 → code2llm-0.5.146/code2llm.egg-info}/PKG-INFO +5 -7
  15. {code2llm-0.5.145 → code2llm-0.5.146}/pyproject.toml +1 -1
  16. code2llm-0.5.145/VERSION +0 -1
  17. {code2llm-0.5.145 → code2llm-0.5.146}/LICENSE +0 -0
  18. {code2llm-0.5.145 → code2llm-0.5.146}/MANIFEST.in +0 -0
  19. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/__main__.py +0 -0
  20. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/__init__.py +0 -0
  21. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/call_graph.py +0 -0
  22. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/cfg.py +0 -0
  23. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/coupling.py +0 -0
  24. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/data_analysis.py +0 -0
  25. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/dfg.py +0 -0
  26. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/pipeline_classifier.py +0 -0
  27. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/pipeline_detector.py +0 -0
  28. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/pipeline_resolver.py +0 -0
  29. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/side_effects.py +0 -0
  30. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/smells.py +0 -0
  31. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/type_inference.py +0 -0
  32. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/utils/__init__.py +0 -0
  33. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/analysis/utils/ast_helpers.py +0 -0
  34. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/api.py +0 -0
  35. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli.py +0 -0
  36. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_analysis.py +0 -0
  37. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_commands.py +0 -0
  38. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_exports/__init__.py +0 -0
  39. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_exports/code2logic.py +0 -0
  40. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_exports/orchestrator_chunked.py +0 -0
  41. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_exports/orchestrator_constants.py +0 -0
  42. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/cli_parser.py +0 -0
  43. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/__init__.py +0 -0
  44. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/analyzer.py +0 -0
  45. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/ast_registry.py +0 -0
  46. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/config.py +0 -0
  47. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/export_pipeline.py +0 -0
  48. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/file_analyzer.py +0 -0
  49. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/file_cache.py +0 -0
  50. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/file_filter.py +0 -0
  51. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/gitignore.py +0 -0
  52. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/incremental.py +0 -0
  53. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/__init__.py +0 -0
  54. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/base.py +0 -0
  55. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/cpp.py +0 -0
  56. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/csharp.py +0 -0
  57. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/generic.py +0 -0
  58. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/go_lang.py +0 -0
  59. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/java.py +0 -0
  60. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/php.py +0 -0
  61. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/ruby.py +0 -0
  62. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/rust.py +0 -0
  63. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/ts_extractors.py +0 -0
  64. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/ts_parser.py +0 -0
  65. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/lang/typescript.py +0 -0
  66. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/large_repo.py +0 -0
  67. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/models.py +0 -0
  68. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/persistent_cache.py +0 -0
  69. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/refactoring.py +0 -0
  70. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/repo_files.py +0 -0
  71. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/streaming/__init__.py +0 -0
  72. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/streaming/cache.py +0 -0
  73. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/streaming/incremental.py +0 -0
  74. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/streaming/prioritizer.py +0 -0
  75. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/streaming/scanner.py +0 -0
  76. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/streaming/strategies.py +0 -0
  77. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/streaming_analyzer.py +0 -0
  78. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/core/toon_size_manager.py +0 -0
  79. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/__init__.py +0 -0
  80. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/article_view.py +0 -0
  81. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/base.py +0 -0
  82. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/context_exporter.py +0 -0
  83. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/context_view.py +0 -0
  84. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/dashboard_data.py +0 -0
  85. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/dashboard_renderer.py +0 -0
  86. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/evolution/__init__.py +0 -0
  87. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/evolution/computation.py +0 -0
  88. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/evolution/constants.py +0 -0
  89. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/evolution/exclusion.py +0 -0
  90. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/evolution/render.py +0 -0
  91. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/evolution/yaml_export.py +0 -0
  92. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/evolution_exporter.py +0 -0
  93. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/flow_constants.py +0 -0
  94. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/flow_exporter.py +0 -0
  95. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/flow_renderer.py +0 -0
  96. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/html_dashboard.py +0 -0
  97. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/index_generator/__init__.py +0 -0
  98. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/index_generator/renderer.py +0 -0
  99. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/index_generator/scanner.py +0 -0
  100. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/index_generator.py +0 -0
  101. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/json_exporter.py +0 -0
  102. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/llm_exporter.py +0 -0
  103. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/map/__init__.py +0 -0
  104. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/map/alerts.py +0 -0
  105. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/map/details.py +0 -0
  106. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/map/header.py +0 -0
  107. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/map/module_list.py +0 -0
  108. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/map/utils.py +0 -0
  109. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/map/yaml_export.py +0 -0
  110. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/map_exporter.py +0 -0
  111. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid/__init__.py +0 -0
  112. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid/calls.py +0 -0
  113. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid/classic.py +0 -0
  114. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid/compact.py +0 -0
  115. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid/flow_compact.py +0 -0
  116. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid/flow_detailed.py +0 -0
  117. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid/flow_full.py +0 -0
  118. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid/utils.py +0 -0
  119. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid_exporter.py +0 -0
  120. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/mermaid_flow_helpers.py +0 -0
  121. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/project_yaml/__init__.py +0 -0
  122. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/project_yaml/constants.py +0 -0
  123. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/project_yaml/evolution.py +0 -0
  124. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/project_yaml/health.py +0 -0
  125. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/project_yaml/hotspots.py +0 -0
  126. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/project_yaml/modules.py +0 -0
  127. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/project_yaml_exporter.py +0 -0
  128. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/readme/__init__.py +0 -0
  129. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/readme/content.py +0 -0
  130. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/readme/files.py +0 -0
  131. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/readme/insights.py +0 -0
  132. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/readme/sections.py +0 -0
  133. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/readme_exporter.py +0 -0
  134. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/report_generators.py +0 -0
  135. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon/__init__.py +0 -0
  136. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon/metrics_duplicates.py +0 -0
  137. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon/metrics_health.py +0 -0
  138. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon/module_detail.py +0 -0
  139. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon/renderer.py +0 -0
  140. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon.py +0 -0
  141. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/toon_view.py +0 -0
  142. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/validate_project.py +0 -0
  143. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/exporters/yaml_exporter.py +0 -0
  144. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/__init__.py +0 -0
  145. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/_utils.py +0 -0
  146. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/llm_flow/__init__.py +0 -0
  147. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/llm_flow/analysis.py +0 -0
  148. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/llm_flow/cli.py +0 -0
  149. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/llm_flow/generator.py +0 -0
  150. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/llm_flow/nodes.py +0 -0
  151. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/llm_flow/parsing.py +0 -0
  152. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/llm_flow/utils.py +0 -0
  153. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/llm_flow.py +0 -0
  154. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/llm_task.py +0 -0
  155. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/mermaid/__init__.py +0 -0
  156. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/mermaid/fix.py +0 -0
  157. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/mermaid/png.py +0 -0
  158. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/mermaid/validation.py +0 -0
  159. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/generators/mermaid.py +0 -0
  160. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/nlp/config.py +0 -0
  161. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/nlp/entity_resolution.py +0 -0
  162. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/nlp/intent_matching.py +0 -0
  163. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/nlp/normalization.py +0 -0
  164. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/nlp/pipeline.py +0 -0
  165. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/parsers/toon_parser.py +0 -0
  166. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/patterns/__init__.py +0 -0
  167. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/patterns/detector.py +0 -0
  168. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/refactor/__init__.py +0 -0
  169. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm/refactor/prompt_engine.py +0 -0
  170. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm.egg-info/SOURCES.txt +0 -0
  171. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm.egg-info/dependency_links.txt +0 -0
  172. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm.egg-info/entry_points.txt +0 -0
  173. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm.egg-info/requires.txt +0 -0
  174. {code2llm-0.5.145 → code2llm-0.5.146}/code2llm.egg-info/top_level.txt +0 -0
  175. {code2llm-0.5.145 → code2llm-0.5.146}/setup.cfg +0 -0
  176. {code2llm-0.5.145 → code2llm-0.5.146}/setup.py +0 -0
  177. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_advanced_analysis.py +0 -0
  178. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_analyzer.py +0 -0
  179. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_cache_invalidation_e2e.py +0 -0
  180. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_calls_toon_export.py +0 -0
  181. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_declarative_collection.py +0 -0
  182. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_deep_analysis.py +0 -0
  183. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_edge_cases.py +0 -0
  184. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_file_analyzer_tagging.py +0 -0
  185. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_flow_exporter.py +0 -0
  186. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_format_quality.py +0 -0
  187. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_multilanguage_e2e.py +0 -0
  188. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_nlp_pipeline.py +0 -0
  189. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_nonpython_cc_calls.py +0 -0
  190. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_orchestrator_cache_mtime.py +0 -0
  191. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_persistent_cache.py +0 -0
  192. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_pipeline_detector.py +0 -0
  193. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_project_toon_export.py +0 -0
  194. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_prompt_engine.py +0 -0
  195. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_prompt_txt.py +0 -0
  196. {code2llm-0.5.145 → code2llm-0.5.146}/tests/test_refactoring_engine.py +0 -0
  197. {code2llm-0.5.145 → code2llm-0.5.146}/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.145
3
+ Version: 0.5.146
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,18 +67,16 @@ 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.145-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
71
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-71.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
70
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.146-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
71
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-72.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
72
72
 
73
- - 🤖 **LLM usage:** $7.5000 (197 commits)
74
- - 👤 **Human dev:** ~$7124 (71.2h @ $100/h, 30min dedup)
73
+ - 🤖 **LLM usage:** $7.5000 (198 commits)
74
+ - 👤 **Human dev:** ~$7224 (72.2h @ $100/h, 30min dedup)
75
75
 
76
76
  Generated on 2026-05-06 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
77
77
 
78
78
  ---
79
79
 
80
-
81
-
82
80
  This directory contains the complete analysis of your project generated by `code2llm`. Each file serves a specific purpose for understanding, refactoring, and documenting your codebase.
83
81
 
84
82
  ## 📁 Generated Files Overview
@@ -3,18 +3,16 @@
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.145-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-71.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.146-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-72.2h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (197 commits)
10
- - 👤 **Human dev:** ~$7124 (71.2h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (198 commits)
10
+ - 👤 **Human dev:** ~$7224 (72.2h @ $100/h, 30min dedup)
11
11
 
12
12
  Generated on 2026-05-06 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
-
17
-
18
16
  This directory contains the complete analysis of your project generated by `code2llm`. Each file serves a specific purpose for understanding, refactoring, and documenting your codebase.
19
17
 
20
18
  ## 📁 Generated Files Overview
@@ -0,0 +1 @@
1
+ 0.5.146
@@ -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.145"
11
+ __version__ = "0.5.146"
12
12
  __author__ = "STTS Project"
13
13
 
14
14
  # Core analysis components (lightweight, always needed)
@@ -2,6 +2,7 @@
2
2
 
3
3
  import os
4
4
  import sys
5
+ import time
5
6
  from pathlib import Path
6
7
  from typing import Optional
7
8
 
@@ -13,6 +14,7 @@ from code2llm.exporters import (
13
14
  ArticleViewGenerator, HTMLDashboardGenerator,
14
15
  load_project_yaml, IndexHTMLGenerator,
15
16
  )
17
+ from .orchestrator import _inject_generation_time as _inject_time
16
18
 
17
19
 
18
20
  def _export_evolution(args, result, output_dir: Path):
@@ -21,9 +23,12 @@ def _export_evolution(args, result, output_dir: Path):
21
23
  return
22
24
  exporter = EvolutionExporter()
23
25
  filepath = output_dir / 'evolution.toon.yaml'
26
+ t0 = time.monotonic()
24
27
  exporter.export(result, str(filepath))
28
+ elapsed = time.monotonic() - t0
29
+ _inject_time(filepath, elapsed)
25
30
  if args.verbose:
26
- print(f" - EVOLUTION (refactoring queue): {filepath}")
31
+ print(f" - EVOLUTION (refactoring queue): {filepath} ({elapsed:.2f}s)")
27
32
 
28
33
 
29
34
  def _export_data_structures(args, result, output_dir: Path):
@@ -43,9 +48,12 @@ def _export_context_fallback(args, result, output_dir: Path, formats: list):
43
48
  return
44
49
  exporter = ContextExporter()
45
50
  filepath = output_dir / 'context.md'
51
+ t0 = time.monotonic()
46
52
  exporter.export(result, str(filepath))
53
+ elapsed = time.monotonic() - t0
54
+ _inject_time(filepath, elapsed)
47
55
  if args.verbose:
48
- print(f" - CONTEXT (LLM narrative): {filepath}")
56
+ print(f" - CONTEXT (LLM narrative): {filepath} ({elapsed:.2f}s)")
49
57
 
50
58
 
51
59
  def _export_readme(args, result, output_dir: Path):
@@ -54,18 +62,24 @@ def _export_readme(args, result, output_dir: Path):
54
62
  return
55
63
  exporter = READMEExporter()
56
64
  filepath = output_dir / 'README.md'
65
+ t0 = time.monotonic()
57
66
  exporter.export(result, str(filepath))
67
+ elapsed = time.monotonic() - t0
68
+ _inject_time(filepath, elapsed)
58
69
  if args.verbose:
59
- print(f" - README (documentation): {filepath}")
70
+ print(f" - README (documentation): {filepath} ({elapsed:.2f}s)")
60
71
 
61
72
 
62
73
  def _export_project_yaml(args, result, output_dir: Path):
63
74
  """Export unified project.yaml — single source of truth."""
64
75
  exporter = ProjectYAMLExporter()
65
76
  filepath = output_dir / 'project.yaml'
77
+ t0 = time.monotonic()
66
78
  exporter.export(result, str(filepath))
79
+ elapsed = time.monotonic() - t0
80
+ _inject_time(filepath, elapsed)
67
81
  if getattr(args, 'verbose', False):
68
- print(f" - PROJECT-YAML (single source of truth): {filepath}")
82
+ print(f" - PROJECT-YAML (single source of truth): {filepath} ({elapsed:.2f}s)")
69
83
  return filepath
70
84
 
71
85
 
@@ -79,10 +93,13 @@ def _export_project_toon(args, result, output_dir: Path):
79
93
 
80
94
  exporter = ToonViewGenerator()
81
95
  filepath = output_dir / 'project.toon.yaml'
96
+ t0 = time.monotonic()
82
97
  exporter.generate(data, str(filepath))
98
+ elapsed = time.monotonic() - t0
99
+ _inject_time(filepath, elapsed)
83
100
 
84
101
  if getattr(args, 'verbose', False):
85
- print(f" - PROJECT-TOON (project overview): {filepath}")
102
+ print(f" - PROJECT-TOON (project overview): {filepath} ({elapsed:.2f}s)")
86
103
 
87
104
  return filepath
88
105
 
@@ -129,9 +146,12 @@ def _export_simple_formats(args, result, output_dir: Path, formats):
129
146
  exporter = exporter_cls()
130
147
  # Export as plain text TOON format with .toon.yaml extension
131
148
  filepath = output_dir / filename
149
+ t0 = time.monotonic()
132
150
  exporter.export(result, str(filepath))
151
+ elapsed = time.monotonic() - t0
152
+ _inject_time(filepath, elapsed)
133
153
  if args.verbose:
134
- print(f" - {label}: {filepath}")
154
+ print(f" - {label}: {filepath} ({elapsed:.2f}s)")
135
155
 
136
156
  # Unified project.yaml (single source of truth)
137
157
  if 'project-yaml' in formats:
@@ -7,6 +7,7 @@ Maintains backward compatibility with all existing --format values.
7
7
  import os
8
8
  import shutil
9
9
  import sys
10
+ import time
10
11
  from pathlib import Path
11
12
  from typing import Optional, List, Dict, Any
12
13
 
@@ -280,9 +281,12 @@ def _export_registry_formats(args, result, output_dir: Path, formats: List[str])
280
281
  kwargs = _get_format_kwargs(fmt, args)
281
282
 
282
283
  try:
284
+ t0 = time.monotonic()
283
285
  exporter.export(result, str(filepath), **kwargs)
286
+ elapsed = time.monotonic() - t0
287
+ _inject_generation_time(filepath, elapsed)
284
288
  if args.verbose:
285
- print(f" - {label}: {filepath}")
289
+ print(f" - {label}: {filepath} ({elapsed:.2f}s)")
286
290
  except Exception as e:
287
291
  if args.verbose:
288
292
  print(f" - {label} export failed: {e}", file=sys.stderr)
@@ -306,6 +310,53 @@ def _export_chunked(
306
310
  _chunked_impl(args, result, output_dir, source_path, formats, requested_formats)
307
311
 
308
312
 
313
+ def _inject_generation_time(filepath: Path, elapsed: float) -> None:
314
+ """Inject generation time comment into the second line of an exported file."""
315
+ try:
316
+ path = Path(filepath)
317
+ if not path.exists():
318
+ return
319
+ suffix = path.suffix.lower()
320
+ name = path.name.lower()
321
+
322
+ # Only inject into text-based files
323
+ if suffix not in ('.yaml', '.yml', '.md', '.txt', '.mmd', '.html', '.json', '.export'):
324
+ return
325
+
326
+ content = path.read_text(encoding='utf-8')
327
+ if not content:
328
+ return
329
+
330
+ tag = f"generated in {elapsed:.2f}s"
331
+
332
+ if suffix in ('.yaml', '.yml', '.mmd', '.export', '.txt') or name.endswith('.toon.yaml'):
333
+ # YAML/Mermaid/text: insert '# generated in X.XXs' after first line
334
+ lines = content.split('\n', 1)
335
+ if len(lines) == 2:
336
+ content = f"{lines[0]}\n# {tag}\n{lines[1]}"
337
+ else:
338
+ content = f"{lines[0]}\n# {tag}\n"
339
+ elif suffix == '.md':
340
+ # Markdown: insert HTML comment after first line
341
+ lines = content.split('\n', 1)
342
+ if len(lines) == 2:
343
+ content = f"{lines[0]}\n<!-- {tag} -->\n{lines[1]}"
344
+ else:
345
+ content = f"{lines[0]}\n<!-- {tag} -->\n"
346
+ elif suffix == '.html':
347
+ # HTML: insert comment after <!DOCTYPE or <html>
348
+ content = content.replace('\n', f'\n<!-- {tag} -->\n', 1)
349
+ elif suffix == '.json':
350
+ # JSON doesn't support comments — skip
351
+ return
352
+ else:
353
+ return
354
+
355
+ path.write_text(content, encoding='utf-8')
356
+ except Exception:
357
+ pass # Never fail the export pipeline for a comment
358
+
359
+
309
360
  # Backward-compatible aliases
310
361
  _export_single_project = _export_single
311
362
  _export_chunked_results = _export_chunked
@@ -4,6 +4,7 @@ This module contains all the individual export handler functions
4
4
  that were extracted from orchestrator.py to reduce its size.
5
5
  """
6
6
 
7
+ import time
7
8
  from pathlib import Path
8
9
  from typing import Any, Dict, List
9
10
 
@@ -20,10 +21,14 @@ from .orchestrator_constants import FORMAT_LABELS
20
21
 
21
22
  def _export_mermaid(args, result, output_dir: Path):
22
23
  """Export mermaid diagrams."""
24
+ from .orchestrator import _inject_generation_time
25
+
23
26
  exporter = MermaidExporter()
24
27
  include_examples = getattr(args, 'flow_include_examples', False)
25
28
 
26
29
  # Core diagrams
30
+ mmd_files = ['flow.mmd', 'calls.mmd', 'compact_flow.mmd']
31
+ t0 = time.monotonic()
27
32
  exporter.export_flow_compact(result, str(output_dir / 'flow.mmd'), include_examples)
28
33
  exporter.export_call_graph(result, str(output_dir / 'calls.mmd'))
29
34
  exporter.export_compact(result, str(output_dir / 'compact_flow.mmd'))
@@ -31,21 +36,27 @@ def _export_mermaid(args, result, output_dir: Path):
31
36
  # Optional detailed diagrams
32
37
  if getattr(args, 'flow_detail', False):
33
38
  exporter.export_flow_detailed(result, str(output_dir / 'flow_detailed.mmd'), include_examples)
39
+ mmd_files.append('flow_detailed.mmd')
34
40
  if getattr(args, 'flow_full', False):
35
41
  exporter.export_flow_full(result, str(output_dir / 'flow_full.mmd'), include_examples)
42
+ mmd_files.append('flow_full.mmd')
43
+ elapsed_mmd = time.monotonic() - t0
44
+
45
+ # Inject timing into each .mmd file
46
+ for mf in mmd_files:
47
+ _inject_generation_time(output_dir / mf, elapsed_mmd)
36
48
 
37
49
  # Also export calls.yaml/toon
38
50
  yaml_exporter = YAMLExporter()
51
+ t0 = time.monotonic()
39
52
  yaml_exporter.export_calls(result, str(output_dir / 'calls.yaml'))
40
53
  yaml_exporter.export_calls_toon(result, str(output_dir / 'calls.toon.yaml'))
54
+ elapsed_calls = time.monotonic() - t0
55
+ _inject_generation_time(output_dir / 'calls.toon.yaml', elapsed_calls)
41
56
 
42
57
  if args.verbose:
43
- files = ['flow.mmd', 'calls.mmd', 'compact_flow.mmd', 'calls.yaml']
44
- if getattr(args, 'flow_detail', False):
45
- files.append('flow_detailed.mmd')
46
- if getattr(args, 'flow_full', False):
47
- files.append('flow_full.mmd')
48
- print(f" - Mermaid: {output_dir}/*.mmd ({', '.join(files)})")
58
+ files = mmd_files + ['calls.yaml']
59
+ print(f" - Mermaid: {output_dir}/*.mmd ({', '.join(files)}) ({elapsed_mmd:.2f}s)")
49
60
 
50
61
  # PNG generation
51
62
  _export_mermaid_pngs(args, output_dir)
@@ -99,28 +110,38 @@ def _export_data_structures(args, result, output_dir: Path):
99
110
  def _export_project_toon(args, result, output_dir: Path):
100
111
  """Export project.toon.yaml from project.yaml data."""
101
112
  from ..exporters.project_yaml_exporter import ProjectYAMLExporter
113
+ from .orchestrator import _inject_generation_time
102
114
 
103
115
  project_yaml_exporter = ProjectYAMLExporter()
104
116
  prev_evolution = load_previous_evolution(output_dir / 'project.yaml')
105
117
  data = project_yaml_exporter._build_project_yaml(result, prev_evolution)
106
118
 
107
119
  generator = ToonViewGenerator()
108
- generator.generate(data, str(output_dir / 'project.toon.yaml'))
120
+ filepath = output_dir / 'project.toon.yaml'
121
+ t0 = time.monotonic()
122
+ generator.generate(data, str(filepath))
123
+ elapsed = time.monotonic() - t0
124
+ _inject_generation_time(filepath, elapsed)
109
125
 
110
126
  if args.verbose:
111
- print(f" - PROJECT-TOON (project overview): {output_dir / 'project.toon.yaml'}")
127
+ print(f" - PROJECT-TOON (project overview): {filepath} ({elapsed:.2f}s)")
112
128
 
113
129
 
114
130
  def _export_readme(args, result, output_dir: Path):
115
131
  """Export README.md."""
116
132
  if getattr(args, 'no_readme', False):
117
133
  return
134
+ from .orchestrator import _inject_generation_time
118
135
  exporter_cls = get_exporter('readme')
119
136
  if exporter_cls:
120
137
  exporter = exporter_cls()
121
- exporter.export(result, str(output_dir / 'README.md'))
138
+ filepath = output_dir / 'README.md'
139
+ t0 = time.monotonic()
140
+ exporter.export(result, str(filepath))
141
+ elapsed = time.monotonic() - t0
142
+ _inject_generation_time(filepath, elapsed)
122
143
  if args.verbose:
123
- print(f" - README (documentation): {output_dir / 'README.md'}")
144
+ print(f" - README (documentation): {filepath} ({elapsed:.2f}s)")
124
145
 
125
146
 
126
147
  def _export_index_html(args, output_dir: Path):
@@ -128,10 +149,14 @@ def _export_index_html(args, output_dir: Path):
128
149
  if 'all' not in getattr(args, 'format', ''):
129
150
  return
130
151
  try:
152
+ from .orchestrator import _inject_generation_time
131
153
  generator = IndexHTMLGenerator(output_dir)
154
+ t0 = time.monotonic()
132
155
  index_path = generator.generate()
156
+ elapsed = time.monotonic() - t0
157
+ _inject_generation_time(index_path, elapsed)
133
158
  if args.verbose:
134
- print(f" - INDEX (file browser): {index_path}")
159
+ print(f" - INDEX (file browser): {index_path} ({elapsed:.2f}s)")
135
160
  except Exception as e:
136
161
  if args.verbose:
137
162
  print(f" - INDEX generation failed: {e}", file=__import__('sys').stderr)
@@ -1,6 +1,7 @@
1
1
  """Prompt generation — prompt.txt for LLM consumption (regular and chunked)."""
2
2
 
3
3
  import sys
4
+ import time
4
5
  from pathlib import Path
5
6
  from typing import List, Optional, Tuple
6
7
 
@@ -30,9 +31,13 @@ def _export_prompt_txt(args, output_dir: Path, formats: list[str], source_path:
30
31
  lines.extend(_build_prompt_footer(chunked=False, file_analysis=file_analysis))
31
32
 
32
33
  prompt_path = output_dir / 'prompt.txt'
34
+ t0 = time.monotonic()
33
35
  prompt_path.write_text("\n".join(lines) + "\n", encoding='utf-8')
36
+ elapsed = time.monotonic() - t0
37
+ from .orchestrator import _inject_generation_time
38
+ _inject_generation_time(prompt_path, elapsed)
34
39
  if args.verbose:
35
- print(f" - PROMPT: {prompt_path}")
40
+ print(f" - PROMPT: {prompt_path} ({elapsed:.2f}s)")
36
41
 
37
42
 
38
43
  def _export_chunked_prompt_txt(args, output_dir: Path, formats: list[str], source_path: Optional[Path] = None, subprojects: list = None) -> None:
@@ -48,7 +48,7 @@ class ProjectYAMLExporter(BaseExporter):
48
48
  self, result: AnalysisResult, prev_evolution: List[Dict]
49
49
  ) -> Dict[str, Any]:
50
50
  """Build complete project.yaml structure."""
51
- line_counts = _scan_line_counts(result.project_path)
51
+ line_counts = _scan_line_counts(result.project_path, result=result)
52
52
  # Filter out venv/site-packages/etc — only count lines of non-excluded files
53
53
  filtered_lines = {
54
54
  k: v for k, v in line_counts.items()
@@ -83,15 +83,12 @@ def _hotspot_description(fi: FunctionInfo, fan_out: int) -> str:
83
83
  return f"calls {fan_out} functions"
84
84
 
85
85
 
86
- @lru_cache(maxsize=8)
87
- def _scan_line_counts(project_path) -> Dict[str, int]:
88
- """Scan project directory for all source file line counts.
86
+ def _scan_line_counts(project_path, result=None) -> Dict[str, int]:
87
+ """Get line counts for project files.
89
88
 
90
- Cached per project_path callers within the same process reuse the result
91
- instead of repeating the rglob + read_text I/O.
89
+ Fast path: derive from AnalysisResult modules (already parsed, no extra I/O).
90
+ Slow fallback: single os.walk pass reading files from disk.
92
91
  """
93
- from ...core.config import ALL_EXTENSIONS
94
-
95
92
  line_counts: Dict[str, int] = {}
96
93
  if not project_path:
97
94
  return line_counts
@@ -99,8 +96,35 @@ def _scan_line_counts(project_path) -> Dict[str, int]:
99
96
  if not pp.is_dir():
100
97
  return line_counts
101
98
 
102
- for ext in ALL_EXTENSIONS:
103
- for src_file in pp.rglob(f"*{ext}"):
99
+ # Fast path: use already-analyzed file data when available
100
+ if result is not None:
101
+ for mname, mi in getattr(result, 'modules', {}).items():
102
+ fpath = mi.file
103
+ if not fpath:
104
+ continue
105
+ try:
106
+ lc = len(Path(fpath).read_text(encoding="utf-8", errors="ignore").splitlines())
107
+ rel = str(Path(fpath).relative_to(pp))
108
+ line_counts[str(fpath)] = lc
109
+ line_counts[rel] = lc
110
+ except Exception:
111
+ pass
112
+ return line_counts
113
+
114
+ # Slow fallback: single walk instead of 73 rglob calls
115
+ from ...core.config import ALL_EXTENSIONS
116
+ ext_set = set(ALL_EXTENSIONS)
117
+ for root, dirs, files in Path(project_path).walk() if hasattr(Path, 'walk') else _walk_compat(pp):
118
+ # Prune excluded directories
119
+ dirs[:] = [d for d in dirs if d not in {
120
+ 'venv', '.venv', 'node_modules', '__pycache__', '.git',
121
+ 'dist', 'build', '.tox', '.mypy_cache', 'egg-info',
122
+ }]
123
+ for fname in files:
124
+ ext = Path(fname).suffix
125
+ if ext not in ext_set:
126
+ continue
127
+ src_file = root / fname
104
128
  try:
105
129
  lc = len(src_file.read_text(encoding="utf-8", errors="ignore").splitlines())
106
130
  rel = str(src_file.relative_to(pp))
@@ -109,3 +133,10 @@ def _scan_line_counts(project_path) -> Dict[str, int]:
109
133
  except Exception:
110
134
  pass
111
135
  return line_counts
136
+
137
+
138
+ def _walk_compat(path):
139
+ """os.walk compatibility wrapper for Path (Python < 3.12)."""
140
+ import os
141
+ for root, dirs, files in os.walk(path):
142
+ yield Path(root), dirs, files
@@ -35,7 +35,7 @@ class MetricsComputer:
35
35
  """Compute all metrics and return context dict."""
36
36
  self.result = result
37
37
  self.project_path = result.project_path
38
- self.line_counts = _scan_line_counts(self.project_path)
38
+ self.line_counts = _scan_line_counts(self.project_path, result=result)
39
39
 
40
40
  # Initialize specialized computers
41
41
  self._core = CoreMetricsComputer(self.line_counts, self.project_path)
@@ -80,55 +80,46 @@ class CoreMetricsComputer:
80
80
  "fan_in": 0,
81
81
  }
82
82
 
83
+ @staticmethod
84
+ def _build_suffix_index(result: AnalysisResult) -> Dict[str, List[Tuple[str, FunctionInfo]]]:
85
+ """Build reverse lookup: simple_name -> [(qualified_name, FunctionInfo)].
86
+
87
+ Replaces O(F) scans per call with O(1) dict lookups.
88
+ """
89
+ idx: Dict[str, List[Tuple[str, FunctionInfo]]] = defaultdict(list)
90
+ for qn, fi in result.functions.items():
91
+ simple = qn.rsplit(".", 1)[-1]
92
+ idx[simple].append((qn, fi))
93
+ return idx
94
+
83
95
  @staticmethod
84
96
  def _compute_fan_in(files: Dict, result: AnalysisResult) -> None:
85
97
  """Compute fan-in per file (how many other files call into this file)."""
86
98
  importers: Dict[str, set] = defaultdict(set)
99
+ suffix_idx = CoreMetricsComputer._build_suffix_index(result)
87
100
 
88
101
  for fname, fi in result.functions.items():
89
- CoreMetricsComputer._process_function_calls(fi, result, importers)
102
+ src_file = fi.file
103
+ # Forward: who calls me? (called_by)
104
+ for callee in fi.called_by:
105
+ callee_info = result.functions.get(callee)
106
+ if callee_info and callee_info.file != src_file:
107
+ importers[src_file].add(callee_info.file)
108
+ # Reverse: who do I call? → target file gets fan-in
109
+ for callee in fi.calls:
110
+ callee_info = result.functions.get(callee)
111
+ if callee_info and callee_info.file != src_file:
112
+ importers[callee_info.file].add(src_file)
113
+ elif not callee_info:
114
+ # O(1) suffix lookup instead of O(F) scan
115
+ for qn, ci in suffix_idx.get(callee, []):
116
+ if ci.file != src_file:
117
+ importers[ci.file].add(src_file)
118
+ break
90
119
 
91
120
  for fpath in files:
92
121
  files[fpath]["fan_in"] = len(importers.get(fpath, set()))
93
122
 
94
- @staticmethod
95
- def _process_function_calls(fi: FunctionInfo, result: AnalysisResult, importers: Dict[str, set]) -> None:
96
- """Process calls for a single function to compute fan-in."""
97
- src_file = fi.file
98
-
99
- # Forward: who calls me? (called_by)
100
- CoreMetricsComputer._process_called_by(fi, result, src_file, importers)
101
-
102
- # Reverse: who do I call? → target file gets fan-in
103
- CoreMetricsComputer._process_callee_calls(fi, result, src_file, importers)
104
-
105
- @staticmethod
106
- def _process_called_by(fi: FunctionInfo, result: AnalysisResult, src_file: str, importers: Dict[str, set]) -> None:
107
- """Process called_by relationships."""
108
- for callee in fi.called_by:
109
- callee_info = result.functions.get(callee)
110
- if callee_info and callee_info.file != src_file:
111
- importers[src_file].add(callee_info.file)
112
-
113
- @staticmethod
114
- def _process_callee_calls(fi: FunctionInfo, result: AnalysisResult, src_file: str, importers: Dict[str, set]) -> None:
115
- """Process callee relationships."""
116
- for callee in fi.calls:
117
- callee_info = result.functions.get(callee)
118
- if callee_info and callee_info.file != src_file:
119
- importers[callee_info.file].add(src_file)
120
- else:
121
- # Suffix match for unqualified names
122
- CoreMetricsComputer._handle_suffix_match(callee, result, src_file, importers)
123
-
124
- @staticmethod
125
- def _handle_suffix_match(callee: str, result: AnalysisResult, src_file: str, importers: Dict[str, set]) -> None:
126
- """Handle suffix matching for unqualified names."""
127
- for qn, ci in result.functions.items():
128
- if qn.endswith(f".{callee}") and ci.file != src_file:
129
- importers[ci.file].add(src_file)
130
- break
131
-
132
123
  def compute_package_metrics(
133
124
  self, files: Dict[str, Dict], result: AnalysisResult
134
125
  ) -> Dict[str, Dict[str, Any]]:
@@ -248,6 +239,12 @@ class CoreMetricsComputer:
248
239
  """Build coupling matrix from cross-module calls."""
249
240
  matrix: Dict[Tuple[str, str], int] = defaultdict(int)
250
241
 
242
+ # Build reverse index: simple_name -> [(qualified, module)] for O(1) lookup
243
+ suffix_idx: Dict[str, List[Tuple[str, str]]] = defaultdict(list)
244
+ for qn, mod in func_to_module.items():
245
+ simple = qn.rsplit(".", 1)[-1]
246
+ suffix_idx[simple].append((qn, mod))
247
+
251
248
  for qname, fi in result.functions.items():
252
249
  if _is_excluded(fi.file):
253
250
  continue
@@ -255,7 +252,9 @@ class CoreMetricsComputer:
255
252
  src_pkg = _package_of_module(src_mod)
256
253
 
257
254
  for callee in fi.calls:
258
- callee_mod = self._resolve_callee_module(callee, func_to_module, src_pkg)
255
+ callee_mod = self._resolve_callee_module(
256
+ callee, func_to_module, src_pkg, suffix_idx
257
+ )
259
258
  if callee_mod and callee_mod != src_mod:
260
259
  dst_pkg = _package_of_module(callee_mod)
261
260
  if dst_pkg and dst_pkg != src_pkg:
@@ -263,31 +262,24 @@ class CoreMetricsComputer:
263
262
 
264
263
  return matrix
265
264
 
266
- def _resolve_callee_module(self, callee: str, func_to_module: Dict[str, str], src_pkg: str) -> Optional[str]:
267
- """Resolve callee to a known function module."""
265
+ def _resolve_callee_module(
266
+ self, callee: str, func_to_module: Dict[str, str],
267
+ src_pkg: str, suffix_idx: Dict[str, List[Tuple[str, str]]],
268
+ ) -> Optional[str]:
269
+ """Resolve callee to a known function module (O(1) via suffix index)."""
268
270
  callee_mod = func_to_module.get(callee)
269
271
  if callee_mod:
270
272
  return callee_mod
271
273
 
272
- # Try suffix match — collect all candidates
273
- candidates = [
274
- (qn, mod) for qn, mod in func_to_module.items()
275
- if qn.endswith(f".{callee}")
276
- ]
277
-
274
+ candidates = suffix_idx.get(callee, [])
278
275
  if len(candidates) == 1:
279
276
  return candidates[0][1]
280
277
  elif candidates:
281
278
  # Prefer callee in same package as caller
282
- same_pkg = [
283
- (qn, mod) for qn, mod in candidates
284
- if _package_of_module(mod) == src_pkg
285
- ]
286
- if same_pkg:
287
- return same_pkg[0][1]
288
- else:
289
- # Pick first cross-package candidate
290
- return candidates[0][1]
279
+ for qn, mod in candidates:
280
+ if _package_of_module(mod) == src_pkg:
281
+ return mod
282
+ return candidates[0][1]
291
283
 
292
284
  return None
293
285
 
@@ -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.145"
7
+ __version__ = "0.5.146"
8
8
 
9
9
  from .pipeline import NLPPipeline
10
10
  from .normalization import QueryNormalizer