"""Helper script for large-project decomposition Robot Framework tests. Usage: python helper_large_project_decompose.py below-threshold python helper_large_project_decompose.py multi-level python helper_large_project_decompose.py closure python helper_large_project_decompose.py topo-sort python helper_large_project_decompose.py cycle-detect python helper_large_project_decompose.py config-validation """ from __future__ import annotations import os import sys import tempfile from pathlib import Path # Ensure source tree is importable _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) from cleveragents.application.services.decomposition_graph import ( # noqa: E402 DependencyClosureComputer, ) from cleveragents.application.services.decomposition_models import ( # noqa: E402 DecompositionConfig, DependencyEdge, DependencyGraph, DependencyType, ) from cleveragents.application.services.decomposition_service import ( # noqa: E402 DecompositionService, ) def _make_files(tmpdir: str, count: int, levels: int = 1) -> list[str]: """Create *count* empty files spread across *levels* directories.""" paths: list[str] = [] for i in range(count): level = i % levels parts = [f"level{d}" for d in range(level + 1)] dirpath = os.path.join(tmpdir, *parts) os.makedirs(dirpath, exist_ok=True) fpath = os.path.join(dirpath, f"file_{i:05d}.py") with open(fpath, "w") as fh: fh.write("x" * 200) paths.append(fpath) return sorted(paths) def cmd_below_threshold() -> None: """Verify decomposition is skipped below threshold.""" tmpdir = tempfile.mkdtemp(prefix="decompose-") files = _make_files(tmpdir, 5, 1) svc = DecompositionService() result = svc.decompose(files, DecompositionConfig(min_files_per_subplan=10)) assert result.metrics.get("skipped") == 1 assert len(result.nodes) == 1 print("decompose-below-threshold-ok") def cmd_multi_level() -> None: """Verify multi-level decomposition produces multiple nodes.""" tmpdir = tempfile.mkdtemp(prefix="decompose-") files = _make_files(tmpdir, 500, 5) svc = DecompositionService() result = svc.decompose(files, DecompositionConfig(max_files_per_subplan=100)) assert len(result.nodes) > 1 assert result.max_depth_reached >= 1 print(f"decompose-multi-level-ok nodes={len(result.nodes)}") def cmd_closure() -> None: """Verify transitive closure computation.""" g = DependencyGraph() for n in ("A", "B", "C", "D"): g.add_node(n) g.add_edge(DependencyEdge("A", "B", DependencyType.IMPORT)) g.add_edge(DependencyEdge("B", "C", DependencyType.IMPORT)) g.add_edge(DependencyEdge("C", "D", DependencyType.IMPORT)) computer = DependencyClosureComputer() closure = computer.compute_closure(g, ["A"]) assert {"A", "B", "C", "D"} <= closure print(f"decompose-closure-ok size={len(closure)}") def cmd_topo_sort() -> None: """Verify topological sort produces valid order.""" g = DependencyGraph() for n in ("A", "B", "C", "D"): g.add_node(n) g.add_edge(DependencyEdge("A", "B", DependencyType.IMPORT)) g.add_edge(DependencyEdge("A", "C", DependencyType.IMPORT)) g.add_edge(DependencyEdge("B", "D", DependencyType.IMPORT)) g.add_edge(DependencyEdge("C", "D", DependencyType.IMPORT)) computer = DependencyClosureComputer() order = computer.topological_sort(g) assert order.index("A") < order.index("B") assert order.index("A") < order.index("C") assert order.index("B") < order.index("D") assert order.index("C") < order.index("D") print(f"decompose-topo-sort-ok order={order}") def cmd_cycle_detect() -> None: """Verify cycle detection finds a cycle.""" g = DependencyGraph() for n in ("A", "B", "C"): g.add_node(n) g.add_edge(DependencyEdge("A", "B", DependencyType.IMPORT)) g.add_edge(DependencyEdge("B", "C", DependencyType.IMPORT)) g.add_edge(DependencyEdge("C", "A", DependencyType.IMPORT)) computer = DependencyClosureComputer() cycles = computer.detect_cycles(g) assert len(cycles) >= 1 print(f"decompose-cycle-detect-ok cycles={len(cycles)}") def cmd_config_validation() -> None: """Verify invalid config raises ValueError.""" for bad_kwargs in ( {"max_depth": 0}, {"max_files_per_subplan": 0}, {"max_tokens_per_subplan": 0}, {"min_files_per_subplan": 0}, ): try: DecompositionConfig(**bad_kwargs) # type: ignore[arg-type] print(f"FAIL: {bad_kwargs} did not raise") sys.exit(1) except ValueError: pass # Valid config should work DecompositionConfig() print("decompose-config-ok") def main() -> None: """Dispatch subcommand.""" if len(sys.argv) < 2: print("Usage: helper_large_project_decompose.py ") sys.exit(1) cmd = sys.argv[1] dispatch = { "below-threshold": cmd_below_threshold, "multi-level": cmd_multi_level, "closure": cmd_closure, "topo-sort": cmd_topo_sort, "cycle-detect": cmd_cycle_detect, "config-validation": cmd_config_validation, } func = dispatch.get(cmd) if func is None: print(f"Unknown command: {cmd}") sys.exit(1) func() if __name__ == "__main__": main()