forked from HAL9000/cleveragents-core
4232907ab9
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
243 lines
8.3 KiB
Python
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 == ""
|