code2llm 0.5.122__tar.gz → 0.5.124__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 (150) hide show
  1. {code2llm-0.5.122 → code2llm-0.5.124}/PKG-INFO +2 -2
  2. {code2llm-0.5.122 → code2llm-0.5.124}/README.md +1 -1
  3. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/__init__.py +1 -1
  4. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/cli_analysis.py +4 -0
  5. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/cli_commands.py +6 -5
  6. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/cli_exports/orchestrator.py +53 -0
  7. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/cli_parser.py +12 -0
  8. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/analyzer.py +10 -0
  9. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/config.py +20 -0
  10. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/base.py +11 -1
  11. code2llm-0.5.124/code2llm/exporters/report_generators.py +76 -0
  12. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/generators/llm_flow.py +35 -4
  13. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/generators/llm_task.py +35 -3
  14. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/generators/mermaid.py +15 -9
  15. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/nlp/__init__.py +1 -1
  16. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm.egg-info/PKG-INFO +2 -2
  17. {code2llm-0.5.122 → code2llm-0.5.124}/pyproject.toml +1 -1
  18. {code2llm-0.5.122 → code2llm-0.5.124}/setup.py +1 -1
  19. code2llm-0.5.122/code2llm/exporters/report_generators.py +0 -34
  20. {code2llm-0.5.122 → code2llm-0.5.124}/LICENSE +0 -0
  21. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/__main__.py +0 -0
  22. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/__init__.py +0 -0
  23. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/call_graph.py +0 -0
  24. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/cfg.py +0 -0
  25. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/coupling.py +0 -0
  26. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/data_analysis.py +0 -0
  27. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/dfg.py +0 -0
  28. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/pipeline_classifier.py +0 -0
  29. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/pipeline_detector.py +0 -0
  30. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/pipeline_resolver.py +0 -0
  31. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/side_effects.py +0 -0
  32. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/smells.py +0 -0
  33. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/type_inference.py +0 -0
  34. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/utils/__init__.py +0 -0
  35. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/analysis/utils/ast_helpers.py +0 -0
  36. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/api.py +0 -0
  37. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/cli.py +0 -0
  38. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/cli_exports/__init__.py +0 -0
  39. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/cli_exports/code2logic.py +0 -0
  40. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/cli_exports/formats.py +0 -0
  41. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/cli_exports/prompt.py +0 -0
  42. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/__init__.py +0 -0
  43. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/ast_registry.py +0 -0
  44. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/export_pipeline.py +0 -0
  45. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/file_analyzer.py +0 -0
  46. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/file_cache.py +0 -0
  47. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/file_filter.py +0 -0
  48. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/gitignore.py +0 -0
  49. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/incremental.py +0 -0
  50. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/__init__.py +0 -0
  51. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/cpp.py +0 -0
  52. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/csharp.py +0 -0
  53. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/generic.py +0 -0
  54. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/go_lang.py +0 -0
  55. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/java.py +0 -0
  56. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/php.py +0 -0
  57. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/ruby.py +0 -0
  58. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/rust.py +0 -0
  59. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/ts_extractors.py +0 -0
  60. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/ts_parser.py +0 -0
  61. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/lang/typescript.py +0 -0
  62. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/large_repo.py +0 -0
  63. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/models.py +0 -0
  64. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/persistent_cache.py +0 -0
  65. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/refactoring.py +0 -0
  66. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/repo_files.py +0 -0
  67. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/streaming/__init__.py +0 -0
  68. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/streaming/cache.py +0 -0
  69. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/streaming/incremental.py +0 -0
  70. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/streaming/prioritizer.py +0 -0
  71. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/streaming/scanner.py +0 -0
  72. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/streaming/strategies.py +0 -0
  73. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/streaming_analyzer.py +0 -0
  74. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/core/toon_size_manager.py +0 -0
  75. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/__init__.py +0 -0
  76. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/article_view.py +0 -0
  77. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/base.py +0 -0
  78. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/context_exporter.py +0 -0
  79. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/context_view.py +0 -0
  80. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/dashboard_data.py +0 -0
  81. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/dashboard_renderer.py +0 -0
  82. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/evolution_exporter.py +0 -0
  83. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/flow_constants.py +0 -0
  84. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/flow_exporter.py +0 -0
  85. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/flow_renderer.py +0 -0
  86. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/html_dashboard.py +0 -0
  87. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/index_generator/__init__.py +0 -0
  88. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/index_generator/renderer.py +0 -0
  89. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/index_generator/scanner.py +0 -0
  90. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/index_generator.py +0 -0
  91. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/json_exporter.py +0 -0
  92. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/llm_exporter.py +0 -0
  93. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/map_exporter.py +0 -0
  94. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/mermaid_exporter.py +0 -0
  95. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/mermaid_flow_helpers.py +0 -0
  96. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/project_yaml/__init__.py +0 -0
  97. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/project_yaml/constants.py +0 -0
  98. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/project_yaml/core.py +0 -0
  99. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/project_yaml/evolution.py +0 -0
  100. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/project_yaml/health.py +0 -0
  101. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/project_yaml/hotspots.py +0 -0
  102. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/project_yaml/modules.py +0 -0
  103. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/project_yaml_exporter.py +0 -0
  104. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/readme_exporter.py +0 -0
  105. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon/__init__.py +0 -0
  106. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon/helpers.py +0 -0
  107. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon/metrics.py +0 -0
  108. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon/metrics_core.py +0 -0
  109. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon/metrics_duplicates.py +0 -0
  110. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon/metrics_health.py +0 -0
  111. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon/module_detail.py +0 -0
  112. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon/renderer.py +0 -0
  113. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon.py +0 -0
  114. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/toon_view.py +0 -0
  115. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/validate_project.py +0 -0
  116. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/exporters/yaml_exporter.py +0 -0
  117. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/generators/__init__.py +0 -0
  118. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/generators/_utils.py +0 -0
  119. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/nlp/config.py +0 -0
  120. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/nlp/entity_resolution.py +0 -0
  121. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/nlp/intent_matching.py +0 -0
  122. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/nlp/normalization.py +0 -0
  123. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/nlp/pipeline.py +0 -0
  124. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/patterns/__init__.py +0 -0
  125. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/patterns/detector.py +0 -0
  126. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/refactor/__init__.py +0 -0
  127. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm/refactor/prompt_engine.py +0 -0
  128. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm.egg-info/SOURCES.txt +0 -0
  129. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm.egg-info/dependency_links.txt +0 -0
  130. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm.egg-info/entry_points.txt +0 -0
  131. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm.egg-info/requires.txt +0 -0
  132. {code2llm-0.5.122 → code2llm-0.5.124}/code2llm.egg-info/top_level.txt +0 -0
  133. {code2llm-0.5.122 → code2llm-0.5.124}/setup.cfg +0 -0
  134. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_advanced_analysis.py +0 -0
  135. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_analyzer.py +0 -0
  136. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_calls_toon_export.py +0 -0
  137. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_deep_analysis.py +0 -0
  138. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_edge_cases.py +0 -0
  139. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_flow_exporter.py +0 -0
  140. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_format_quality.py +0 -0
  141. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_multilanguage_e2e.py +0 -0
  142. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_nlp_pipeline.py +0 -0
  143. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_nonpython_cc_calls.py +0 -0
  144. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_persistent_cache.py +0 -0
  145. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_pipeline_detector.py +0 -0
  146. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_project_toon_export.py +0 -0
  147. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_prompt_engine.py +0 -0
  148. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_prompt_txt.py +0 -0
  149. {code2llm-0.5.122 → code2llm-0.5.124}/tests/test_refactoring_engine.py +0 -0
  150. {code2llm-0.5.122 → code2llm-0.5.124}/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.122
