code2llm 0.5.180__tar.gz → 0.5.185__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 (233) hide show
  1. {code2llm-0.5.180/code2llm.egg-info → code2llm-0.5.185}/PKG-INFO +7 -6
  2. {code2llm-0.5.180 → code2llm-0.5.185}/README.md +6 -5
  3. code2llm-0.5.185/VERSION +1 -0
  4. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/__init__.py +1 -1
  5. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/smell_engine.py +11 -6
  6. code2llm-0.5.185/code2llm/autoupdate.py +178 -0
  7. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli.py +6 -0
  8. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_exports/code2logic.py +4 -0
  9. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_exports/formats.py +11 -0
  10. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_exports/orchestrator.py +2 -0
  11. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_exports/orchestrator_constants.py +3 -0
  12. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_parser.py +1 -1
  13. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/analyzer.py +59 -8
  14. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/config.py +1 -1
  15. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/file_filter.py +66 -11
  16. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/persistent_cache.py +14 -0
  17. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/streaming/prioritizer.py +22 -15
  18. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/__init__.py +8 -0
  19. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/flow_constants.py +4 -0
  20. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/map/module_list.py +1 -1
  21. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/map/utils.py +1 -1
  22. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/map/yaml_export.py +1 -1
  23. code2llm-0.5.185/code2llm/exporters/projections_exporter.py +139 -0
  24. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/_render_section_helpers.py +20 -11
  25. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/helpers.py +21 -2
  26. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/mermaid/png.py +45 -26
  27. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/nlp/__init__.py +1 -1
  28. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/refactor/prompt_engine.py +1 -1
  29. {code2llm-0.5.180 → code2llm-0.5.185/code2llm.egg-info}/PKG-INFO +7 -6
  30. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm.egg-info/SOURCES.txt +7 -1
  31. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm.egg-info/top_level.txt +0 -1
  32. {code2llm-0.5.180 → code2llm-0.5.185}/pyproject.toml +8 -2
  33. {code2llm-0.5.180 → code2llm-0.5.185}/setup.py +1 -1
  34. code2llm-0.5.185/tests/test_analysis_env.py +46 -0
  35. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_analyzer.py +69 -0
  36. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_edge_cases.py +33 -0
  37. code2llm-0.5.185/tests/test_perf_exporters.py +74 -0
  38. code2llm-0.5.185/tests/test_projections_exporter.py +130 -0
  39. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_prompt_engine.py +2 -2
  40. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_smells_data_clumps.py +51 -0
  41. code2llm-0.5.185/tests/test_version_parity.py +38 -0
  42. code2llm-0.5.180/VERSION +0 -1
  43. {code2llm-0.5.180 → code2llm-0.5.185}/LICENSE +0 -0
  44. {code2llm-0.5.180 → code2llm-0.5.185}/MANIFEST.in +0 -0
  45. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/__main__.py +0 -0
  46. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/__init__.py +0 -0
  47. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/_data_impl.py +0 -0
  48. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/call_graph.py +0 -0
  49. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/call_graph_engine.py +0 -0
  50. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/cfg.py +0 -0
  51. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/complexity.py +0 -0
  52. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/coupling.py +0 -0
  53. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/data_analysis.py +0 -0
  54. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/dfg.py +0 -0
  55. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/native_bridge.py +0 -0
  56. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/pipeline_classifier.py +0 -0
  57. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/pipeline_detector.py +0 -0
  58. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/pipeline_resolver.py +0 -0
  59. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/side_effects.py +0 -0
  60. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/smells.py +0 -0
  61. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/type_inference.py +0 -0
  62. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/utils/__init__.py +0 -0
  63. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/analysis/utils/ast_helpers.py +0 -0
  64. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/api.py +0 -0
  65. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_analysis.py +0 -0
  66. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_commands.py +0 -0
  67. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_exports/__init__.py +0 -0
  68. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_exports/orchestrator_chunked.py +0 -0
  69. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_exports/orchestrator_handlers.py +0 -0
  70. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/cli_exports/prompt.py +0 -0
  71. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/__init__.py +0 -0
  72. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/ast_registry.py +0 -0
  73. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/export_pipeline.py +0 -0
  74. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/file_analyzer.py +0 -0
  75. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/file_cache.py +0 -0
  76. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/gitignore.py +0 -0
  77. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/incremental.py +0 -0
  78. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/__init__.py +0 -0
  79. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/_c_parser.py +0 -0
  80. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/_calls.py +0 -0
  81. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/_complexity.py +0 -0
  82. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/base.py +0 -0
  83. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/cpp.py +0 -0
  84. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/csharp.py +0 -0
  85. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/generic.py +0 -0
  86. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/go_lang.py +0 -0
  87. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/java.py +0 -0
  88. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/js_analysis.py +0 -0
  89. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/php.py +0 -0
  90. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/ruby.py +0 -0
  91. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/rust.py +0 -0
  92. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/ts_extractors.py +0 -0
  93. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/ts_parser.py +0 -0
  94. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/lang/typescript.py +0 -0
  95. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/large_repo.py +0 -0
  96. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/models.py +0 -0
  97. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/refactoring.py +0 -0
  98. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/repo_files.py +0 -0
  99. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/source_classifier.py +0 -0
  100. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/streaming/__init__.py +0 -0
  101. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/streaming/cache.py +0 -0
  102. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/streaming/incremental.py +0 -0
  103. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/streaming/scanner.py +0 -0
  104. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/streaming/strategies.py +0 -0
  105. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/streaming_analyzer.py +0 -0
  106. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/core/toon_size_manager.py +0 -0
  107. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/article_view.py +0 -0
  108. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/base.py +0 -0
  109. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/context_exporter.py +0 -0
  110. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/context_view.py +0 -0
  111. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/dashboard_data.py +0 -0
  112. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/dashboard_renderer.py +0 -0
  113. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/evolution/__init__.py +0 -0
  114. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/evolution/computation.py +0 -0
  115. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/evolution/constants.py +0 -0
  116. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/evolution/exclusion.py +0 -0
  117. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/evolution/render.py +0 -0
  118. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/evolution/yaml_export.py +0 -0
  119. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/evolution_exporter.py +0 -0
  120. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/flow_exporter.py +0 -0
  121. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/flow_renderer.py +0 -0
  122. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/html_dashboard.py +0 -0
  123. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/index_generator/__init__.py +0 -0
  124. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/index_generator/renderer.py +0 -0
  125. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/index_generator/scanner.py +0 -0
  126. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/index_generator.py +0 -0
  127. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/json_exporter.py +0 -0
  128. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/llm_exporter.py +0 -0
  129. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/map/__init__.py +0 -0
  130. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/map/alerts.py +0 -0
  131. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/map/details.py +0 -0
  132. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/map/header.py +0 -0
  133. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/map_exporter.py +0 -0
  134. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid/__init__.py +0 -0
  135. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid/calls.py +0 -0
  136. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid/classic.py +0 -0
  137. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid/compact.py +0 -0
  138. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid/flow_compact.py +0 -0
  139. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid/flow_detailed.py +0 -0
  140. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid/flow_full.py +0 -0
  141. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid/utils.py +0 -0
  142. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid_exporter.py +0 -0
  143. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/mermaid_flow_helpers.py +0 -0
  144. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/planfile_tickets.py +0 -0
  145. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/project_yaml/__init__.py +0 -0
  146. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/project_yaml/constants.py +0 -0
  147. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/project_yaml/core.py +0 -0
  148. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/project_yaml/evolution.py +0 -0
  149. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/project_yaml/health.py +0 -0
  150. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/project_yaml/hotspots.py +0 -0
  151. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/project_yaml/modules.py +0 -0
  152. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/project_yaml_exporter.py +0 -0
  153. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/readme/__init__.py +0 -0
  154. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/readme/content.py +0 -0
  155. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/readme/files.py +0 -0
  156. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/readme/insights.py +0 -0
  157. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/readme/sections.py +0 -0
  158. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/readme_exporter.py +0 -0
  159. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/report_generators.py +0 -0
  160. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/__init__.py +0 -0
  161. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/_render_coupling_helpers.py +0 -0
  162. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/constants.py +0 -0
  163. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/metrics.py +0 -0
  164. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/metrics_core.py +0 -0
  165. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/metrics_duplicates.py +0 -0
  166. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/metrics_health.py +0 -0
  167. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/module_detail.py +0 -0
  168. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon/renderer.py +0 -0
  169. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon.py +0 -0
  170. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/toon_view.py +0 -0
  171. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/validate_project.py +0 -0
  172. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/exporters/yaml_exporter.py +0 -0
  173. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/__init__.py +0 -0
  174. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/_utils.py +0 -0
  175. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/llm_flow/__init__.py +0 -0
  176. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/llm_flow/analysis.py +0 -0
  177. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/llm_flow/cli.py +0 -0
  178. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/llm_flow/generator.py +0 -0
  179. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/llm_flow/nodes.py +0 -0
  180. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/llm_flow/parsing.py +0 -0
  181. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/llm_flow/utils.py +0 -0
  182. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/llm_flow.py +0 -0
  183. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/llm_task.py +0 -0
  184. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/mermaid/__init__.py +0 -0
  185. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/mermaid/fix.py +0 -0
  186. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/mermaid/validation.py +0 -0
  187. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/generators/mermaid.py +0 -0
  188. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/nlp/config.py +0 -0
  189. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/nlp/entity_resolution.py +0 -0
  190. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/nlp/intent_matching.py +0 -0
  191. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/nlp/normalization.py +0 -0
  192. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/nlp/pipeline.py +0 -0
  193. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/parsers/toon_parser.py +0 -0
  194. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/patterns/__init__.py +0 -0
  195. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/patterns/detector.py +0 -0
  196. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/refactor/__init__.py +0 -0
  197. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/templates/extract_method.md +0 -0
  198. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm/templates/move_method.md +0 -0
  199. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm.egg-info/dependency_links.txt +0 -0
  200. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm.egg-info/entry_points.txt +0 -0
  201. {code2llm-0.5.180 → code2llm-0.5.185}/code2llm.egg-info/requires.txt +0 -0
  202. {code2llm-0.5.180 → code2llm-0.5.185}/setup.cfg +0 -0
  203. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_advanced_analysis.py +0 -0
  204. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_analysis_cache_version.py +0 -0
  205. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_cache_invalidation_e2e.py +0 -0
  206. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_call_graph_engine.py +0 -0
  207. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_calls_toon_export.py +0 -0
  208. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_complexity.py +0 -0
  209. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_declarative_collection.py +0 -0
  210. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_deep_analysis.py +0 -0
  211. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_dup_groups.py +0 -0
  212. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_export_cache_flags.py +0 -0
  213. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_export_cache_ownership.py +0 -0
  214. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_export_profile_contract.py +0 -0
  215. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_file_analyzer_tagging.py +0 -0
  216. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_flow_exporter.py +0 -0
  217. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_format_quality.py +0 -0
  218. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_js_complexity_boundaries.py +0 -0
  219. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_mermaid_png.py +0 -0
  220. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_multilanguage_e2e.py +0 -0
  221. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_native_rust_bridge.py +0 -0
  222. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_nlp_pipeline.py +0 -0
  223. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_nonpython_cc_calls.py +0 -0
  224. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_orchestrator_cache_mtime.py +0 -0
  225. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_persistent_cache.py +0 -0
  226. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_pipeline_detector.py +0 -0
  227. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_planfile_tickets_exporter.py +0 -0
  228. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_project_toon_export.py +0 -0
  229. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_prompt_txt.py +0 -0
  230. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_refactoring_engine.py +0 -0
  231. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_smell_engine.py +0 -0
  232. {code2llm-0.5.180 → code2llm-0.5.185}/tests/test_smells_shotgun_surgery.py +0 -0
  233. {code2llm-0.5.180 → code2llm-0.5.185}/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.180
