code2llm 0.5.37__tar.gz → 0.5.38__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 (102) hide show
  1. {code2llm-0.5.37 → code2llm-0.5.38}/PKG-INFO +4 -4
  2. {code2llm-0.5.37 → code2llm-0.5.38}/README.md +3 -3
  3. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/__init__.py +1 -1
  4. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/cli.py +2 -241
  5. code2llm-0.5.38/code2llm/cli_analysis.py +282 -0
  6. code2llm-0.5.38/code2llm/cli_exports/__init__.py +48 -0
  7. code2llm-0.5.38/code2llm/cli_exports/code2logic.py +127 -0
  8. code2llm-0.5.38/code2llm/cli_exports/formats.py +225 -0
  9. code2llm-0.5.38/code2llm/cli_exports/orchestrator.py +96 -0
  10. code2llm-0.5.38/code2llm/cli_exports/prompt.py +162 -0
  11. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/project_yaml_exporter.py +99 -83
  12. code2llm-0.5.38/code2llm/exporters/validate_project.py +118 -0
  13. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/nlp/__init__.py +1 -1
  14. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm.egg-info/PKG-INFO +4 -4
  15. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm.egg-info/SOURCES.txt +7 -1
  16. {code2llm-0.5.37 → code2llm-0.5.38}/pyproject.toml +1 -1
  17. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_prompt_txt.py +6 -4
  18. code2llm-0.5.37/code2llm/cli_exports.py +0 -633
  19. {code2llm-0.5.37 → code2llm-0.5.38}/LICENSE +0 -0
  20. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/__main__.py +0 -0
  21. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/__init__.py +0 -0
  22. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/call_graph.py +0 -0
  23. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/cfg.py +0 -0
  24. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/coupling.py +0 -0
  25. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/data_analysis.py +0 -0
  26. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/dfg.py +0 -0
  27. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/pipeline_detector.py +0 -0
  28. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/side_effects.py +0 -0
  29. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/smells.py +0 -0
  30. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/analysis/type_inference.py +0 -0
  31. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/__init__.py +0 -0
  32. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/analyzer.py +0 -0
  33. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/config.py +0 -0
  34. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/core/__init__.py +0 -0
  35. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/core/file_analyzer.py +0 -0
  36. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/core/file_cache.py +0 -0
  37. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/core/file_filter.py +0 -0
  38. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/core/refactoring.py +0 -0
  39. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/large_repo.py +0 -0
  40. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/models.py +0 -0
  41. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/streaming/__init__.py +0 -0
  42. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/streaming/cache.py +0 -0
  43. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/streaming/incremental.py +0 -0
  44. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/streaming/prioritizer.py +0 -0
  45. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/streaming/scanner.py +0 -0
  46. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/streaming/strategies.py +0 -0
  47. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/streaming_analyzer.py +0 -0
  48. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/core/toon_size_manager.py +0 -0
  49. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/__init__.py +0 -0
  50. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/article_view.py +0 -0
  51. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/base.py +0 -0
  52. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/context_exporter.py +0 -0
  53. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/context_view.py +0 -0
  54. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/evolution_exporter.py +0 -0
  55. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/flow_constants.py +0 -0
  56. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/flow_exporter.py +0 -0
  57. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/flow_renderer.py +0 -0
  58. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/html_dashboard.py +0 -0
  59. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/json_exporter.py +0 -0
  60. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/llm_exporter.py +0 -0
  61. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/map_exporter.py +0 -0
  62. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/mermaid_exporter.py +0 -0
  63. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/readme_exporter.py +0 -0
  64. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/report_generators.py +0 -0
  65. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/toon/__init__.py +0 -0
  66. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/toon/helpers.py +0 -0
  67. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/toon/metrics.py +0 -0
  68. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/toon/module_detail.py +0 -0
  69. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/toon/renderer.py +0 -0
  70. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/toon.py +0 -0
  71. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/toon_view.py +0 -0
  72. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/exporters/yaml_exporter.py +0 -0
  73. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/generators/__init__.py +0 -0
  74. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/generators/llm_flow.py +0 -0
  75. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/generators/llm_task.py +0 -0
  76. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/generators/mermaid.py +0 -0
  77. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/nlp/config.py +0 -0
  78. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/nlp/entity_resolution.py +0 -0
  79. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/nlp/intent_matching.py +0 -0
  80. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/nlp/normalization.py +0 -0
  81. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/nlp/pipeline.py +0 -0
  82. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/patterns/__init__.py +0 -0
  83. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/patterns/detector.py +0 -0
  84. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/refactor/__init__.py +0 -0
  85. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm/refactor/prompt_engine.py +0 -0
  86. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm.egg-info/dependency_links.txt +0 -0
  87. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm.egg-info/entry_points.txt +0 -0
  88. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm.egg-info/requires.txt +0 -0
  89. {code2llm-0.5.37 → code2llm-0.5.38}/code2llm.egg-info/top_level.txt +0 -0
  90. {code2llm-0.5.37 → code2llm-0.5.38}/setup.cfg +0 -0
  91. {code2llm-0.5.37 → code2llm-0.5.38}/setup.py +0 -0
  92. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_advanced_analysis.py +0 -0
  93. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_analyzer.py +0 -0
  94. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_deep_analysis.py +0 -0
  95. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_edge_cases.py +0 -0
  96. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_flow_exporter.py +0 -0
  97. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_format_quality.py +0 -0
  98. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_nlp_pipeline.py +0 -0
  99. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_pipeline_detector.py +0 -0
  100. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_prompt_engine.py +0 -0
  101. {code2llm-0.5.37 → code2llm-0.5.38}/tests/test_refactoring_engine.py +0 -0
  102. {code2llm-0.5.37 → code2llm-0.5.38}/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.37