3
+ Version: 0.5.124
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.122-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.124-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.122-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.124-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)
@@ -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.122"
11
+ __version__ = "0.5.124"
12
12
  __author__ = "STTS Project"
13
13
 
14
14
  # Core analysis components (lightweight, always needed)
@@ -84,6 +84,10 @@ def _build_config(args, output_dir: Path):
84
84
  # Persistent cache flags (read via getattr with defaults in analyzer.py)
85
85
  no_cache = getattr(args, 'no_cache', False) or getattr(args, 'force', False)
86
86
  config.no_cache = no_cache
87
+ # Watch mode for auto-detecting changed files
88
+ config.watch = getattr(args, 'watch', False)
89
+ # Dry-run mode (handled in orchestrator, but stored for reference)
90
+ config.dry_run = getattr(args, 'dry_run', False)
87
91
  return config
88
92
 
89
93
 
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  from typing import Optional
7
7
 
8
8
  from .cli_exports import _run_report
9
+ from .core.config import DEFAULT_CACHE_MAX_AGE_DAYS, KB
9
10
 
10
11
 
11
12
  def handle_special_commands() -> Optional[int]:
@@ -39,18 +40,18 @@ def handle_cache_command(args_list) -> int:
39
40
  parser.add_argument('action', choices=['status', 'clear', 'gc'], help='Cache action')
