Files
temp/features/steps/large_project_decomposition_coverage_steps.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

243 lines
8.3 KiB
Python

"""Additional step implementations for decomposition coverage scenarios.
Exercises edge cases and code paths not covered by the primary
scenarios: config validation bounds, token estimation errors,
token_map parameter, closure trimming/clearing, service wrappers,
property access, empty-list helpers, and short-path directory keys.
"""
from __future__ import annotations
from typing import Any
from behave import given, then, when # type: ignore[import-untyped]
from cleveragents.application.services.decomposition_graph import (
DependencyClosureComputer,
)
from cleveragents.application.services.decomposition_models import (
DecompositionConfig,
DependencyEdge,
DependencyGraph,
DependencyType,
)
# ---------------------------------------------------------------------------
# Additional config validation
# ---------------------------------------------------------------------------
@when("I create a config with max_files_per_subplan {n:d}")
def step_when_create_bad_max_files(context: Any, n: int) -> None:
try:
DecompositionConfig(max_files_per_subplan=n)
except ValueError as exc:
context.error = exc
@when("I create a config with max_tokens_per_subplan {n:d}")
def step_when_create_bad_max_tokens(context: Any, n: int) -> None:
try:
DecompositionConfig(max_tokens_per_subplan=n)
except ValueError as exc:
context.error = exc
@when("I create a config with min_files_per_subplan {n:d}")
def step_when_create_bad_min_files(context: Any, n: int) -> None:
try:
DecompositionConfig(min_files_per_subplan=n)
except ValueError as exc:
context.error = exc
# ---------------------------------------------------------------------------
# estimate_tokens edge cases
# ---------------------------------------------------------------------------
@when("I estimate tokens for a nonexistent file")
def step_when_estimate_nonexistent(context: Any) -> None:
from cleveragents.application.services.decomposition_clustering import (
estimate_tokens_for_path,
)
context.token_estimate = estimate_tokens_for_path("/no/such/file.py")
@then("the token estimate should be {n:d}")
def step_then_token_estimate(context: Any, n: int) -> None:
assert context.token_estimate == n
# ---------------------------------------------------------------------------
# cluster_by_size with token_map
# ---------------------------------------------------------------------------
@given("a set of files with known token counts")
def step_given_files_with_tokens(context: Any) -> None:
context.sized_files = ["a.py", "b.py", "c.py", "d.py"]
context.token_map = {"a.py": 40, "b.py": 60, "c.py": 30, "d.py": 70}
@when("I cluster by size with a token_map and max_tokens {n:d}")
def step_when_cluster_by_size_tokenmap(context: Any, n: int) -> None:
from cleveragents.application.services.decomposition_clustering import (
ClusteringStrategy,
)
context.sized_clusters = ClusteringStrategy.cluster_by_size(
context.sized_files, n, token_map=context.token_map
)
@then("the clusters should respect the token_map values")
def step_then_clusters_respect_tokenmap(context: Any) -> None:
assert len(context.sized_clusters) >= 2
for cluster in context.sized_clusters:
total = sum(context.token_map[f] for f in cluster)
assert total <= 100 + 70 # max_tokens + single large file
# ---------------------------------------------------------------------------
# closure trimming with multiple roots
# ---------------------------------------------------------------------------
@given("a dependency graph with two roots each reaching 3 nodes")
def step_given_two_root_graph(context: Any) -> None:
g = DependencyGraph()
for n in ("R1", "R1a", "R1b", "R2", "R2a", "R2b"):
g.add_node(n)
g.add_edge(DependencyEdge("R1", "R1a", DependencyType.IMPORT))
g.add_edge(DependencyEdge("R1", "R1b", DependencyType.IMPORT))
g.add_edge(DependencyEdge("R2", "R2a", DependencyType.IMPORT))
g.add_edge(DependencyEdge("R2", "R2b", DependencyType.IMPORT))
context.graph = g
@when("I compute closure from both roots with cutoff {n:d}")
def step_when_closure_both_roots_cutoff(context: Any, n: int) -> None:
computer = DependencyClosureComputer()
context.closure = computer.compute_closure(context.graph, ["R1", "R2"], cutoff=n)
# ---------------------------------------------------------------------------
# clear cache
# ---------------------------------------------------------------------------
@when("I compute closure from A and then clear the cache")
def step_when_closure_and_clear(context: Any) -> None:
computer = DependencyClosureComputer()
context.closure_before = computer.compute_closure(context.graph, ["A"])
computer.clear_cache()
context.closure_after = computer.compute_closure(context.graph, ["A"])
@then("computing closure from A again should still work")
def step_then_closure_after_clear(context: Any) -> None:
assert context.closure_before == context.closure_after
# ---------------------------------------------------------------------------
# service wrapper methods
# ---------------------------------------------------------------------------
@when("I compute dependency order through the service")
def step_when_service_topo(context: Any) -> None:
context.service_order = context.svc.compute_dependency_order(context.graph)
@then("A should appear before B and C in the service order")
def step_then_service_a_before_bc(context: Any) -> None:
order = context.service_order
assert order.index("A") < order.index("B")
assert order.index("A") < order.index("C")
@then("B and C should appear before D in the service order")
def step_then_service_bc_before_d(context: Any) -> None:
order = context.service_order
assert order.index("B") < order.index("D")
assert order.index("C") < order.index("D")
@when("I compute closure through the service from A")
def step_when_service_closure(context: Any) -> None:
context.service_closure = context.svc.compute_closure(context.graph, ["A"])
@then("the service closure should contain A, B, C, D")
def step_then_service_closure_abcd(context: Any) -> None:
assert {"A", "B", "C", "D"} <= context.service_closure
# ---------------------------------------------------------------------------
# decision_service property
# ---------------------------------------------------------------------------
@then("the decision service property should return the mock")
def step_then_ds_property_mock(context: Any) -> None:
assert context.svc.decision_service is context.mock_ds
@then("the decision service property should return None")
def step_then_ds_property_none(context: Any) -> None:
assert context.svc.decision_service is None
# ---------------------------------------------------------------------------
# empty file edge cases
# ---------------------------------------------------------------------------
@when("I compute dominant extension of an empty list")
def step_when_dominant_ext_empty(context: Any) -> None:
from cleveragents.application.services.decomposition_service import (
_dominant_extension,
)
context.dominant_ext = _dominant_extension([])
@then("the dominant extension should be empty")
def step_then_dominant_ext_empty(context: Any) -> None:
assert context.dominant_ext == ""
@when("I compute common prefix of an empty list")
def step_when_common_prefix_empty(context: Any) -> None:
from cleveragents.application.services.decomposition_service import (
_common_prefix,
)
context.common_pfx = _common_prefix([])
@then("the common prefix should be empty")
def step_then_common_prefix_empty(context: Any) -> None:
assert context.common_pfx == ""
# ---------------------------------------------------------------------------
# directory key short path
# ---------------------------------------------------------------------------
@when("I compute directory key for a single-component path")
def step_when_dir_key_short(context: Any) -> None:
from cleveragents.application.services.decomposition_clustering import (
_directory_key,
)
context.dir_key = _directory_key("file.py", depth=2)
@then("the directory key should be empty string")
def step_then_dir_key_empty(context: Any) -> None:
assert context.dir_key == ""