3
+ Version: 0.5.38
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
@@ -60,7 +60,7 @@ When you run `code2llm ./ -f all`, the following files are created:
60
60
 
61
61
  | File | Format | Purpose | Key Insights |
62
62
  |------|--------|---------|--------------|
63
- | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 55 critical functions, 1 god modules |
63
+ | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 52 critical functions, 0 god modules |
64
64
  | `project.toon` | **TOON** | **🧠 Project logic** - Compact module view from code2logic | Generated via code2logic integration |
65
65
 
66
66
  ### 🤖 LLM-Ready Documentation
@@ -375,9 +375,9 @@ code2llm ./ -f yaml --separate-orphans
375
375
 
376
376
  **Generated by**: `code2llm ./ -f all --readme`
377
377
  **Analysis Date**: 2026-03-05
378
- **Total Functions**: 774
378
+ **Total Functions**: 820
379
379
  **Total Classes**: 104
380
- **Modules**: 92
380
+ **Modules**: 102
381
381
 
382
382
  For more information about code2llm, visit: https://github.com/tom-sapletta/code2llm
383
383
 
@@ -10,7 +10,7 @@ When you run `code2llm ./ -f all`, the following files are created:
10
10
 
11
11
  | File | Format | Purpose | Key Insights |
12
12
  |------|--------|---------|--------------|
13
- | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 55 critical functions, 1 god modules |
13
+ | `analysis.toon` | **TOON** | **🔥 Health diagnostics** - Complexity, god modules, coupling | 52 critical functions, 0 god modules |
14
14
  | `project.toon` | **TOON** | **🧠 Project logic** - Compact module view from code2logic | Generated via code2logic integration |
15
15
 
16
16
  ### 🤖 LLM-Ready Documentation
@@ -325,9 +325,9 @@ code2llm ./ -f yaml --separate-orphans
325
325
 
326
326
  **Generated by**: `code2llm ./ -f all --readme`
327
327
  **Analysis Date**: 2026-03-05
328
- **Total Functions**: 774
328
+ **Total Functions**: 820
329
329
  **Total Classes**: 104
330
- **Modules**: 92
330
+ **Modules**: 102
331
331
 
332
332
  For more information about code2llm, visit: https://github.com/tom-sapletta/code2llm