3
+ Version: 0.5.185
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: Tom Sapletta
@@ -77,13 +77,13 @@ Dynamic: requires-python
77
77
 
78
78
  ## AI Cost Tracking
79
79
 
80
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.176-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
81
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$8.32-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-105.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fdeep%2Fdeep--v4--pro-lightgrey)
80
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.183-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
81
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.57-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-140.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fdeep%2Fdeep--v4--pro-lightgrey)
82
82
 
83
- - 🤖 **LLM usage:** $8.3176 (263 commits)
84
- - 👤 **Human dev:** ~$10535 (105.3h @ $100/h, 30min dedup)
83
+ - 🤖 **LLM usage:** $0.5710 (336 commits)
84
+ - 👤 **Human dev:** ~$14034 (140.3h @ $100/h, 30min dedup)
85
85
 
86
- Generated on 2026-08-06 using [openrouter/deep/deep-v4-pro](https://openrouter.ai/deep/deep-v4-pro)
86
+ Generated on 2026-10-18 using [openrouter/deep/deep-v4-pro](https://openrouter.ai/deep/deep-v4-pro)
87
87
 
88
88
  ---
89
89
 
@@ -101,6 +101,7 @@ When you run `code2llm ./ -f all`, the following files are created:
101
101
  |------|--------|---------|--------------|
102
102
  | `evolution.toon.yaml` | **YAML** | **📋 Refactoring queue** - Prioritized improvements | 0 refactoring actions needed |
103
103
  | `map.toon.yaml` | **YAML** | **🗺️ Structural map + project header** - Modules, imports, exports, signatures, stats, alerts, hotspots, trend | Project architecture overview |
104
+ | `projections.json` | **JSON** | **🔎 Bounded symbol catalog** (`code2llm.projection/v1`) for intent retrieval / external indexers — deterministic, digested, capped at 2048 records | Reuse contract without analyzer internals |
104
105
 
105
106
  ### 🤖 LLM-Ready Documentation
106
107
 
@@ -2,13 +2,13 @@
2
2
 
3
3
  ## AI Cost Tracking
4
4
 
5
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.176-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
6
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$8.32-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-105.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fdeep%2Fdeep--v4--pro-lightgrey)
5
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.5.183-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
6
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.57-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-140.3h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fdeep%2Fdeep--v4--pro-lightgrey)
7
7
 
