Files
cleveragents-core/features/steps/decomposition_test_helpers.py
freemo 4232907ab9 feat(plan): add large-project decomposition and dependency closure
Add hierarchical decomposition with 4+ levels and bounded context per
subplan. Implement decomposition heuristics (max_files_per_subplan,
max_tokens_per_subplan, language/dir clustering). Add dependency closure
computation for large graphs and DAG execution ordering. Add bounded
dependency closure with cutoff thresholds and memoization for 10K+
files. Record decomposition decisions in DecisionService
(strategy_choice + subplan_spawn entries).

New modules:
- decomposition_models.py: DecompositionConfig, DecompositionNode,
  DecompositionResult, DependencyEdge, DependencyGraph
- decomposition_clustering.py: ClusteringStrategy with directory,
  language, and size clustering plus deterministic sort
- decomposition_graph.py: DependencyClosureComputer with bounded
  closure, topological sort, cycle detection, and memoization
- decomposition_service.py: DecompositionService orchestrating
  hierarchy building and decision recording

Settings: planner_max_depth, planner_max_files_per_subplan,
planner_max_tokens_per_subplan, planner_min_files_per_subplan

Closes #205
2026-03-03 21:44:20 +00:00

113 lines
3.7 KiB
Python

"""Shared test helpers for decomposition BDD step files.
Provides factory functions for creating temporary files, dependency
graphs, and other test fixtures used by both the primary and coverage
step implementations.
"""
from __future__ import annotations
import itertools
import os
from cleveragents.application.services.decomposition_models import (
DependencyEdge,
DependencyGraph,
DependencyType,
)
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) # ~50 tokens
paths.append(fpath)
return sorted(paths)
def make_lang_files(tmpdir: str, py_count: int, ts_count: int) -> list[str]:
"""Create Python and TypeScript files in separate dirs."""
paths: list[str] = []
py_dir = os.path.join(tmpdir, "src", "python")
ts_dir = os.path.join(tmpdir, "src", "typescript")
os.makedirs(py_dir, exist_ok=True)
os.makedirs(ts_dir, exist_ok=True)
for i in range(py_count):
fpath = os.path.join(py_dir, f"mod_{i:04d}.py")
with open(fpath, "w") as fh:
fh.write("x" * 200)
paths.append(fpath)
for i in range(ts_count):
fpath = os.path.join(ts_dir, f"mod_{i:04d}.ts")
with open(fpath, "w") as fh:
fh.write("x" * 200)
paths.append(fpath)
return sorted(paths)
def make_dir_files(tmpdir: str) -> list[str]:
"""Create files in two distinct directories."""
paths: list[str] = []
for sub in ("src/api", "src/web"):
dirpath = os.path.join(tmpdir, sub)
os.makedirs(dirpath, exist_ok=True)
for i in range(50):
fpath = os.path.join(dirpath, f"f_{i:03d}.py")
with open(fpath, "w") as fh:
fh.write("x" * 200)
paths.append(fpath)
return sorted(paths)
def make_same_dir_files(tmpdir: str, count: int) -> list[str]:
"""Create *count* files all in the same directory with same extension."""
dirpath = os.path.join(tmpdir, "flat")
os.makedirs(dirpath, exist_ok=True)
paths: list[str] = []
for i in range(count):
fpath = os.path.join(dirpath, f"f_{i:04d}.py")
with open(fpath, "w") as fh:
fh.write("x" * 200)
paths.append(fpath)
return sorted(paths)
def chain_graph(*names: str) -> DependencyGraph:
"""Build a chain A -> B -> C -> ..."""
g = DependencyGraph()
for n in names:
g.add_node(n)
for src, tgt in itertools.pairwise(names):
g.add_edge(DependencyEdge(src, tgt, DependencyType.IMPORT))
return g
def diamond_graph() -> DependencyGraph:
"""Build A->B, A->C, B->D, C->D."""
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))
return g
def cycle_graph() -> DependencyGraph:
"""Build A -> B -> C -> A."""
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))
return g