"""Step definitions for features/depth_breadth_projection.feature. Tests the Depth/Breadth Projection System and Skeleton Context Propagation: ProjectionSpec, ProjectedNode, DepthBreadthProjector, PlanContextInheritance, ChildContextResult, InheritanceConfig, and built-in DetailLevelMap presets. """ from __future__ import annotations from behave import given, then, when from behave.runner import Context from pydantic import ValidationError from cleveragents.application.services.depth_breadth_projection import ( ChildContextResult, DepthBreadthProjector, InheritanceConfig, PlanContextInheritance, ProjectedNode, ProjectionSpec, code_detail_map, database_detail_map, docs_detail_map, ) from cleveragents.domain.models.acms.crp import ContextRequest from cleveragents.domain.models.core.context_fragment import ( ContextFragment, FragmentProvenance, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _PROV = FragmentProvenance(resource_uri="test://projection") def _make_frag( uko_node: str = "test://node", content: str = "test", token_count: int = 100, relevance_score: float = 0.8, detail_depth: int = 3, ) -> ContextFragment: return ContextFragment( uko_node=uko_node, content=content, token_count=token_count, relevance_score=relevance_score, detail_depth=detail_depth, provenance=_PROV, ) class _FakeAssembledContext: """Minimal stand-in for AssembledContext.""" def __init__( self, fragments: list[ContextFragment], context_hash: str = "abc123", ) -> None: self.fragments = fragments self.context_hash = context_hash class _MockSkeletonCompressor: """Compressor that returns a fixed number of fragments.""" def __init__(self, n_fragments: int = 2) -> None: self._n = n_fragments def compress( self, fragments: tuple[ContextFragment, ...], skeleton_budget: int, ) -> tuple[ContextFragment, ...]: return tuple(fragments[: self._n]) # --------------------------------------------------------------------------- # Given steps # --------------------------------------------------------------------------- @given("the depth/breadth projection modules are available") def step_modules_available(context: Context) -> None: pass @given("a UKO graph with nodes A B C and edges A->B B->C") def step_graph_abc(context: Context) -> None: context.adjacency = { "A": ["B"], "B": ["C"], } @given("a UKO graph with nodes A B and edges A->B") def step_graph_ab(context: Context) -> None: context.adjacency = { "A": ["B"], } @given("a UKO graph with nodes A and no edges") def step_graph_a_only(context: Context) -> None: adj: dict[str, list[str]] = {} context.adjacency = adj @given("a UKO graph with nodes A B C D and edges A->B C->D") def step_graph_abcd(context: Context) -> None: context.adjacency = { "A": ["B"], "C": ["D"], } @given('a code DetailLevelMap is registered for domain "{domain}"') def step_register_code_map(context: Context, domain: str) -> None: projector = DepthBreadthProjector() projector.register_detail_map(domain, code_detail_map()) context.projector = projector @given("a parent assembled context with {n:d} fragments at detail_depth {depth:d}") def step_parent_context(context: Context, n: int, depth: int) -> None: frags = [ _make_frag( uko_node=f"module://mod_{i}", content=f"module_{i}", token_count=100, detail_depth=depth, ) for i in range(n) ] context.parent_context = _FakeAssembledContext(frags, context_hash="parenthash123") @given("a parent assembled context with the following inheritance fragments:") def step_parent_context_table(context: Context) -> None: frags: list[ContextFragment] = [] for row in context.table: metadata: dict[str, str] = {} if row["domain"].strip(): metadata["detail_level_domain"] = row["domain"] relevance_score = float(row["score"]) if "score" in row.headings else 0.8 frags.append( ContextFragment( uko_node=row["uko_node"], content=row["content"], token_count=int(row["tokens"]), detail_depth=int(row["depth"]), relevance_score=relevance_score, provenance=FragmentProvenance(resource_uri=row["resource_uri"]), metadata=metadata, ) ) context.parent_context = _FakeAssembledContext(frags, context_hash="parenthash123") @given("a PlanContextInheritance service with default config") def step_inheritance_default(context: Context) -> None: context.inheritance_service = PlanContextInheritance() @given( "a PlanContextInheritance service with default config and min_skeleton_tokens {min_tok:d}" ) def step_inheritance_default_min_tokens(context: Context, min_tok: int) -> None: context.inheritance_service = PlanContextInheritance( config=InheritanceConfig(min_skeleton_tokens=min_tok) ) @given("a mock skeleton compressor that returns {n:d} fragments") def step_mock_compressor(context: Context, n: int) -> None: context.mock_compressor = _MockSkeletonCompressor(n_fragments=n) @given("a PlanContextInheritance service with the mock compressor") def step_inheritance_with_compressor(context: Context) -> None: context.inheritance_service = PlanContextInheritance( skeleton_compressor=context.mock_compressor, ) @given( "a PlanContextInheritance service with the mock compressor and min_skeleton_tokens {min_tok:d}" ) def step_inheritance_with_min_tokens(context: Context, min_tok: int) -> None: config = InheritanceConfig(min_skeleton_tokens=min_tok) context.inheritance_service = PlanContextInheritance( config=config, skeleton_compressor=context.mock_compressor, ) # --------------------------------------------------------------------------- # When steps — ProjectionSpec # --------------------------------------------------------------------------- @when( 'I create a ProjectionSpec with focus "{focus}" and breadth {breadth:d} and depth {depth:d}' ) def step_create_spec(context: Context, focus: str, breadth: int, depth: int) -> None: context.proj_spec = ProjectionSpec( focus=(focus,), breadth=breadth, depth=depth, ) @when("I create a ProjectionSpec with empty focus") def step_create_spec_empty_focus(context: Context) -> None: context.proj_error = None try: context.proj_spec = ProjectionSpec(focus=(), breadth=2, depth=3) except ValidationError as exc: context.proj_error = exc @when("I create a ProjectionSpec with negative depth") def step_create_spec_negative_depth(context: Context) -> None: context.proj_error = None try: context.proj_spec = ProjectionSpec(focus=("A",), breadth=2, depth=-1) except (ValueError, ValidationError) as exc: context.proj_error = exc @when('I create a ProjectionSpec with focus "{focus}" and named depth "{depth}"') def step_create_spec_named(context: Context, focus: str, depth: str) -> None: context.proj_spec = ProjectionSpec( focus=(focus,), depth=depth, ) # --------------------------------------------------------------------------- # When steps — ProjectedNode # --------------------------------------------------------------------------- @when( 'I create a ProjectedNode with uri "{uri}" and distance {dist:d} and resolved_depth {depth:d}' ) def step_create_projected_node( context: Context, uri: str, dist: int, depth: int ) -> None: context.proj_node = ProjectedNode(uri=uri, distance=dist, resolved_depth=depth) # --------------------------------------------------------------------------- # When steps — DepthBreadthProjector # --------------------------------------------------------------------------- @when('I project with focus "{focus}" and breadth {breadth:d} and depth {depth:d}') def step_project(context: Context, focus: str, breadth: int, depth: int) -> None: projector = getattr(context, "projector", None) or DepthBreadthProjector() spec = ProjectionSpec(focus=(focus,), breadth=breadth, depth=depth) context.proj_result = projector.project(spec, context.adjacency) @when( 'I project with focus "{focus}" and breadth {breadth:d} and depth {depth:d} and gradient {gradient}' ) def step_project_gradient( context: Context, focus: str, breadth: int, depth: int, gradient: str ) -> None: grad = gradient.strip().lower() == "true" projector = DepthBreadthProjector() spec = ProjectionSpec( focus=(focus,), breadth=breadth, depth=depth, depth_gradient=grad ) context.proj_result = projector.project(spec, context.adjacency) @when( 'I project with focus "{focus}" and breadth {breadth:d} and named depth "{depth}" in domain "{domain}"' ) def step_project_named( context: Context, focus: str, breadth: int, depth: str, domain: str ) -> None: projector = getattr(context, "projector", None) or DepthBreadthProjector() spec = ProjectionSpec(focus=(focus,), breadth=breadth, depth=depth, domain=domain) context.proj_error = None try: context.proj_result = projector.project(spec, context.adjacency) except ValueError as exc: context.proj_error = exc @when( 'I project with multi-focus "{f1}" and "{f2}" and breadth {breadth:d} and depth {depth:d}' ) def step_project_multi_focus( context: Context, f1: str, f2: str, breadth: int, depth: int ) -> None: projector = DepthBreadthProjector() spec = ProjectionSpec(focus=(f1, f2), breadth=breadth, depth=depth) context.proj_result = projector.project(spec, context.adjacency) @when( 'I project to fragments with focus "{focus}" and breadth {breadth:d} and depth {depth:d}' ) def step_project_fragments( context: Context, focus: str, breadth: int, depth: int ) -> None: projector = DepthBreadthProjector() spec = ProjectionSpec(focus=(focus,), breadth=breadth, depth=depth) context.proj_fragments = projector.project_to_fragments(spec, context.adjacency) # --------------------------------------------------------------------------- # When steps — DetailLevelMap presets # --------------------------------------------------------------------------- @when("I create the code detail level map preset") def step_code_map(context: Context) -> None: context.code_map = code_detail_map() @when("I create the docs detail level map preset") def step_docs_map(context: Context) -> None: context.docs_map = docs_detail_map() @when("I create the database detail level map preset") def step_db_map(context: Context) -> None: context.db_map = database_detail_map() # --------------------------------------------------------------------------- # When steps — PlanContextInheritance # --------------------------------------------------------------------------- @when('I compute child context with focus "{focus}" and budget {budget:d}') def step_compute_child(context: Context, focus: str, budget: int) -> None: context.child_error = None try: context.child_result = context.inheritance_service.compute_child_context( parent_context=context.parent_context, child_focus=[focus], child_token_budget=budget, ) except ValueError as exc: context.child_error = exc @when( 'I compute child context with focus "{focus}" and budget {budget:d} at depth_in_tree {pd:d} to {cd:d}' ) def step_compute_child_depths( context: Context, focus: str, budget: int, pd: int, cd: int ) -> None: context.child_result = context.inheritance_service.compute_child_context( parent_context=context.parent_context, child_focus=[focus], child_token_budget=budget, parent_depth_in_tree=pd, child_depth_in_tree=cd, ) @when("I compute child context with parent_depth {pd:d} and child_depth {cd:d}") def step_compute_child_invalid_depth(context: Context, pd: int, cd: int) -> None: context.child_error = None try: context.child_result = context.inheritance_service.compute_child_context( parent_context=context.parent_context, child_focus=["class://X"], child_token_budget=1000, parent_depth_in_tree=pd, child_depth_in_tree=cd, ) except ValueError as exc: context.child_error = exc @when("I create an InheritanceConfig with defaults") def step_default_config(context: Context) -> None: context.inheritance_config = InheritanceConfig() @when('I extract child focus from decisions "{d1}" and "{d2}"') def step_extract_focus(context: Context, d1: str, d2: str) -> None: context.child_focus = PlanContextInheritance.extract_child_focus([d1, d2]) @when('I extract child focus from empty decisions with fallback "{fallback}"') def step_extract_focus_fallback(context: Context, fallback: str) -> None: context.child_focus = PlanContextInheritance.extract_child_focus( [], fallback_focus=[fallback] ) @when("I create a ChildContextResult with a request and skeleton data") def step_create_child_result(context: Context) -> None: req = ContextRequest(focus=["class://Test"], breadth=1, depth=5) frag = _make_frag(uko_node="test://skel", content="skeleton", token_count=50) context.child_ctx_result = ChildContextResult( request=req, skeleton_fragments=(frag,), parent_context_hash="hash123", skeleton_budget=200, depth_delta=1, ) # --------------------------------------------------------------------------- # Then steps — ProjectionSpec # --------------------------------------------------------------------------- @then('the projection spec focus should contain "{uri}"') def step_spec_focus(context: Context, uri: str) -> None: assert uri in context.proj_spec.focus, ( f"Expected {uri!r} in focus {context.proj_spec.focus}" ) @then("the projection spec breadth should be {expected:d}") def step_spec_breadth(context: Context, expected: int) -> None: assert context.proj_spec.breadth == expected @then("the projection spec depth should be {expected:d}") def step_spec_depth_int(context: Context, expected: int) -> None: assert context.proj_spec.depth == expected @then('the projection spec depth should be "{expected}"') def step_spec_depth_str(context: Context, expected: str) -> None: assert context.proj_spec.depth == expected @then("the projection spec depth_gradient should be True") def step_spec_gradient_true(context: Context) -> None: assert context.proj_spec.depth_gradient is True @then('the projection spec domain should be "{expected}"') def step_spec_domain(context: Context, expected: str) -> None: assert context.proj_spec.domain == expected @then("the projection spec should be immutable") def step_spec_immutable(context: Context) -> None: try: context.proj_spec.breadth = 99 # type: ignore[misc] assert False, "Should have raised" # noqa: B011 except (ValidationError, TypeError, AttributeError): pass @then("a projection ValidationError should be raised") def step_proj_validation_error(context: Context) -> None: assert context.proj_error is not None, "Expected ValidationError but none raised" assert isinstance(context.proj_error, ValidationError) @then('a projection ValueError should be raised mentioning "{keyword}"') def step_proj_value_error(context: Context, keyword: str) -> None: error = getattr(context, "proj_error", None) or getattr( context, "child_error", None ) assert error is not None, f"Expected ValueError mentioning {keyword!r}" assert keyword.lower() in str(error).lower(), f"Expected '{keyword}' in: {error}" # --------------------------------------------------------------------------- # Then steps — ProjectedNode # --------------------------------------------------------------------------- @then('the projected node uri should be "{expected}"') def step_node_uri(context: Context, expected: str) -> None: assert context.proj_node.uri == expected @then("the projected node distance should be {expected:d}") def step_node_distance(context: Context, expected: int) -> None: assert context.proj_node.distance == expected @then("the projected node resolved_depth should be {expected:d}") def step_node_resolved_depth(context: Context, expected: int) -> None: assert context.proj_node.resolved_depth == expected @then("the projected node should be immutable") def step_node_immutable(context: Context) -> None: try: context.proj_node.distance = 99 # type: ignore[misc] assert False, "Should have raised" # noqa: B011 except (ValidationError, TypeError, AttributeError): pass # --------------------------------------------------------------------------- # Then steps — DepthBreadthProjector # --------------------------------------------------------------------------- @then("the projection should contain exactly {count:d} node") def step_proj_count_singular(context: Context, count: int) -> None: assert len(context.proj_result) == count, ( f"Expected {count} nodes, got {len(context.proj_result)}" ) @then("the projection should contain exactly {count:d} nodes") def step_proj_count(context: Context, count: int) -> None: assert len(context.proj_result) == count, ( f"Expected {count} nodes, got {len(context.proj_result)}" ) @then('the projected nodes should include "{uri}" at distance {dist:d}') def step_proj_node_at_distance(context: Context, uri: str, dist: int) -> None: found = [n for n in context.proj_result if n.uri == uri] assert found, f"Node {uri!r} not found in projection" assert found[0].distance == dist, ( f"Expected {uri!r} at distance {dist}, got {found[0].distance}" ) @then('the projected node "{uri}" should have resolved_depth {depth:d}') def step_proj_node_depth(context: Context, uri: str, depth: int) -> None: found = [n for n in context.proj_result if n.uri == uri] assert found, f"Node {uri!r} not found" assert found[0].resolved_depth == depth, ( f"Expected {uri!r} depth={depth}, got {found[0].resolved_depth}" ) @then('the projected node "{uri}" should have resolved_depth greater than {depth:d}') def step_proj_node_depth_gt(context: Context, uri: str, depth: int) -> None: found = [n for n in context.proj_result if n.uri == uri] assert found, f"Node {uri!r} not found" assert found[0].resolved_depth > depth, ( f"Expected {uri!r} depth > {depth}, got {found[0].resolved_depth}" ) @then('the projected node "{uri}" should have resolved_depth less than {depth:d}') def step_proj_node_depth_lt(context: Context, uri: str, depth: int) -> None: found = [n for n in context.proj_result if n.uri == uri] assert found, f"Node {uri!r} not found" assert found[0].resolved_depth < depth, ( f"Expected {uri!r} depth < {depth}, got {found[0].resolved_depth}" ) @then("the result should contain {count:d} ContextFragments") def step_fragment_count(context: Context, count: int) -> None: assert len(context.proj_fragments) == count @then('the first fragment uko_node should be "{expected}"') def step_first_frag_uko(context: Context, expected: str) -> None: assert context.proj_fragments[0].uko_node == expected @then('all fragments should have metadata key "{key}" set to True') def step_fragments_metadata(context: Context, key: str) -> None: for frag in context.proj_fragments: assert frag.metadata.get(key) is True, ( f"Fragment {frag.uko_node} missing metadata key {key!r}" ) # --------------------------------------------------------------------------- # Then steps — DetailLevelMap presets # --------------------------------------------------------------------------- @then('the code map should resolve "{level}" to {expected:d}') def step_code_resolve(context: Context, level: str, expected: int) -> None: assert context.code_map.resolve(level) == expected @then('the docs map should resolve "{level}" to {expected:d}') def step_docs_resolve(context: Context, level: str, expected: int) -> None: assert context.docs_map.resolve(level) == expected @then('the database map should resolve "{level}" to {expected:d}') def step_db_resolve(context: Context, level: str, expected: int) -> None: assert context.db_map.resolve(level) == expected # --------------------------------------------------------------------------- # Then steps — PlanContextInheritance # --------------------------------------------------------------------------- @then("the child result should contain a ContextRequest") def step_child_has_request(context: Context) -> None: assert hasattr(context.child_result, "request"), "No request in result" assert isinstance(context.child_result.request, ContextRequest) @then('the child request focus should contain "{uri}"') def step_child_focus_contains(context: Context, uri: str) -> None: assert uri in context.child_result.request.focus @then("the child request depth_gradient should be True") def step_child_gradient(context: Context) -> None: assert context.child_result.request.depth_gradient is True @then("the child result depth_delta should be {expected:d}") def step_child_depth_delta(context: Context, expected: int) -> None: assert context.child_result.depth_delta == expected @then("the child request depth should be greater than {depth:d}") def step_child_depth_gt(context: Context, depth: int) -> None: assert context.child_result.request.depth > depth, ( f"Expected depth > {depth}, got {context.child_result.request.depth}" ) @then("the child request breadth should be at most {max_breadth:d}") def step_child_breadth_max(context: Context, max_breadth: int) -> None: assert context.child_result.request.breadth <= max_breadth, ( f"Expected breadth <= {max_breadth}, got {context.child_result.request.breadth}" ) @then("the child result skeleton_fragments should have {count:d} entries") def step_child_skeleton_count(context: Context, count: int) -> None: assert len(context.child_result.skeleton_fragments) == count, ( f"Expected {count} skeleton fragments, got {len(context.child_result.skeleton_fragments)}" ) @then("all child skeleton fragments should have detail depth at most {max_depth:d}") def step_child_skeleton_depth_max(context: Context, max_depth: int) -> None: depths = [ fragment.detail_depth for fragment in context.child_result.skeleton_fragments ] assert all(depth <= max_depth for depth in depths), ( f"Expected child skeleton fragment depths <= {max_depth}, got {depths}" ) @then('a child skeleton fragment should contain "{snippet}"') def step_child_skeleton_contains(context: Context, snippet: str) -> None: contents = [ fragment.content for fragment in context.child_result.skeleton_fragments ] assert any(snippet in content for content in contents), ( f"Expected child skeleton fragment to contain {snippet!r}, got {contents!r}" ) @then('the first child skeleton fragment should contain "{snippet}"') def step_first_child_skeleton_contains(context: Context, snippet: str) -> None: first = context.child_result.skeleton_fragments[0].content assert snippet in first, ( f"Expected first child skeleton fragment to contain {snippet!r}, got {first!r}" ) @then("the inheritance config skeleton_ratio should be {expected:g}") def step_config_ratio(context: Context, expected: float) -> None: assert context.inheritance_config.skeleton_ratio == expected @then('the child focus should contain "{uri}"') def step_focus_contains_single(context: Context, uri: str) -> None: assert uri in context.child_focus @then('the child focus should include both "{uri1}" and "{uri2}"') def step_focus_contains_both(context: Context, uri1: str, uri2: str) -> None: assert uri1 in context.child_focus assert uri2 in context.child_focus # --------------------------------------------------------------------------- # Then steps — ChildContextResult # --------------------------------------------------------------------------- @then("the child context result should be immutable") def step_result_immutable(context: Context) -> None: try: context.child_ctx_result.depth_delta = 99 # type: ignore[misc] assert False, "Should have raised" # noqa: B011 except (ValidationError, TypeError, AttributeError): pass @then("the child context result should contain the request") def step_result_has_request(context: Context) -> None: assert isinstance(context.child_ctx_result.request, ContextRequest)