8
- - 🤖 **LLM usage:** $8.3176 (263 commits)
9
- - 👤 **Human dev:** ~$10535 (105.3h @ $100/h, 30min dedup)
8
+ - 🤖 **LLM usage:** $0.5710 (336 commits)
9
+ - 👤 **Human dev:** ~$14034 (140.3h @ $100/h, 30min dedup)
10
10
 
11
- Generated on 2026-08-06 using [openrouter/deep/deep-v4-pro](https://openrouter.ai/deep/deep-v4-pro)
11
+ Generated on 2026-10-18 using [openrouter/deep/deep-v4-pro](https://openrouter.ai/deep/deep-v4-pro)
12
12
 
13
13
  ---
14
14
 
@@ -26,6 +26,7 @@ When you run `code2llm ./ -f all`, the following files are created:
26
26
  |------|--------|---------|--------------|
27
27
  | `evolution.toon.yaml` | **YAML** | **📋 Refactoring queue** - Prioritized improvements | 0 refactoring actions needed |
28
28
  | `map.toon.yaml` | **YAML** | **🗺️ Structural map + project header** - Modules, imports, exports, signatures, stats, alerts, hotspots, trend | Project architecture overview |
29
+ | `projections.json` | **JSON** | **🔎 Bounded symbol catalog** (`code2llm.projection/v1`) for intent retrieval / external indexers — deterministic, digested, capped at 2048 records | Reuse contract without analyzer internals |
29
30
 
30
31
  ### 🤖 LLM-Ready Documentation
31
32
 
@@ -0,0 +1 @@
1
+ 0.5.185
@@ -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.179"
11
+ __version__ = "0.5.185"
12
12
  __author__ = "STTS Project"
13
13
 
14
14
  # Core analysis components (lightweight, always needed)
@@ -212,22 +212,27 @@ def detect_data_clumps(
212
212
 
213
213
  for (file, args), funcs in arg_sets.items():
214
214
  if len(funcs) >= 2:
215
- for func_name in funcs:
215
+ # Names feed exported ticket identities, so hash/discovery order
216
+ # must not turn the same finding into a new ticket after restart.
217
+ ordered_args = sorted(args)
218
+ ordered_funcs = sorted(funcs)
219
+ arg_names = ", ".join(ordered_args)
220
+ for func_name in ordered_funcs:
216
221
  func_info = functions[func_name]
217
222
  smells.append(
218
223
  CodeSmell(
219
- name=f"Data Clump: {', '.join(args)}",
224
+ name=f"Data Clump: {arg_names}",
220
225
  type="data_clump",
221
226
  file=func_info.file,
222
227
  line=func_info.line,
223
228
  severity=0.6,
224
229
  description=(
225
- f"Arguments ({', '.join(args)}) are used together"
226
- f" in multiple functions: {', '.join(funcs)}."
230
+ f"Arguments ({arg_names}) are used together"
231
+ f" in multiple functions: {', '.join(ordered_funcs)}."
227
232
  ),
228
233
  context={
229
- "clump": list(args),
230
- "related_functions": funcs,
234
+ "clump": ordered_args,
235
+ "related_functions": ordered_funcs,
231
236
  },
232
237
  )
233
238
  )
@@ -0,0 +1,178 @@
1
+ """Autonomous non-blocking background update checker and auto-upgrader for CLI tools.
2
+
3
+ Checks PyPI for package updates at most once every TTL interval (default: 24h).
4
+ Spawns a detached background process with zero network delay on the CLI invocation.
5
+ If an update is available:
6
+ - If AUTO_UPGRADE=1 or <PKG>_AUTO_UPGRADE=1: spawns a background pip install --upgrade.
7
+ - Otherwise, reports cleanly on stderr.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ import time
17
+ from importlib import metadata
18
+ from pathlib import Path
19
+
20
+
21
+ def _get_cache_dir(pkg_name: str) -> Path:
22
+ """Return platform cache directory for package update tracking."""
23
+ xdg_cache = os.environ.get("XDG_CACHE_HOME")
24
+ if xdg_cache:
25
+ base = Path(xdg_cache)
26
+ else:
27
+ base = Path.home() / ".cache"
28
+ pkg_cache = base / pkg_name
29
+ pkg_cache.mkdir(parents=True, exist_ok=True)
30
+ return pkg_cache
31
+
32
+
33
+ def _is_newer(latest: str, current: str) -> bool:
34
+ """Check if latest is strictly newer than current, with zero hard dependency."""
35
+ try:
36
+ from packaging import version
37
+ return version.parse(latest) > version.parse(current)
38
+ except Exception:
39
+ pass
40
+
41
+ def parse_simple(v: str) -> tuple:
42
+ nums = []
43
+ for part in v.split("."):
44
+ digits = ""
45
+ for ch in part:
46
+ if ch.isdigit():
47
+ digits += ch
48
+ else:
49
+ break
50
+ nums.append(int(digits) if digits else 0)
51
+ return tuple(nums)
52
+
53
+ return parse_simple(latest) > parse_simple(current)
54
+
55
+
56
+ def _spawn_detached_check(pkg_name: str, current_version: str, cache_file: Path) -> None:
57
+ """Launch completely detached background worker process."""
58
+ worker_code = f"""
59
+ import json, sys, time
60
+ from urllib import request
61
+ from pathlib import Path
62
+
63
+ pkg_name = {pkg_name!r}
64
+ current_version = {current_version!r}
65
+ cache_file = Path({str(cache_file)!r})
66
+
67
+ latest = None
68
+ try:
69
+ url = f"https://pypi.org/pypi/{{pkg_name}}/json"
70
+ req = request.Request(url, headers={{"User-Agent": f"{{pkg_name}}-autoupdate"}})
71
+ with request.urlopen(req, timeout=3.0) as resp:
72
+ if resp.status == 200:
73
+ data = json.loads(resp.read().decode("utf-8"))
74
+ latest = data.get("info", {{}}).get("version")
75
+ except Exception:
76
+ pass
77
+
78
+ cache_data = {{
79
+ "last_check": time.time(),
80
+ "current_version": current_version,
81
+ "latest_version": latest or current_version,
82
+ }}
83
+ try:
84
+ cache_file.write_text(json.dumps(cache_data), encoding="utf-8")
85
+ except Exception:
86
+ pass
87
+ """
88
+ try:
89
+ subprocess.Popen(
90
+ [sys.executable, "-c", worker_code],
91
+ stdout=subprocess.DEVNULL,
92
+ stderr=subprocess.DEVNULL,
93
+ stdin=subprocess.DEVNULL,
94
+ start_new_session=True,
95
+ )
96
+ except Exception:
97
+ pass
98
+
99
+
100
+ def _spawn_background_upgrade(pkg_name: str) -> None:
101
+ """Spawn background pip install --upgrade if auto-upgrade is enabled."""
102
+ try:
103
+ subprocess.Popen(
104
+ [sys.executable, "-m", "pip", "install", "--upgrade", "--quiet", "--disable-pip-version-check", pkg_name],
105
+ stdout=subprocess.DEVNULL,
106
+ stderr=subprocess.DEVNULL,
107
+ stdin=subprocess.DEVNULL,
108
+ start_new_session=True,
109
+ )
110
+ except Exception:
111
+ pass
112
+
113
+
114
+ def check_for_updates(
115
+ pkg_name: str,
116
+ ttl_seconds: int = 86400, # 24 hours
117
+ ) -> None:
118
+ """Non-blocking check for package updates.
119
+
120
+ Reads previous check result from disk (fast, ~0.1ms). If an update was found
121
+ in a previous run:
122
+ - If AUTO_UPGRADE=1 or <PKG>_AUTO_UPGRADE=1, triggers a silent background pip upgrade.
123
+ - Otherwise, displays a friendly notice on stderr.
124
+ Spawns a detached process to query PyPI in the background only when the cache is expired.
125
+ """
126
+ if os.environ.get("CI") or os.environ.get("NO_AUTOUPDATE"):
127
+ return
128
+
129
+ try:
130
+ current_version = metadata.version(pkg_name)
131
+ except Exception:
132
+ return
133
+
134
+ cache_dir = _get_cache_dir(pkg_name)
135
+ cache_file = cache_dir / "update_check.json"
136
+
137
+ should_query = True
138
+ cached_latest = None
139
+
140
+ if cache_file.exists():
141
+ try:
142
+ cached = json.loads(cache_file.read_text(encoding="utf-8"))
143
+ last_check = cached.get("last_check", 0)
144
+ cached_latest = cached.get("latest_version")
145
+ if time.time() - last_check < ttl_seconds:
146
+ should_query = False
147
+ except Exception:
148
+ pass
149
+
150
+ if cached_latest and cached_latest != current_version:
151
+ try:
152
+ if _is_newer(cached_latest, current_version):
153
+ safe_pkg = pkg_name.upper().replace("-", "_")
154
+ env_pkg_key = safe_pkg + "_AUTO_UPGRADE"
155
+ auto_upgrade_enabled = (
156
+ os.environ.get("AUTO_UPGRADE") == "1" or
157
+ os.environ.get(env_pkg_key) == "1"
158
+ )
159
+ if auto_upgrade_enabled:
160
+ sys.stderr.write(
161
+ "\n⚡ [" + pkg_name + "] Automatyczna aktualizacja w tle: "
162
+ + current_version + " → " + cached_latest + "...\n"
163
+ )
164
+ sys.stderr.flush()
165
+ _spawn_background_upgrade(pkg_name)
166
+ else:
167
+ sys.stderr.write(
168
+ "\n💡 [" + pkg_name + "] Nowa wersja dostępna: "
169
+ + current_version + " → " + cached_latest + "\n"
170
+ + " Aby zaktualizować, uruchom: pip install --upgrade " + pkg_name + "\n"
171
+ + " (lub ustaw AUTO_UPGRADE=1)\n\n"
172
+ )
173
+ sys.stderr.flush()
174
+ except Exception:
175
+ pass
176
+
177
+ if should_query:
178
+ _spawn_detached_check(pkg_name, current_version, cache_file)
@@ -29,6 +29,12 @@ _validate_chunked_output = validate_chunked_output
29
29
 
30
30
  def main():
31
31
  """Main CLI entry point."""
32
+ try:
33
+ from .autoupdate import check_for_updates
34
+ check_for_updates("code2llm")
35
+ except Exception:
36
+ pass
37
+
32
38
  # Handle special sub-commands first
33
39
  special_result = handle_special_commands()
34
40
  if special_result is not None:
@@ -65,6 +65,10 @@ def _build_code2logic_cmd(args, source_path: Path, output_dir: Path) -> list[str
65
65
  "-o",
66
66
  str(output_dir),
67
67
  ]
68
+ excludes = getattr(args, "exclude", None)
69
+ if excludes:
70
+ cmd.append("--exclude")
71
+ cmd.extend(str(x) for x in excludes)
68
72
  if not args.verbose:
69
73
  cmd.append("-q")
70
74
  return cmd
@@ -30,6 +30,8 @@ from code2llm.exporters import (
30
30
  EvolutionExporter,
31
31
  READMEExporter,
32
32
  ProjectYAMLExporter,
33
+ ProjectionsExporter,
34
+ PROJECTION_FILENAME,
33
35
  ToonViewGenerator,
34
36
  ContextViewGenerator,
35
37
  ArticleViewGenerator,
@@ -195,6 +197,15 @@ def _export_simple_formats(args, result, output_dir: Path, formats):
195
197
  if args.verbose:
196
198
  print(f" - JSON: {filepath}")
197
199
 
200
+ if "projections" in formats:
201
+ exporter = ProjectionsExporter()
202
+ filepath = output_dir / PROJECTION_FILENAME
203
+ t0 = time.monotonic()
204
+ exporter.export(result, str(filepath))
205
+ elapsed = time.monotonic() - t0
206
+ if args.verbose:
207
+ print(f" - PROJECTIONS: {filepath} ({elapsed:.2f}s)")
208
+
198
209
 
199
210
  def _export_yaml(args, result, output_dir: Path):
200
211
  """Export YAML with optional split/separated modes."""
@@ -252,6 +252,8 @@ def _expand_all_formats(requested: List[str], include_png: bool = False) -> List
252
252
  if "all" not in requested:
253
253
  return requested[:]
254
254
  formats = ["toon", "map", "context", "evolution", "mermaid", "planfile"]
255
+ if include_png and "mermaid" not in formats:
256
+ formats.append("mermaid")
255
257
  return formats
256
258
 
257
259
 
@@ -20,6 +20,7 @@ FORMAT_FILENAMES: Dict[str, str] = {
20
20
  "planfile": "planfile-tickets.yaml",
21
21
  "readme": "README.md",
22
22
  "project-yaml": "project.yaml",
23
+ "projections": "projections.json",
23
24
  }
24
25
 
25
26
  # Files produced per format in dry-run preview
@@ -33,6 +34,7 @@ FORMAT_DRY_RUN_FILES: Dict[str, List[str]] = {
33
34
  "yaml": ["analysis.yaml"],
34
35
  "json": ["analysis.json"],
35
36
  "readme": ["README.md"],
37
+ "projections": ["projections.json"],
36
38
  }
37
39
 
38
40
  # Human-readable labels
@@ -47,6 +49,7 @@ FORMAT_LABELS: Dict[str, str] = {
47
49
  "planfile": "PLANFILE (ticket suggestions)",
48
50
  "readme": "README (documentation)",
49
51
  "project-yaml": "PROJECT-YAML (single source of truth)",
52
+ "projections": "PROJECTIONS (intent retrieval)",
50
53
  }
51
54
 
52
55
  # ---------------------------------------------------------------------------
@@ -97,7 +97,7 @@ Strategy Options (--strategy):
97
97
  default="toon",
98
98
  help=(
99
99
  "Output formats: toon,map,flow,context,code2logic,yaml,json,mermaid,"
100
- "evolution,calls,png,project-yaml,planfile,all (default: toon)"
100
+ "evolution,calls,png,project-yaml,planfile,projections,all (default: toon)"
101
101
  ),
102
102
  )
103
103
 
@@ -36,9 +36,12 @@
36
36
  # Config is serialised to a plain dict before pickling (dataclasses are not picklable).
37
37
  # Results from worker processes are plain dicts; _merge_results rebuilds dataclasses.
38
38
 
39
+ import hashlib
39
40
  import logging
41
+ import multiprocessing as mp
40
42
  import os
41
43
  import time
44
+ from collections import Counter
42
45
  from concurrent.futures import ProcessPoolExecutor, as_completed
43
46
  from pathlib import Path
44
47
  from typing import Dict, List, Optional, Tuple
@@ -171,7 +174,7 @@ class ProjectAnalyzer:
171
174
  cached_results: List[Dict] = []
172
175
  for fp in cached_paths:
173
176
  r = pcache.get_file_result(fp)
174
- if r is not None:
177
+ if isinstance(r, dict) and getattr(r.get("module"), "name", None) == path_to_module[fp]:
175
178
  cached_results.append(r)
176
179
  else:
177
180
  changed_paths.append(fp)
@@ -317,12 +320,16 @@ class ProjectAnalyzer:
317
320
  project_name = project_path.name
318
321
 
319
322
  # Fast path: native Rust directory traversal
323
+ skip_dirs = list(
324
+ _SKIP_DIR_NAMES
325
+ | getattr(self.file_filter, "_simple_dir_excludes", set())
326
+ )
320
327
  native_files = native_walk_project_files(
321
328
  project_str,
322
329
  extensions=list(ALL_EXTENSIONS),
323
330
  filenames=list(ALL_FILENAMES),
324
331
  filename_prefixes=list(LANGUAGE_FILENAME_PREFIXES),
325
- skip_dirs=list(_SKIP_DIR_NAMES),
332
+ skip_dirs=skip_dirs,
326
333
  respect_gitignore=getattr(self.config.filters, "gitignore_enabled", True),
327
334
  )
328
335
  if native_files is not None:
@@ -331,7 +338,7 @@ class ProjectAnalyzer:
331
338
  for f_path, mod_name in native_files
332
339
  if self.file_filter.should_process(f_path)
333
340
  ]
334
- return filtered
341
+ return self._source_module_names(filtered, project_path)
335
342
 
336
343
  files = []
337
344
  ext_set = set(ALL_EXTENSIONS)
@@ -362,7 +369,32 @@ class ProjectAnalyzer:
362
369
  module_name = self._compute_module_name(rel, filename, project_name)
363
370
  files.append((file_str, module_name))
364
371
 
365
- return files
372
+ return self._source_module_names(files, project_path)
373
+
374
+ @staticmethod
375
+ def _source_module_names(files, project_path):
376
+ """Give colliding logical names distinct, deterministic static identities.
377
+
378
+ File/package aliases can coexist on disk. Keeping either under their
379
+ shared import name would silently overwrite symbols and may bind calls
380
+ to the wrong file. Unique names retain compatibility; ambiguous names
381
+ receive source-path identities before analysis, on both file walkers.
382
+ """
383
+ counts = Counter(name for _, name in files)
384
+ reserved = set(counts)
385
+ selected = {}
386
+ for file_path, name in sorted(files):
387
+ if counts[name] == 1:
388
+ selected[file_path] = name
389
+ continue
390
+ relative = Path(file_path).relative_to(project_path).as_posix()
391
+ digest = hashlib.sha256(relative.encode("utf-8")).hexdigest()
392
+ candidate = name + ".__source_" + digest
393
+ while candidate in reserved:
394
+ candidate += "_"
395
+ reserved.add(candidate)
396
+ selected[file_path] = candidate
397
+ return [(path, selected[path]) for path, _ in files]
366
398
 
367
399
  def _wrap_tqdm(self, iterator, total: int, desc: str = "Analyzing"):
368
400
  """Wrap iterator with tqdm when verbose=False and tqdm is available."""
@@ -390,7 +422,14 @@ class ProjectAnalyzer:
390
422
  "output_dir": self.config.output_dir,
391
423
  }
392
424
 
393
- with ProcessPoolExecutor(max_workers=workers) as executor:
425
+ # Use forkserver if available (Linux) to prevent deadlocks with native C/Rust extensions
426
+ # and multi-threaded runtimes when forking.
427
+ try:
428
+ mp_context = mp.get_context("forkserver")
429
+ except ValueError:
430
+ mp_context = None
431
+
432
+ with ProcessPoolExecutor(max_workers=workers, mp_context=mp_context) as executor:
394
433
  # Submit all jobs
395
434
  future_to_file = {
396
435
  executor.submit(
@@ -401,7 +440,9 @@ class ProjectAnalyzer:
401
440
 
402
441
  # Collect results as they complete (with optional progress bar)
403
442
  completed = 0
404
- iterator = self._wrap_tqdm(as_completed(future_to_file), total=len(files))
443
+ iterator = self._wrap_tqdm(
444
+ as_completed(future_to_file), total=len(files), desc="Analyzing"
445
+ )
405
446
  for future in iterator:
406
447
  file_path, _ = future_to_file[future]
407
448
  try:
@@ -422,11 +463,21 @@ class ProjectAnalyzer:
422
463
  analyzer = FileAnalyzer(self.config, self.cache)
423
464
  total = len(files)
424
465
 
425
- file_iterator = self._wrap_tqdm(list(enumerate(files, 1)), total=total)
466
+ file_iterator = self._wrap_tqdm(
467
+ list(enumerate(files, 1)), total=total, desc="Analyzing"
468
+ )
426
469
  for i, (file_path, module_name) in file_iterator:
427
470
  try:
428
- result = analyzer.analyze_file(file_path, module_name)
471
+ cached = self.cache.get_fast(file_path) if self.cache else None
472
+ stale_identity = cached is not None and (
473
+ not isinstance(cached, dict)
474
+ or getattr(cached.get("module"), "name", None) != module_name
475
+ )
476
+ selected = FileAnalyzer(self.config, None) if stale_identity else analyzer
477
+ result = selected.analyze_file(file_path, module_name)
429
478
  if result:
479
+ if stale_identity:
480
+ self.cache.put_fast(file_path, result)
430
481
  results.append(result)
431
482
  except Exception as e:
432
483
  if self.config.verbose:
@@ -91,7 +91,7 @@ class PerformanceConfig:
91
91
  def get_workers(self) -> int:
92
92
  """Get effective worker count (auto-detect if set to 0)."""
93
93
  if self.parallel_workers <= 0:
94
- return _get_optimal_workers(default=4)
94
+ return _get_optimal_workers(default=4, max_per_gb=2.0)
95
95
  return self.parallel_workers
96
96
 
97
97
  def apply_fast_mode(self) -> None:
@@ -66,6 +66,15 @@ _SKIP_DIR_NAMES = (
66
66
  ".serverless",
67
67
  ".netlify",
68
68
  ".vercel",
69
+ # Additional heavy directories and vendor bundles
70
+ "c2004",
71
+ "c2004-git-backup",
72
+ "workshop",
73
+ "managed_components",
74
+ "vendor",
75
+ ".subactor",
76
+ ".worktrees",
77
+ ".planfile",
69
78
  }
70
79
  )
71
80
  | ARCHIVE_DIR_NAMES
@@ -83,13 +92,31 @@ class FastFileFilter:
83
92
  # Split patterns into fast substring checks vs pre-compiled regex
84
93
  self._simple_excludes = []
85
94
  self._regex_excludes = []
95
+ self._simple_dir_excludes: set[str] = set()
96
+ wildcard_patterns = []
86
97
  for p in config.exclude_patterns:
87
98
  p_lower = p.lower()
99
+ p_dir = p_lower.removesuffix("/**").removesuffix("/*").rstrip("/")
100
+ if p_dir and not any(c in p_dir for c in ("*", "?", "[")):
101
+ self._simple_dir_excludes.add(p_dir)
102
+ if "/" in p_dir:
103
+ self._simple_dir_excludes.add(p_dir.split("/")[-1])
88
104
  if any(c in p_lower for c in ("*", "?", "[")):
89
105
  self._regex_excludes.append(re.compile(fnmatch.translate(p_lower)))
106
+ wildcard_patterns.append(p_lower)
90
107
  else:
91
108
  self._simple_excludes.append(p_lower)
92
109
 
110
+ if wildcard_patterns:
111
+ try:
112
+ self._combined_regex_excludes = re.compile(
113
+ "|".join(f"(?:{fnmatch.translate(p)})" for p in wildcard_patterns)
114
+ )
115
+ except Exception:
116
+ self._combined_regex_excludes = None
117
+ else:
118
+ self._combined_regex_excludes = None
119
+
93
120
  self._simple_includes = []
94
121
  self._regex_includes = []
95
122
  for p in config.include_patterns:
@@ -107,7 +134,11 @@ class FastFileFilter:
107
134
  def should_skip_dir(self, dirname: str) -> bool:
108
135
  """Fast O(1) check: skip this directory entirely during tree walk?"""
109
136
  lower = dirname.lower()
110
- return lower.startswith(".") or lower in _SKIP_DIR_NAMES
137
+ return (
138
+ lower.startswith(".")
139
+ or lower in _SKIP_DIR_NAMES
140
+ or lower in self._simple_dir_excludes
141
+ )
111
142
 
112
143
  def _passes_gitignore(self, file_path: str) -> bool:
113
144
  """Check if file passes gitignore patterns (True = pass, False = excluded)."""
@@ -115,17 +146,32 @@ class FastFileFilter:
115
146
  return True
116
147
  return not self._gitignore_parser.is_ignored(Path(file_path), self.project_path)
117
148
 
118
- def _passes_excludes(self, path_lower: str, basename_lower: str) -> bool:
149
+ def _passes_excludes(
150
+ self, path_lower: str, basename_lower: str, rel_lower: str | None = None
151
+ ) -> bool:
119
152
  """Check if file passes exclude patterns (True = pass, False = excluded)."""
120
153
  # Fast substring excludes
121
154
  for pattern in self._simple_excludes:
122
- if pattern in path_lower:
155
+ if pattern in path_lower or (rel_lower and pattern in rel_lower):
123
156
  return False
124
157
 
125
- # Pre-compiled wildcard excludes
126
- for regex in self._regex_excludes:
127
- if regex.match(path_lower) or regex.match(basename_lower):
158
+ # Fast combined regex check
159
+ if self._combined_regex_excludes is not None:
160
+ if (
161
+ self._combined_regex_excludes.match(path_lower)
162
+ or self._combined_regex_excludes.match(basename_lower)
163
+ or (rel_lower and self._combined_regex_excludes.match(rel_lower))
164
+ ):
128
165
  return False
166
+ else:
167
+ # Pre-compiled wildcard excludes
168
+ for regex in self._regex_excludes:
169
+ if (
170
+ regex.match(path_lower)
171
+ or regex.match(basename_lower)
172
+ or (rel_lower and regex.match(rel_lower))
173
+ ):
174
+ return False
129
175
 
130
176
  return True
131
177
 
@@ -146,11 +192,20 @@ class FastFileFilter:
146
192
  if is_generated_artifact(file_path, self.project_path):
147
193
  return False
148
194
 
149
- return (
150
- self._passes_gitignore(file_path)
151
- and self._passes_excludes(path_lower, basename_lower)
152
- and self._passes_includes(path_lower)
153
- )
195
+ if not self._passes_gitignore(file_path):
196
+ return False
197
+
198
+ rel_lower = None
199
+ if self.project_path:
200
+ try:
201
+ rel_lower = str(Path(file_path).relative_to(self.project_path)).lower()
202
+ except ValueError:
203
+ pass
204
+
205
+ if not self._passes_excludes(path_lower, basename_lower, rel_lower):
206
+ return False
207
+
208
+ return self._passes_includes(path_lower)
154
209
 
155
210
  def _passes_line_count(self, line_count: int) -> bool:
156
211
  """Check if function passes line count threshold."""