333
333
 
@@ -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.37"
11
+ __version__ = "0.5.38"
12
12
  __author__ = "STTS Project"
13
13
 
14
14
  # Core analysis components (lightweight, always needed)
@@ -18,6 +18,7 @@ from .cli_exports import (
18
18
  _export_simple_formats, _export_yaml, _export_mermaid, _export_refactor_prompts,
19
19
  _export_project_yaml, _run_report,
20
20
  )
21
+ from .cli_analysis import _run_analysis
21
22
 
22
23
 
23
24
 
@@ -459,247 +460,7 @@ def main():
459
460
  return 0
460
461
 
461
462
 
462
- def _run_analysis(args, source_path: Path, output_dir: Path):
463
- """Run code analysis with configured strategy.
464
-
465
- Returns AnalysisResult or exits on error.
466
- For large repos, may analyze in chunks and merge results.
467
- """
468
- from .core.large_repo import (
469
- HierarchicalRepoSplitter, should_use_chunking, get_analysis_plan
470
- )
471
-
472
- # Check if we should use chunked analysis
473
- # Auto-chunk when estimated output > chunk_size (default 256KB = ~85 files)
474
- # --no-chunk explicitly disables chunking
475
- use_chunking = (
476
- not args.no_chunk and
477
- (args.chunk or should_use_chunking(source_path, args.chunk_size))
478
- )
479
-
480
- if use_chunking:
481
- if args.verbose:
482
- print(f"Large repository detected - using chunked analysis")
483
- # Set chunk flag so export knows to use chunked mode
484
- args.chunk = True
485
- return _run_chunked_analysis(args, source_path, output_dir)
486
-
487
- # Standard single-analysis flow
488
- config = Config(
489
- mode=args.mode,
490
- max_depth_enumeration=args.max_depth,
491
- detect_state_machines=not args.no_patterns,
492
- detect_recursion=not args.no_patterns,
493
- output_dir=str(output_dir)
494
- )
495
-
496
- try:
497
- if args.streaming or args.strategy in ['quick', 'deep']:
498
- result = _run_streaming_analysis(args, config, source_path)
499
- else:
500
- analyzer = ProjectAnalyzer(config)
501
- result = analyzer.analyze_project(str(source_path))
502
-
503
- if args.verbose:
504
- print(f"\nAnalysis complete:")
505
- print(f" - Functions: {len(result.functions)}")
506
- print(f" - Classes: {len(result.classes)}")
507
- print(f" - CFG nodes: {len(result.nodes)}")
508
- print(f" - CFG edges: {len(result.edges)}")
509
-
510
- return result
511
-
512
- except Exception as e:
513
- print(f"Error during analysis: {e}", file=sys.stderr)
514
- sys.exit(1)
515
-
516
-
517
- def _run_chunked_analysis(args, source_path: Path, output_dir: Path):
518
- """Analyze large repository using hierarchical chunking.
519
-
520
- Strategy:
521
- 1. Level 1 folders first
522
- 2. If >256KB, split to level 2 subfolders
523
- 3. If still too big, use file chunking
524
- """
525
- from .core.large_repo import HierarchicalRepoSplitter
526
-
527
- splitter = HierarchicalRepoSplitter(
528
- size_limit_kb=args.chunk_size,
529
- max_files_per_chunk=args.max_files_per_chunk
530
- )
531
-
532
- # Get hierarchical analysis plan
533
- subprojects = splitter.get_analysis_plan(source_path)
534
-
535
- if args.verbose:
536
- print(f"Hierarchical analysis plan ({len(subprojects)} chunks):")
537
- level_counts = {}
538
- for sp in subprojects:
539
- level_counts[sp.level] = level_counts.get(sp.level, 0) + 1
540
-
541
- for level in sorted(level_counts.keys()):
542
- level_name = {0: 'root', 1: 'level-1', 2: 'level-2', 3: 'file-chunks'}.get(level, f'level-{level}')
543
- print(f" {level_name}: {level_counts[level]} chunks")
544
-
545
- print("\nChunks:")
546
- for sp in subprojects:
547
- level_indicator = " " * sp.level
548
- size_info = f"~{sp.estimated_size_kb}KB"
549
- print(f"{level_indicator}{sp.name}: {sp.file_count} files ({size_info})")
550
-
551
- # Filter subprojects if requested
552
- if args.only_subproject:
553
- subprojects = [sp for sp in subprojects if sp.name == args.only_subproject or sp.name.startswith(args.only_subproject + '.')]
554
- if not subprojects:
555
- print(f"Error: Subproject '{args.only_subproject}' not found", file=sys.stderr)
556
- sys.exit(1)
557
-
558
- if args.skip_subprojects:
559
- subprojects = [sp for sp in subprojects if not any(sp.name.startswith(skip) for skip in args.skip_subprojects)]
560
-
561
- # Analyze each subproject
562
- all_results = []
563
- for i, subproject in enumerate(subprojects, 1):
564
- if args.verbose:
565
- level_name = {0: 'root', 1: 'L1', 2: 'L2', 3: 'chunk'}.get(subproject.level, f'L{subproject.level}')
566
- print(f"\n[{i}/{len(subprojects)}] Analyzing [{level_name}]: {subproject.name}")
567
-
568
- sp_output_dir = output_dir / subproject.name.replace('.', '_')
569
- sp_output_dir.mkdir(parents=True, exist_ok=True)
570
-
571
- result = _analyze_subproject(args, subproject, sp_output_dir)
572
- if result:
573
- all_results.append((subproject.name, result, sp_output_dir))
574
-
575
- # Create merged summary
576
- merged_result = _merge_chunked_results(all_results, source_path)
577
-
578
- if args.verbose:
579
- print(f"\nChunked analysis complete:")
580
- print(f" - Chunks analyzed: {len(all_results)}")
581
- print(f" - Total functions: {len(merged_result.functions)}")
582
- print(f" - Total classes: {len(merged_result.classes)}")
583
-
584
- return merged_result
585
-
586
-
587
- def _analyze_subproject(args, subproject, output_dir: Path):
588
- """Analyze and export a single subproject."""
589
- from .core.analyzer import ProjectAnalyzer
590
- from .core.config import Config
591
- from .cli_exports import _export_simple_formats, _export_evolution, _export_chunked_prompt_txt, _export_code2logic
592
-
593
- config = Config(
594
- mode=args.mode,
595
- max_depth_enumeration=args.max_depth,
596
- detect_state_machines=not args.no_patterns,
597
- detect_recursion=not args.no_patterns,
598
- output_dir=str(output_dir),
599
- verbose=args.verbose
600
- )
601
-
602
- analyzer = ProjectAnalyzer(config)
603
-
604
- try:
605
- # Analyze subproject
606
- result = analyzer.analyze_project(str(subproject.path))
607
-
608
- # Export results for this subproject
609
- formats = [f.strip() for f in args.format.split(',')]
610
- if 'all' in formats:
611
- formats = ['toon', 'context', 'evolution', 'code2logic']
612
-
613
- # Export simple formats (toon, context)
614
- _export_simple_formats(args, result, output_dir, formats)
615
-
616
- # Export evolution
617
- if 'evolution' in formats or 'all' in formats:
618
- _export_evolution(args, result, output_dir)
619
-
620
- if args.verbose:
621
- print(f" ✓ Exported {subproject.name}: {len(result.functions)} functions")
622
-
623
- return result
624
- except Exception as e:
625
- print(f"Warning: Failed to analyze {subproject.name}: {e}", file=sys.stderr)
626
- return None
627
-
628
-
629
- def _merge_chunked_results(all_results, source_path: Path):
630
- """Merge results from multiple subproject analyses."""
631
- from .core.models import AnalysisResult
632
-
633
- merged = AnalysisResult(project_path=str(source_path))
634
-
635
- for name, result, output_dir in all_results:
636
- if not result:
637
- continue
638
-
639
- # Prefix with subproject name to avoid collisions
640
- prefix = f"{name}."
641
-
642
- # Merge functions
643
- for func_name, func_info in result.functions.items():
644
- new_name = f"{prefix}{func_name}" if '.' not in func_name else func_name
645
- merged.functions[new_name] = func_info
646
-
647
- # Merge classes
648
- for class_name, class_info in result.classes.items():
649
- new_name = f"{prefix}{class_name}" if '.' not in class_name else class_name
650
- merged.classes[new_name] = class_info
651
-
652
- # Merge modules
653
- for mod_name, mod_info in result.modules.items():
654
- new_name = f"{prefix}{mod_name}" if '.' not in mod_name else mod_name
655
- merged.modules[new_name] = mod_info
656
-
657
- # Merge nodes and edges (simplified - just count)
658
- merged.nodes.update(result.nodes)
659
- merged.edges.extend(result.edges)
660
-
661
- return merged
662
-
663
-
664
- def _run_streaming_analysis(args, config, source_path: Path):
665
- """Run streaming analysis with progress reporting."""
666
- from .core.streaming_analyzer import (
667
- StreamingAnalyzer, STRATEGY_QUICK,
668
- STRATEGY_STANDARD, STRATEGY_DEEP
669
- )
670
-
671
- strategy_map = {
672
- 'quick': STRATEGY_QUICK,
673
- 'standard': STRATEGY_STANDARD,
674
- 'deep': STRATEGY_DEEP
675
- }
676
- strategy = strategy_map.get(args.strategy, STRATEGY_STANDARD)
677
-
678
- # Adjust strategy for memory limit
679
- strategy.max_files_in_memory = min(
680
- strategy.max_files_in_memory,
681
- args.max_memory // 10
682
- )
683
-
684
- analyzer = StreamingAnalyzer(config, strategy)
685
-
686
- if args.verbose:
687
- def on_progress(update):
688
- pct = update.get('percentage', 0)
689
- print(f"\r[{pct:.0f}%] {update.get('message', '')}", end='', flush=True)
690
- analyzer.set_progress_callback(on_progress)
691
-
692
- print(f"Analyzing with {args.strategy} strategy...")
693
- for update in analyzer.analyze_streaming(str(source_path)):
694
- if update['type'] == 'complete':
695
- if args.verbose:
696
- print()
697
- print(f"Completed in {update.get('elapsed_seconds', 0):.1f}s")
698
-
699
- # Re-run standard analyzer for full results
700
- # TODO: Modify streaming to accumulate results properly
701
- analyzer = ProjectAnalyzer(config)
702
- return analyzer.analyze_project(str(source_path))
463
+ # Analysis functions are in cli_analysis.py — imported at top of file
703
464
 