40
41
  parser.add_argument('--all', action='store_true', dest='all_projects',
41
42
  help='Apply to all cached projects (clear only)')
42
- parser.add_argument('--max-age', type=int, default=30, metavar='DAYS',
43
- help='Max age in days for gc (default: 30)')
43
+ parser.add_argument('--max-age', type=int, default=DEFAULT_CACHE_MAX_AGE_DAYS, metavar='DAYS',
44
+ help=f'Max age in days for gc (default: {DEFAULT_CACHE_MAX_AGE_DAYS})')
44
45
  args = parser.parse_args(args_list)
45
46
 
46
47
  if args.action == 'status':
47
48
  projects = get_all_projects()
48
49
  root = _DEFAULT_ROOT
49
- total_mb = sum(p.get('cache_size_bytes', 0) for p in projects) / (1024 * 1024)
50
+ total_mb = sum(p.get('cache_size_bytes', 0) for p in projects) / (KB * KB)
50
51
  print(f"Cache: {root}")
51
52
  print(f" Projects: {len(projects)} Total: {total_mb:.1f} MB")
52
53
  for p in projects:
53
- size_mb = p.get('cache_size_bytes', 0) / (1024 * 1024)
54
+ size_mb = p.get('cache_size_bytes', 0) / (KB * KB)
54
55
  updated = p.get('updated_at', 0)
55
56
  age_min = int((time.time() - updated) / 60) if updated else 0
56
57
  age_str = f"{age_min}m ago" if age_min < 120 else f"{age_min//60}h ago"
@@ -246,7 +247,7 @@ def _get_file_sizes(chunk_dir: Path, required_files: list[str]) -> str:
246
247
  sizes = []
247
248
  for req_file in required_files:
248
249
  size = (chunk_dir / req_file).stat().st_size
249
- sizes.append(f"{req_file}:{size//1024}KB" if size > 1024 else f"{req_file}:{size}B")
250
+ sizes.append(f"{req_file}:{size//KB}KB" if size > KB else f"{req_file}:{size}B")
250
251
  return ", ".join(sizes)
251
252
 
252
253
 
@@ -42,6 +42,18 @@ FORMAT_FILENAMES: Dict[str, str] = {
42
42
  'project-yaml': 'project.yaml',
43
43
  }
44
44
 
45
+ # Files produced per format in dry-run preview
46
+ FORMAT_DRY_RUN_FILES: Dict[str, List[str]] = {
47
+ 'toon': ['analysis.toon'],
48
+ 'map': ['map.toon.yaml'],
49
+ 'evolution': ['evolution.toon.yaml'],
50
+ 'context': ['context.md'],
51
+ 'mermaid': ['calls.mmd', 'calls.png'],
52
+ 'yaml': ['analysis.yaml'],
53
+ 'json': ['analysis.json'],
54
+ 'readme': ['README.md'],
55
+ }
56
+
45
57
  # Human-readable labels
46
58
  FORMAT_LABELS: Dict[str, str] = {
47
59
  'toon': 'TOON (diagnostics)',
@@ -69,6 +81,41 @@ def _build_export_config(args, formats: List[str]) -> Dict[str, Any]:
69
81
  }
70
82
 
71
83
 
