@phase2 @acms @acms_pipeline_phase2 Feature: ACMS Pipeline Phase 2 — Fragment Fusion Components As a CleverAgents developer I want production-grade Phase 2 pipeline components So that context assembly deduplicates, resolves depths, scores, and packs fragments # =========================================================================== # ScoredFragment — Domain Model # =========================================================================== @scored_fragment Scenario: Create a ScoredFragment with component breakdown Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10 for phase2 When I create a ScoredFragment with composite_score 0.85 and components: | component | value | | relevance | 0.9 | | hierarchy | 0.8 | | quality | 0.7 | | recency | 1.0 | Then the ScoredFragment composite_score should be 0.85 And the ScoredFragment should have component "relevance" with value 0.9 And the ScoredFragment should have component "hierarchy" with value 0.8 And the ScoredFragment should have component "quality" with value 0.7 And the ScoredFragment should have component "recency" with value 1.0 And the ScoredFragment should reference the original fragment @scored_fragment @validation Scenario: ScoredFragment rejects composite score above 1.0 Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10 for phase2 When I create a ScoredFragment with invalid composite_score 1.5 Then a phase2 validation error should be raised @scored_fragment @validation Scenario: ScoredFragment rejects negative composite score Given a context fragment with uko_node "project://app/main.py" and content "hello" and token_count 10 for phase2 When I create a ScoredFragment with invalid composite_score -0.1 Then a phase2 validation error should be raised # =========================================================================== # ContentHashDeduplicator # =========================================================================== @deduplicator Scenario: Deduplicate fragments with identical content for same UKO node Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | hello world | 0.5 | 10 | 3 | | project://app/main.py | hello world | 0.9 | 10 | 3 | When I deduplicate the fragments Then 1 fragment should remain after deduplication And the surviving fragment should have relevance score 0.9 @deduplicator Scenario: Deduplicate keeps distinct content for same UKO node Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | hello world | 0.5 | 10 | 3 | | project://app/main.py | goodbye world | 0.9 | 12 | 3 | When I deduplicate the fragments Then 2 fragments should remain after deduplication @deduplicator Scenario: Deduplicate keeps fragments from different UKO nodes Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | hello world | 0.5 | 10 | 3 | | project://app/io.py | hello world | 0.9 | 10 | 3 | When I deduplicate the fragments Then 2 fragments should remain after deduplication @deduplicator Scenario: Deduplicate with empty fragment list Given an empty phase2 fragment list When I deduplicate the fragments Then 0 fragments should remain after deduplication @deduplicator Scenario: Deduplicate three copies retains highest scored Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | abc | 0.3 | 5 | 1 | | project://app/main.py | abc | 0.7 | 5 | 1 | | project://app/main.py | abc | 0.5 | 5 | 1 | When I deduplicate the fragments Then 1 fragment should remain after deduplication And the surviving fragment should have relevance score 0.7 # =========================================================================== # MaxDepthResolver # =========================================================================== @depth_resolver Scenario: Resolve depth conflicts to maximum depth per node Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | brief summary | 0.8 | 10 | 2 | | project://app/main.py | full source | 0.8 | 100 | 9 | When I resolve depth conflicts Then 1 fragment should remain after depth resolution And the surviving fragment should have detail depth 9 @depth_resolver Scenario: Resolve preserves fragments for distinct nodes Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | alpha | 0.8 | 10 | 2 | | project://app/io.py | beta | 0.7 | 20 | 5 | When I resolve depth conflicts Then 2 fragments should remain after depth resolution @depth_resolver Scenario: Resolve same depth keeps higher relevance Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | alpha | 0.3 | 10 | 5 | | project://app/main.py | beta | 0.9 | 20 | 5 | When I resolve depth conflicts Then 1 fragment should remain after depth resolution And the surviving fragment should have relevance score 0.9 @depth_resolver Scenario: Resolve with empty fragment list Given an empty phase2 fragment list When I resolve depth conflicts Then 0 fragments should remain after depth resolution @depth_resolver Scenario: Resolve preserves original fragment order Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/a.py | aaa | 0.5 | 10 | 3 | | project://app/b.py | bbb | 0.6 | 10 | 3 | | project://app/c.py | ccc | 0.7 | 10 | 3 | When I resolve depth conflicts Then 3 fragments should remain after depth resolution And the resolved fragment order should be "aaa", "bbb", "ccc" # =========================================================================== # WeightedCompositeScorer # =========================================================================== @scorer Scenario: Score fragments with default weights Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | hello | 0.8 | 10 | 3 | When I score the fragments with default weights Then the scored fragment should have a composite relevance score And the scored fragment metadata should contain original relevance "0.8" @scorer Scenario: Score with custom weights Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | hello | 1.0 | 10 | 3 | And scorer weights: relevance=1.0, hierarchy=0.0, quality=0.0, recency=0.0 When I score the fragments with custom weights Then the scored fragment relevance score should be 1.0 @scorer Scenario: Score with metadata-provided hierarchy weight Given a phase2 fragment with metadata: | uko_node | content | score | tokens | depth | hierarchy_weight | strategy_quality | recency_bonus | | project://app/main.py | hello | 0.8 | 10 | 3 | 0.9 | 0.7 | 1.0 | When I score the fragments with default weights Then the scored fragment metadata should contain component "hierarchy" near 0.9 @scorer Scenario: Score empty fragment list Given an empty phase2 fragment list When I score the fragments with default weights Then 0 scored fragments should be returned @scorer Scenario: Detailed scoring returns ScoredFragment objects Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | hello | 0.8 | 10 | 3 | | project://app/io.py | world | 0.6 | 15 | 5 | When I score the fragments using score_detailed Then 2 ScoredFragment objects should be returned And each ScoredFragment should have a composite_score between 0.0 and 1.0 And each ScoredFragment should have score_components @scorer Scenario: Score composite is clamped to 0.0-1.0 Given a phase2 fragment with metadata: | uko_node | content | score | tokens | depth | hierarchy_weight | strategy_quality | recency_bonus | | project://app/main.py | hello | 1.0 | 10 | 3 | 1.0 | 1.0 | 1.0 | When I score the fragments with default weights Then the scored fragment relevance score should be at most 1.0 # =========================================================================== # GreedyKnapsackPacker # =========================================================================== @packer Scenario: Pack fragments within budget Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | alpha | 0.9 | 100 | 5 | | project://app/io.py | beta | 0.7 | 100 | 3 | | project://app/util.py | gamma | 0.5 | 100 | 2 | And a phase2 budget with max_tokens 250 and reserved_tokens 0 When I pack the fragments into budget Then 2 fragments should be packed And the packed total tokens should be at most 250 @packer Scenario: Pack all fragments when budget is sufficient Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | alpha | 0.9 | 50 | 5 | | project://app/io.py | beta | 0.7 | 50 | 3 | And a phase2 budget with max_tokens 200 and reserved_tokens 0 When I pack the fragments into budget Then 2 fragments should be packed @packer Scenario: Pack empty fragment list Given an empty phase2 fragment list And a phase2 budget with max_tokens 1000 and reserved_tokens 0 When I pack the fragments into budget Then 0 fragments should be packed @packer Scenario: Pack excludes fragments below minimum token threshold Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | a | 0.9 | 5 | 3 | | project://app/io.py | beta | 0.7 | 50 | 3 | And a phase2 budget with max_tokens 1000 and reserved_tokens 0 When I pack the fragments into budget Then 1 fragment should be packed And the packed fragment uko_node should be "project://app/io.py" @packer Scenario: Pack with depth fallback uses lower-depth alternative Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | full source code | 0.9 | 500 | 9 | | project://app/main.py | brief summary | 0.5 | 50 | 2 | | project://app/io.py | io module | 0.7 | 100 | 5 | And a phase2 budget with max_tokens 200 and reserved_tokens 0 When I pack the fragments into budget Then the packed fragments should include uko_node "project://app/main.py" And the packed fragment for "project://app/main.py" should have depth 2 @packer Scenario: Pack respects reserved tokens Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | alpha | 0.9 | 100 | 5 | | project://app/io.py | beta | 0.7 | 100 | 3 | And a phase2 budget with max_tokens 250 and reserved_tokens 100 When I pack the fragments into budget Then 1 fragment should be packed And the packed total tokens should be at most 150 @packer Scenario: Pack prefers higher scored fragments Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | alpha | 0.3 | 100 | 5 | | project://app/io.py | beta | 0.9 | 100 | 3 | And a phase2 budget with max_tokens 150 and reserved_tokens 0 When I pack the fragments into budget Then 1 fragment should be packed And the packed fragment uko_node should be "project://app/io.py" @packer Scenario: Single fragment over entire budget is excluded Given the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | huge content | 0.9 | 10000 | 9 | And a phase2 budget with max_tokens 100 and reserved_tokens 0 When I pack the fragments into budget Then 0 fragments should be packed # =========================================================================== # Pipeline Integration — DI injection of Phase 2 components # =========================================================================== @pipeline_integration Scenario: Inject ContentHashDeduplicator into pipeline Given the ACMS pipeline with a ContentHashDeduplicator And the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | hello world | 0.5 | 10 | 3 | | project://app/main.py | hello world | 0.9 | 10 | 3 | When I assemble context through the pipeline Then the pipeline output should contain 1 fragment @pipeline_integration Scenario: Inject MaxDepthResolver into pipeline Given the ACMS pipeline with a MaxDepthResolver And the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | brief | 0.8 | 10 | 2 | | project://app/main.py | detailed | 0.8 | 50 | 9 | When I assemble context through the pipeline Then the pipeline output should contain 1 fragment @pipeline_integration Scenario: Inject WeightedCompositeScorer into pipeline Given the ACMS pipeline with a WeightedCompositeScorer And the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | hello | 0.8 | 10 | 3 | When I assemble context through the pipeline Then the pipeline output fragments should have updated relevance scores @pipeline_integration Scenario: Inject GreedyKnapsackPacker into pipeline Given the ACMS pipeline with a GreedyKnapsackPacker And the following phase2 fragments: | uko_node | content | score | tokens | depth | | project://app/main.py | alpha | 0.9 | 100 | 5 | | project://app/io.py | beta | 0.7 | 100 | 3 | | project://app/util.py | gamma | 0.5 | 100 | 2 | And a phase2 budget with max_tokens 250 and reserved_tokens 0 When I assemble context through the pipeline with budget Then the pipeline output total tokens should be at most 250