704
465
 
705
466
  def generate_llm_context(args_list):
@@ -0,0 +1,282 @@
1
+ """Analysis execution — chunked, streaming, and standard analysis flows.
2
+
3
+ Extracted from cli.py to isolate analysis orchestration from CLI parsing.
4
+ """
5
+
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import List, Optional, Tuple
9
+
10
+
11
+ def _run_analysis(args, source_path: Path, output_dir: Path):
12
+ """Run code analysis with configured strategy.
13
+
14
+ Returns AnalysisResult or exits on error.
15
+ For large repos, may analyze in chunks and merge results.
16
+ """
17
+ from .core.large_repo import should_use_chunking
18
+
19
+ # --no-chunk explicitly disables chunking
20
+ use_chunking = (
21
+ not args.no_chunk and
22
+ (args.chunk or should_use_chunking(source_path, args.chunk_size))
23
+ )
24
+
25
+ if use_chunking:
26
+ if args.verbose:
27
+ print(f"Large repository detected - using chunked analysis")
28
+ args.chunk = True
29
+ return _run_chunked_analysis(args, source_path, output_dir)
30
+
31
+ return _run_standard_analysis(args, source_path, output_dir)
32
+
33
+
34
+ def _run_standard_analysis(args, source_path: Path, output_dir: Path):
35
+ """Standard single-project analysis flow."""
36
+ from .core.config import Config
37
+ from .core.analyzer import ProjectAnalyzer
38
+
39
+ config = _build_config(args, output_dir)
40
+
41
+ try:
42
+ if args.streaming or args.strategy in ['quick', 'deep']:
43
+ result = _run_streaming_analysis(args, config, source_path)
44
+ else:
45
+ analyzer = ProjectAnalyzer(config)
46
+ result = analyzer.analyze_project(str(source_path))
47
+
48
+ if args.verbose:
49
+ _print_analysis_summary(result)
50
+
51
+ return result
52
+
53
+ except Exception as e:
54
+ print(f"Error during analysis: {e}", file=sys.stderr)
55
+ sys.exit(1)
56
+
57
+
58
+ def _build_config(args, output_dir: Path):
59
+ """Build analysis Config from CLI args."""
60
+ from .core.config import Config
61
+ return Config(
62
+ mode=args.mode,
63
+ max_depth_enumeration=args.max_depth,
64
+ detect_state_machines=not args.no_patterns,
65
+ detect_recursion=not args.no_patterns,
66
+ output_dir=str(output_dir)
67
+ )
68
+
69
+
70
+ def _print_analysis_summary(result) -> None:
71
+ """Print analysis completion summary."""
72
+ print(f"\nAnalysis complete:")
73
+ print(f" - Functions: {len(result.functions)}")
74
+ print(f" - Classes: {len(result.classes)}")
75
+ print(f" - CFG nodes: {len(result.nodes)}")
76
+ print(f" - CFG edges: {len(result.edges)}")
77
+
78
+
79
+ # ------------------------------------------------------------------
80
+ # Chunked analysis
81
+ # ------------------------------------------------------------------
82
+ def _run_chunked_analysis(args, source_path: Path, output_dir: Path):
83
+ """Analyze large repository using hierarchical chunking.
84
+
85
+ Strategy:
86
+ 1. Level 1 folders first
87
+ 2. If >256KB, split to level 2 subfolders
88
+ 3. If still too big, use file chunking
89
+ """
90
+ from .core.large_repo import HierarchicalRepoSplitter
91
+
92
+ splitter = HierarchicalRepoSplitter(
93
+ size_limit_kb=args.chunk_size,
94
+ max_files_per_chunk=args.max_files_per_chunk
95
+ )
96
+
97
+ subprojects = splitter.get_analysis_plan(source_path)
98
+
99
+ if args.verbose:
100
+ _print_chunked_plan(subprojects)
101
+
102
+ subprojects = _filter_subprojects(args, subprojects)
103
+
104
+ all_results = _analyze_all_subprojects(args, subprojects, output_dir)
105
+
106
+ merged_result = _merge_chunked_results(all_results, source_path)
107
+
108
+ if args.verbose:
109
+ print(f"\nChunked analysis complete:")
110
+ print(f" - Chunks analyzed: {len(all_results)}")
111
+ print(f" - Total functions: {len(merged_result.functions)}")
112
+ print(f" - Total classes: {len(merged_result.classes)}")
113
+
114
+ return merged_result
115
+
116
+
117
+ def _print_chunked_plan(subprojects) -> None:
118
+ """Print hierarchical analysis plan summary."""
119
+ print(f"Hierarchical analysis plan ({len(subprojects)} chunks):")
120
+ level_counts = {}
121
+ for sp in subprojects:
122
+ level_counts[sp.level] = level_counts.get(sp.level, 0) + 1
123
+
124
+ for level in sorted(level_counts.keys()):
125
+ level_name = {0: 'root', 1: 'level-1', 2: 'level-2', 3: 'file-chunks'}.get(level, f'level-{level}')
126
+ print(f" {level_name}: {level_counts[level]} chunks")
127
+
128
+ print("\nChunks:")
129
+ for sp in subprojects:
130
+ level_indicator = " " * sp.level
131
+ size_info = f"~{sp.estimated_size_kb}KB"
132
+ print(f"{level_indicator}{sp.name}: {sp.file_count} files ({size_info})")
133
+
134
+
135
+ def _filter_subprojects(args, subprojects) -> list:
136
+ """Apply --only-subproject and --skip-subprojects filters."""
137
+ if args.only_subproject:
138
+ subprojects = [
139
+ sp for sp in subprojects
140
+ if sp.name == args.only_subproject
141
+ or sp.name.startswith(args.only_subproject + '.')
142
+ ]
143
+ if not subprojects:
144
+ print(f"Error: Subproject '{args.only_subproject}' not found", file=sys.stderr)
145
+ sys.exit(1)
146
+
147
+ if args.skip_subprojects:
148
+ subprojects = [
149
+ sp for sp in subprojects
150
+ if not any(sp.name.startswith(skip) for skip in args.skip_subprojects)
151
+ ]
152
+
153
+ return subprojects
154
+
155
+
156
+ def _analyze_all_subprojects(args, subprojects, output_dir: Path) -> list:
157
+ """Analyze each subproject and collect results."""
158
+ all_results = []
159
+ for i, subproject in enumerate(subprojects, 1):
160
+ if args.verbose:
161
+ level_name = {0: 'root', 1: 'L1', 2: 'L2', 3: 'chunk'}.get(subproject.level, f'L{subproject.level}')
162
+ print(f"\n[{i}/{len(subprojects)}] Analyzing [{level_name}]: {subproject.name}")
163
+
164
+ sp_output_dir = output_dir / subproject.name.replace('.', '_')
165
+ sp_output_dir.mkdir(parents=True, exist_ok=True)
166
+
167
+ result = _analyze_subproject(args, subproject, sp_output_dir)
168
+ if result:
169
+ all_results.append((subproject.name, result, sp_output_dir))
170
+ return all_results
171
+
172
+
173
+ def _analyze_subproject(args, subproject, output_dir: Path):
174
+ """Analyze and export a single subproject."""
175
+ from .core.analyzer import ProjectAnalyzer
176
+ from .core.config import Config
177
+ from .cli_exports import _export_simple_formats, _export_evolution
178
+
179
+ config = Config(
180
+ mode=args.mode,
181
+ max_depth_enumeration=args.max_depth,
182
+ detect_state_machines=not args.no_patterns,
183
+ detect_recursion=not args.no_patterns,
184
+ output_dir=str(output_dir),
185
+ verbose=args.verbose
186
+ )
187
+
188
+ analyzer = ProjectAnalyzer(config)
189
+
190
+ try:
191
+ result = analyzer.analyze_project(str(subproject.path))
192
+
193
+ formats = [f.strip() for f in args.format.split(',')]
194
+ if 'all' in formats:
195
+ formats = ['toon', 'context', 'evolution', 'code2logic']
196
+
197
+ _export_simple_formats(args, result, output_dir, formats)
198
+
199
+ if 'evolution' in formats or 'all' in formats:
200
+ _export_evolution(args, result, output_dir)
201
+
202
+ if args.verbose:
203
+ print(f" ✓ Exported {subproject.name}: {len(result.functions)} functions")
204
+
205
+ return result
206
+ except Exception as e:
207
+ print(f"Warning: Failed to analyze {subproject.name}: {e}", file=sys.stderr)
208
+ return None
209
+
210
+
211
+ def _merge_chunked_results(all_results, source_path: Path):
212
+ """Merge results from multiple subproject analyses."""
213
+ from .core.models import AnalysisResult
214
+
215
+ merged = AnalysisResult(project_path=str(source_path))
216
+
217
+ for name, result, output_dir in all_results:
218
+ if not result:
219
+ continue
220
+
221
+ prefix = f"{name}."
222
+
223
+ for func_name, func_info in result.functions.items():
224
+ new_name = f"{prefix}{func_name}" if '.' not in func_name else func_name
225
+ merged.functions[new_name] = func_info
226
+
227
+ for class_name, class_info in result.classes.items():
228
+ new_name = f"{prefix}{class_name}" if '.' not in class_name else class_name
229
+ merged.classes[new_name] = class_info
230
+
231
+ for mod_name, mod_info in result.modules.items():
232
+ new_name = f"{prefix}{mod_name}" if '.' not in mod_name else mod_name
233
+ merged.modules[new_name] = mod_info
234
+
235
+ merged.nodes.update(result.nodes)
236
+ merged.edges.extend(result.edges)
237
+
238
+ return merged
239
+
240
+
241
+ # ------------------------------------------------------------------
242
+ # Streaming analysis
243
+ # ------------------------------------------------------------------
244
+ def _run_streaming_analysis(args, config, source_path: Path):
245
+ """Run streaming analysis with progress reporting."""
246
+ from .core.analyzer import ProjectAnalyzer
247
+ from .core.streaming_analyzer import (
248
+ StreamingAnalyzer, STRATEGY_QUICK,
249
+ STRATEGY_STANDARD, STRATEGY_DEEP
250
+ )
251
+
252
+ strategy_map = {
253
+ 'quick': STRATEGY_QUICK,
254
+ 'standard': STRATEGY_STANDARD,
255
+ 'deep': STRATEGY_DEEP
256
+ }
257
+ strategy = strategy_map.get(args.strategy, STRATEGY_STANDARD)
258
+
259
+ strategy.max_files_in_memory = min(
260
+ strategy.max_files_in_memory,
261
+ args.max_memory // 10
262
+ )
263
+
264
+ analyzer = StreamingAnalyzer(config, strategy)
265
+
266
+ if args.verbose:
267
+ def on_progress(update):
268
+ pct = update.get('percentage', 0)
269
+ print(f"\r[{pct:.0f}%] {update.get('message', '')}", end='', flush=True)
270
+ analyzer.set_progress_callback(on_progress)
271
+
272
+ print(f"Analyzing with {args.strategy} strategy...")
273
+ for update in analyzer.analyze_streaming(str(source_path)):
274
+ if update['type'] == 'complete':
275
+ if args.verbose:
276
+ print()
277
+ print(f"Completed in {update.get('elapsed_seconds', 0):.1f}s")
278
+
279
+ # Re-run standard analyzer for full results
280
+ # TODO: Modify streaming to accumulate results properly
281
+ analyzer = ProjectAnalyzer(config)
282
+ return analyzer.analyze_project(str(source_path))
@@ -0,0 +1,48 @@
1
+ """CLI export functions — split into thematic modules.
2
+
3
+ Thin re-export so existing ``from .cli_exports import ...`` continues to work.
4
+ """
5
+
6
+ from .formats import (
7
+ _export_simple_formats,
8
+ _export_yaml,
9
+ _export_mermaid,
10
+ _export_evolution,
11
+ _export_data_structures,
12
+ _export_context_fallback,
13
+ _export_readme,
14
+ _export_refactor_prompts,
15
+ _export_project_yaml,
16
+ _run_report,
17
+ )
18
+ from .prompt import (
19
+ _export_prompt_txt,
20
+ _export_chunked_prompt_txt,
21
+ )
22
+ from .code2logic import (
23
+ _export_code2logic,
24
+ )
25
+ from .orchestrator import (
26
+ _run_exports,
27
+ _export_single_project,
28
+ _export_chunked_results,
29
+ )
30
+
31
+ __all__ = [
32
+ "_export_simple_formats",
33
+ "_export_yaml",
34
+ "_export_mermaid",
35
+ "_export_evolution",
36
+ "_export_data_structures",
37
+ "_export_context_fallback",
38
+ "_export_readme",
39
+ "_export_refactor_prompts",
40
+ "_export_project_yaml",
41
+ "_run_report",
42
+ "_export_prompt_txt",
43
+ "_export_chunked_prompt_txt",
44
+ "_export_code2logic",
45
+ "_run_exports",
46
+ "_export_single_project",
47
+ "_export_chunked_results",
48
+ ]