84
+ def _collect_dry_run_files(formats: List[str], output_dir: Path) -> List[Path]:
85
+ """Return the list of files that would be written for the given formats."""
86
+ output_files: List[Path] = []
87
+ for fmt in formats:
88
+ for name in FORMAT_DRY_RUN_FILES.get(fmt, []):
89
+ output_files.append(output_dir / name)
90
+ output_files.append(output_dir / 'project.toon.yaml')
91
+ output_files.append(output_dir / 'prompt.txt')
92
+ return output_files
93
+
94
+
95
+ def _show_dry_run_plan(formats: List[str], output_dir: Path, is_chunked: bool, result) -> None:
96
+ """Display what would be exported in dry-run mode."""
97
+ print("\n📋 DRY-RUN: Would export the following:\n")
98
+
99
+ output_files = _collect_dry_run_files(formats, output_dir)
100
+
101
+ size_hint = ""
102
+ func_count = len(getattr(result, 'functions', []))
103
+ if func_count > 0:
104
+ size_hint = f" (~{func_count * 50 // 1024}KB est.)"
105
+
106
+ for f in sorted(set(output_files)):
107
+ print(f" 📄 {f}{size_hint}")
108
+
109
+ stats = getattr(result, 'stats', {})
110
+ if stats:
111
+ print(f"\n📊 Based on analysis:")
112
+ print(f" - Functions: {stats.get('functions_found', 'N/A')}")
113
+ print(f" - Classes: {stats.get('classes_found', 'N/A')}")
114
+ print(f" - Files: {stats.get('files_processed', 'N/A')}")
115
+
116
+ print(f"\n✅ Dry-run complete. Use without --dry-run to export.\n")
117
+
118
+
72
119
  def _run_exports(args, result, output_dir: Path, source_path: Optional[Path] = None):
