Skip to content

Commit fad8c2a

Browse files
refactor(goal): code analysis engine
changes: - file: cli.py area: cli removed: [_merge_chunked_results, _run_chunked_analysis, _run_streaming_analysis, _analyze_subproject, _run_analysis, on_progress] - file: cli_analysis.py area: analyzer added: [_merge_chunked_results, _run_chunked_analysis, _build_config, _run_streaming_analysis, _run_standard_analysis, _analyze_all_subprojects, +6 more] - file: cli_exports.py area: cli removed: [_export_prompt_txt, _export_project_yaml, _export_chunked_results, _export_readme, _export_simple_formats, _run_exports, +24 more] - file: code2logic.py area: cli added: [_build_code2logic_cmd, _find_code2logic_output, _handle_code2logic_error, _normalize_code2logic_output, _check_code2logic_installed, _run_code2logic, +2 more] - file: formats.py area: cli added: [_export_project_yaml, _export_readme, _export_simple_formats, _run_report, _export_evolution, _export_refactor_prompts, +4 more] - file: orchestrator.py area: cli added: [_export_chunked_results, _export_single_project, _run_exports] - file: prompt.py area: cli added: [_export_prompt_txt, _get_prompt_paths, _build_subprojects_section, _build_missing_files_section, _get_missing_files, _export_chunked_prompt_txt, +3 more] - file: project_yaml_exporter.py area: core added: [_group_by_file, _compute_module_entry, _build_class_export, _compute_inbound_deps, _build_function_exports] modified: [_build_modules, _build_exports, ProjectYAMLExporter] - file: validate_project.py area: core added: [validate_project_yaml, _cross_check_toon, _check_required_keys] - file: test_prompt_txt.py area: test modified: [test_export_code2logic_does_not_add_quiet_flag_when_verbose, test_export_code2logic_adds_quiet_flag_when_not_verbose, TestCode2logicExport] dependencies: flow: "cli→cli_analysis→cli_exports, orchestrator→code2logic" - cli.py -> cli_exports.py - cli.py -> cli_analysis.py - cli_analysis.py -> cli_exports.py - orchestrator.py -> formats.py - orchestrator.py -> code2logic.py stats: lines: "+4032/-1831 (net +2201)" files: 14 complexity: "+98% complexity (monitor)"
1 parent b7560f2 commit fad8c2a

26 files changed

Lines changed: 4068 additions & 1827 deletions

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
## [0.5.38] - 2026-03-05
2+
3+
### Summary
4+
5+
refactor(goal): code analysis engine
6+
7+
### Docs
8+
9+
- docs: update README
10+
- docs: update context.md
11+
- docs: update README
12+
- docs: update context.md
13+
14+
### Test
15+
16+
- update tests/test_prompt_txt.py
17+
18+
### Other
19+
20+
- update analysis.toon
21+
- update batch_1/analysis.toon
22+
- update code2llm/cli.py
23+
- update code2llm/cli_analysis.py
24+
- update code2llm/cli_exports.py
25+
- update code2llm/cli_exports/__init__.py
26+
- update code2llm/cli_exports/code2logic.py
27+
- update code2llm/cli_exports/formats.py
28+
- update code2llm/cli_exports/orchestrator.py
29+
- update code2llm/cli_exports/prompt.py
30+
- ... and 6 more
31+
32+
133
## [0.5.37] - 2026-03-05
234

335
### Summary

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ When you run `code2llm ./ -f all`, the following files are created:
1010

1111
| File | Format | Purpose | Key Insights |
1212
|------|--------|---------|--------------|
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 |
1414
| `project.toon` | **TOON** | **🧠 Project logic** - Compact module view from code2logic | Generated via code2logic integration |
1515

1616
### 🤖 LLM-Ready Documentation
@@ -325,9 +325,9 @@ code2llm ./ -f yaml --separate-orphans
325325

326326
**Generated by**: `code2llm ./ -f all --readme`
327327
**Analysis Date**: 2026-03-05
328-
**Total Functions**: 774
328+
**Total Functions**: 820
329329
**Total Classes**: 104
330-
**Modules**: 92
330+
**Modules**: 102
331331

332332
For more information about code2llm, visit: https://github.com/tom-sapletta/code2llm
333333

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.5.37
1+
0.5.38

analysis.toon

Lines changed: 203 additions & 137 deletions
Large diffs are not rendered by default.

batch_1/analysis.toon

Lines changed: 216 additions & 150 deletions
Large diffs are not rendered by default.

code2llm/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
and entity resolution with multilingual support.
99
"""
1010

11-
__version__ = "0.5.37"
11+
__version__ = "0.5.38"
1212
__author__ = "STTS Project"
1313

1414
# Core analysis components (lightweight, always needed)

code2llm/cli.py

Lines changed: 2 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
_export_simple_formats, _export_yaml, _export_mermaid, _export_refactor_prompts,
1919
_export_project_yaml, _run_report,
2020
)
21+
from .cli_analysis import _run_analysis
2122

2223

2324

@@ -459,247 +460,7 @@ def main():
459460
return 0
460461

461462

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
703464

704465

705466
def generate_llm_context(args_list):

0 commit comments

Comments
 (0)