73
120
  """Export analysis results in requested formats.
74
121
 
@@ -79,6 +126,12 @@ def _run_exports(args, result, output_dir: Path, source_path: Optional[Path] = N
79
126
  requested_formats = [f.strip() for f in args.format.split(',')]
80
127
  formats = _expand_all_formats(requested_formats, getattr(args, 'png', False))
81
128
  is_chunked = getattr(args, 'chunk', False)
129
+ dry_run = getattr(args, 'dry_run', False)
130
+
131
+ # Dry-run: show what would be exported without writing
132
+ if dry_run:
133
+ _show_dry_run_plan(formats, output_dir, is_chunked, result)
134
+ return
82
135
 
83
136
  # Skip cache for chunked or when explicitly disabled
84
137
  skip_cache = is_chunked or getattr(args, 'no_cache', False)
@@ -147,6 +147,18 @@ Strategy Options (--strategy):
147
147
  help='Force re-analysis and re-export even when cache is valid (alias for --no-cache)'
148
148
  )
149
149
 
150
+ parser.add_argument(
151
+ '--dry-run',
152
+ action='store_true',
153
+ help='Show what would be exported without writing files'
154
+ )
155
+
156
+ parser.add_argument(
157
+ '--watch',
158
+ action='store_true',
159
+ help='Auto-detect changed files and only re-analyze those (faster subsequent runs)'
160
+ )
161
+
150
162
  parser.add_argument(
151
163
  '--strategy',
152
164
  choices=['quick', 'standard', 'deep'],
@@ -52,6 +52,16 @@ class ProjectAnalyzer:
52
52
  print(f" - Parallel: {self.config.performance.parallel_enabled}, Workers: {workers}")
53
53
 
54
54
  pcache, cached_results, files_to_analyze = self._load_from_persistent_cache(files, project_path)
55
+
56
+ # Watch mode: show what changed
57
+ if getattr(self.config, 'watch', False) and files_to_analyze:
58
+ print(f"\n👁️ Watch mode: {len(files_to_analyze)} files changed since last run:")
59
+ for fp, _ in files_to_analyze[:10]:
60
+ print(f" • {Path(fp).name}")
61
+ if len(files_to_analyze) > 10:
62
+ print(f" ... and {len(files_to_analyze) - 10} more")
63
+ print()
64
+
55
65
  fresh_results = self._run_analysis(files_to_analyze)
56
66
  self._store_to_persistent_cache(pcache, files_to_analyze, fresh_results)
57
67
 
@@ -36,6 +36,26 @@ DEFAULT_CACHE_TTL_HOURS = 24
36
36
  DEFAULT_MAX_MEMORY_MB = 2048
37
37
  DEFAULT_PROGRESS_BAR_THRESHOLD = 50 # File count threshold for progress bar
38
38
 
39
+ # Complexity thresholds
40
+ CC_LOW_THRESHOLD = 5 # Rank A
41
+ CC_MEDIUM_THRESHOLD = 10 # Rank B
42
+ CC_HIGH_THRESHOLD = 20 # Rank C
43
+ CC_CRITICAL_THRESHOLD = 50 # For warnings
44
+
45
+ # Size limits
46
+ KB = 1024
47
+ MB = 1024 * 1024
48
+ MAX_FILE_SIZE_KB = 256
49
+ CHUNK_SIZE_KB = 256
50
+
51
+ # Timeouts
52
+ DEFAULT_PNG_TIMEOUT = 60
53
+ DEFAULT_MERMAID_MAX_TEXT_SIZE = 2_000_000
54
+ DEFAULT_MERMAID_MAX_EDGES = 20_000
55
+
56
+ # Cache settings
57
+ DEFAULT_CACHE_MAX_AGE_DAYS = 30
58
+
39
59
 
40
60
  class AnalysisMode(str, Enum):
41
61
  """Available analysis modes."""
@@ -3,6 +3,12 @@
3
3
  import re
4
4
  from typing import Dict, List
5
5
 
6
+ from code2llm.core.config import (
7
+ CC_LOW_THRESHOLD,
8
+ CC_MEDIUM_THRESHOLD,
9
+ CC_HIGH_THRESHOLD,
10
+ )
11
+
6
12
 
7
13
  # Branching keywords per language family
8
14
  CC_PATTERNS = {
@@ -64,7 +70,11 @@ def calculate_complexity_regex(content: str, result: Dict,
64
70
  cc = 1
65
71
  else:
66
72
  cc = 1 + len(pattern.findall(body))
67
- rank = 'A' if cc <= 5 else ('B' if cc <= 10 else ('C' if cc <= 20 else 'D'))
73
+ rank = (
74
+ 'A' if cc <= CC_LOW_THRESHOLD
75
+ else ('B' if cc <= CC_MEDIUM_THRESHOLD
76
+ else ('C' if cc <= CC_HIGH_THRESHOLD else 'D'))
77
+ )
68
78
  func_info.complexity = {
69
79
  'cyclomatic_complexity': cc,
70
80
  'cc_rank': rank,
@@ -0,0 +1,76 @@
1
+ """Report generators — produce views from project.yaml (single source of truth).
2
+
3
+ Thin re-export module. Actual generators live in separate files:
4
+ toon_view.py → ToonViewGenerator → project.toon.yaml
5
+ context_view.py → ContextViewGenerator → context.md
6
+ article_view.py → ArticleViewGenerator → status.md
7
+ html_dashboard.py → HTMLDashboardGenerator → dashboard.html
8
+ """
9
+
10
+ import yaml
11
+ from typing import Any, Dict
12
+
13
+ from .toon_view import ToonViewGenerator
14
+ from .context_view import ContextViewGenerator
15
+ from .article_view import ArticleViewGenerator
16
+ from .html_dashboard import HTMLDashboardGenerator
17
+
18
+
19
+ def load_project_yaml(path: str) -> Dict[str, Any]:
20
+ """Load and validate project.yaml with detailed error reporting."""
21
+ import yaml
22
+ from yaml.scanner import ScannerError
23
+ from yaml.parser import ParserError
24
+
25
+ try:
26
+ with open(path, "r", encoding="utf-8") as f:
27
+ content = f.read()
28
+ except FileNotFoundError:
29
+ raise ValueError(f"project.yaml not found: {path}")
30
+ except Exception as e:
31
+ raise ValueError(f"Cannot read project.yaml ({path}): {e}")
32
+
33
+ # Check for empty file
34
+ if not content.strip():
35
+ raise ValueError(f"project.yaml is empty: {path}")
36
+
37
+ # Try to parse YAML with detailed error reporting
38
+ try:
39
+ data = yaml.safe_load(content)
40
+ except ScannerError as e:
41
+ line = e.problem_mark.line + 1 if e.problem_mark else "?"
42
+ col = e.problem_mark.column if e.problem_mark else "?"
43
+ raise ValueError(
44
+ f"YAML syntax error in {path} at line {line}, column {col}: {e.problem}\n"
45
+ f"Hint: Check indentation and special characters (:, -, #)")
46
+ except ParserError as e:
47
+ line = e.problem_mark.line + 1 if e.problem_mark else "?"
48
+ raise ValueError(
49
+ f"YAML parse error in {path} at line {line}: {e.problem}\n"
50
+ f"Hint: Verify YAML structure (mapping vs list)")
51
+ except Exception as e:
52
+ raise ValueError(f"YAML error in {path}: {e}")
53
+
54
+ # Validate structure
55
+ if data is None:
56
+ raise ValueError(f"project.yaml is null/empty: {path}")
57
+ if not isinstance(data, dict):
58
+ raise ValueError(
59
+ f"Invalid project.yaml: expected dict/object, got {type(data).__name__} in {path}\n"
60
+ f"Hint: YAML must start with key-value pairs, not a list")
61
+ if "version" not in data:
62
+ raise ValueError(
63
+ f"Invalid project.yaml: missing required 'version' key in {path}\n"
64
+ f"Required keys: version, project, analysis\n"
65
+ f"Found keys: {list(data.keys())[:10]}")
66
+
67
+ return data
68
+
69
+
70
+ __all__ = [
71
+ "load_project_yaml",
72
+ "ToonViewGenerator",
73
+ "ContextViewGenerator",
74
+ "ArticleViewGenerator",
75
+ "HTMLDashboardGenerator",
76
+ ]
@@ -21,11 +21,42 @@ def _strip_bom(text: str) -> str:
21
21
 
22
22
 
23
23
  def _safe_read_yaml(path: Path) -> Dict[str, Any]:
24
- """Read YAML file safely, handling BOM and type validation."""
25
- raw = _strip_bom(path.read_text(encoding="utf-8"))
26
- loaded = yaml.safe_load(raw) or {}
24
+ """Read YAML file safely, handling BOM and type validation with detailed errors."""
25
+ from yaml.scanner import ScannerError
26
+ from yaml.parser import ParserError
27
+
28
+ try:
29
+ raw = _strip_bom(path.read_text(encoding="utf-8"))
30
+ except FileNotFoundError:
31
+ raise ValueError(f"File not found: {path}")
32
+ except Exception as e:
33
+ raise ValueError(f"Cannot read file {path}: {e}")
34
+
35
+ if not raw.strip():
36
+ raise ValueError(f"File is empty: {path}")
37
+
38
+ try:
39
+ loaded = yaml.safe_load(raw)
40
+ except ScannerError as e:
41
+ line = e.problem_mark.line + 1 if e.problem_mark else "?"
42
+ col = e.problem_mark.column if e.problem_mark else "?"
43
+ raise ValueError(
44
+ f"YAML syntax error in {path} at line {line}, column {col}: {e.problem}\n"
45
+ f"Hint: Check indentation, avoid tabs, watch special characters (:, -, #)")
46
+ except ParserError as e:
47
+ line = e.problem_mark.line + 1 if e.problem_mark else "?"
48
+ raise ValueError(
49
+ f"YAML parse error in {path} at line {line}: {e.problem}\n"
50
+ f"Hint: Verify structure - are you using list where dict expected?")
51
+ except Exception as e:
52
+ raise ValueError(f"YAML error in {path}: {e}")
53
+
54
+ if loaded is None:
55
+ raise ValueError(f"File is null/empty YAML: {path}")
27
56
  if not isinstance(loaded, dict):
28
- raise ValueError("analysis.yaml must be a mapping at top-level")
57
+ raise ValueError(
58
+ f"Expected YAML mapping (dict), got {type(loaded).__name__} in {path}\n"
59
+ f"Hint: File must start with 'key: value' pairs, not a list")
29
60
  return loaded
30
61
 
31
62
 
@@ -212,13 +212,45 @@ def parse_llm_task_text(text: str) -> Dict[str, Any]:
212
212
 
213
213
 
214
214
  def load_input(path: Path) -> Dict[str, Any]:
215
- raw = path.read_text(encoding="utf-8")
215
+ """Load input file with detailed YAML/JSON error reporting."""
216
+ from yaml.scanner import ScannerError
217
+ from yaml.parser import ParserError
218
+
219
+ try:
220
+ raw = path.read_text(encoding="utf-8")
221
+ except FileNotFoundError:
222
+ raise ValueError(f"Input file not found: {path}")
223
+ except Exception as e:
224
+ raise ValueError(f"Cannot read input file {path}: {e}")
225
+
216
226
  raw = _strip_bom(raw)
217
227
 
228
+ if not raw.strip():
229
+ raise ValueError(f"Input file is empty: {path}")
230
+
218
231
  if path.suffix.lower() in {".yaml", ".yml"}:
219
- loaded = yaml.safe_load(raw) or {}
232
+ try:
233
+ loaded = yaml.safe_load(raw)
234
+ except ScannerError as e:
235
+ line = e.problem_mark.line + 1 if e.problem_mark else "?"
236
+ col = e.problem_mark.column if e.problem_mark else "?"
237
+ raise ValueError(
238
+ f"YAML syntax error at line {line}, column {col}: {e.problem}\n"
239
+ f"Hint: Check indentation in {path}")
240
+ except ParserError as e:
241
+ line = e.problem_mark.line + 1 if e.problem_mark else "?"
242
+ raise ValueError(
243
+ f"YAML parse error at line {line}: {e.problem}\n"
244
+ f"Hint: Verify YAML structure in {path}")
245
+ except Exception as e:
246
+ raise ValueError(f"YAML error in {path}: {e}")
247
+
248
+ if loaded is None:
249
+ raise ValueError(f"YAML file is null/empty: {path}")
220
250
  if not isinstance(loaded, dict):
221
- raise ValueError("YAML input must be a mapping/object at top level")
251
+ raise ValueError(
252
+ f"YAML must be a mapping/object, got {type(loaded).__name__} in {path}\n"
253
+ f"Hint: File should start with 'key: value' pairs")
222
254
  return loaded
223
255
 
224
256
  if path.suffix.lower() == ".json":
@@ -12,6 +12,12 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
12
12
  from pathlib import Path
13
13
  from typing import List, Optional, Tuple
14
14
 
15
+ from code2llm.core.config import (
16
+ DEFAULT_PNG_TIMEOUT,
17
+ DEFAULT_MERMAID_MAX_TEXT_SIZE,
18
+ DEFAULT_MERMAID_MAX_EDGES,
19
+ )
20
+
15
21
 
16
22
  def validate_mermaid_file(mmd_path: Path) -> List[str]:
17
23
  """Validate Mermaid file and return list of errors."""
@@ -283,7 +289,7 @@ def _prepare_and_render(mmd_file: Path, output_dir: Path, timeout: int) -> bool:
283
289
 
284
290
 
285
291
  def generate_pngs(
286
- input_dir: Path, output_dir: Path, timeout: int = 60, max_workers: int = 0
292
+ input_dir: Path, output_dir: Path, timeout: int = DEFAULT_PNG_TIMEOUT, max_workers: int = 0
287
293
  ) -> int:
288
294
  """Generate PNG files from all .mmd files in input_dir (parallel).
289
295
 
@@ -310,14 +316,14 @@ def generate_pngs(
310
316
  def _setup_puppeteer_config() -> tuple[int, int, Optional[str]]:
311
317
  """Setup puppeteer config file and return (max_text_size, max_edges, cfg_path)."""
312
318
  try:
313
- max_text_size = int(os.getenv('CODE2FLOW_MERMAID_MAX_TEXT_SIZE', '2000000'))
319
+ max_text_size = int(os.getenv('CODE2FLOW_MERMAID_MAX_TEXT_SIZE', str(DEFAULT_MERMAID_MAX_TEXT_SIZE)))
314
320
  except Exception:
315
- max_text_size = 2000000
321
+ max_text_size = DEFAULT_MERMAID_MAX_TEXT_SIZE
316
322
 
317
323
  try:
318
- max_edges = int(os.getenv('CODE2FLOW_MERMAID_MAX_EDGES', '20000'))
324
+ max_edges = int(os.getenv('CODE2FLOW_MERMAID_MAX_EDGES', str(DEFAULT_MERMAID_MAX_EDGES)))
319
325
  except Exception:
320
- max_edges = 20000
326
+ max_edges = DEFAULT_MERMAID_MAX_EDGES
321
327
 
322
328
  cfg_path: Optional[str] = None
323
329
  try:
@@ -406,7 +412,7 @@ def _run_mmdc_subprocess(
406
412
  return False
407
413
 
408
414
 
409
- def generate_single_png(mmd_file: Path, output_file: Path, timeout: int = 60) -> bool:
415
+ def generate_single_png(mmd_file: Path, output_file: Path, timeout: int = DEFAULT_PNG_TIMEOUT) -> bool:
410
416
  """Generate PNG from single Mermaid file using available renderers."""
411
417
  # Create output directory
412
418
  output_file.parent.mkdir(parents=True, exist_ok=True)
@@ -431,9 +437,9 @@ def generate_single_png(mmd_file: Path, output_file: Path, timeout: int = 60) ->
431
437
  def generate_with_puppeteer(
432
438
  mmd_file: Path,
433
439
  output_file: Path,
434
- timeout: int = 60,
435
- max_text_size: int = 2000000,
436
- max_edges: int = 20000,
440
+ timeout: int = DEFAULT_PNG_TIMEOUT,
441
+ max_text_size: int = DEFAULT_MERMAID_MAX_TEXT_SIZE,
442
+ max_edges: int = DEFAULT_MERMAID_MAX_EDGES,
437
443
  ) -> bool:
438
444
  """Generate PNG using Puppeteer with HTML template."""
439
445
  try:
@@ -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.122"
7
+ __version__ = "0.5.124"
8
8
 
9
9
  from .pipeline import NLPPipeline
10
10
  from .normalization import QueryNormalizer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code2llm
3
- Version: 0.5.122
3
+ Version: 0.5.124
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.122-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.124-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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code2llm"
7
- version = "0.5.122"
7
+ version = "0.5.124"
8
8
  description = "High-performance Python code flow analysis with optimized TOON format - CFG, DFG, call graphs, and intelligent code queries"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -4,7 +4,7 @@ from setuptools import setup, find_packages
4
4
  import os
5
5
 
6
6
  # Read version
7
- version = "0.5.121"
7
+ version = "0.5.123"
8
8
 
9
9
  # Read long description
10
10
  def read_readme():
@@ -1,34 +0,0 @@
1
- """Report generators — produce views from project.yaml (single source of truth).
2
-
3
- Thin re-export module. Actual generators live in separate files:
4
- toon_view.py → ToonViewGenerator → project.toon.yaml
5
- context_view.py → ContextViewGenerator → context.md
6
- article_view.py → ArticleViewGenerator → status.md
7
- html_dashboard.py → HTMLDashboardGenerator → dashboard.html
8
- """
9
-
10
- import yaml
11
- from typing import Any, Dict
12
-
13
- from .toon_view import ToonViewGenerator
14
- from .context_view import ContextViewGenerator
15
- from .article_view import ArticleViewGenerator
16
- from .html_dashboard import HTMLDashboardGenerator
17
-
18
-
19
- def load_project_yaml(path: str) -> Dict[str, Any]:
20
- """Load and validate project.yaml."""
21
- with open(path, "r", encoding="utf-8") as f:
22
- data = yaml.safe_load(f)
23
- if not isinstance(data, dict) or "version" not in data:
24
- raise ValueError(f"Invalid project.yaml: missing 'version' key in {path}")
25
- return data
26
-
27
-
28
- __all__ = [
29
- "load_project_yaml",
30
- "ToonViewGenerator",
31
- "ContextViewGenerator",
32
- "ArticleViewGenerator",
33
- "HTMLDashboardGenerator",
34
- ]
File without changes
File without changes
File without changes
File without changes