diff --git a/potpie/context-engine/application/readers/_common.py b/potpie/context-engine/application/readers/_common.py index 8eb0e9855..d1e512cfc 100644 --- a/potpie/context-engine/application/readers/_common.py +++ b/potpie/context-engine/application/readers/_common.py @@ -121,6 +121,20 @@ def claim_corroboration(row: ClaimRow) -> int: return 1 +def claim_semantic_similarity(row: ClaimRow) -> float | None: + """Backend-stamped query similarity, when the read carried a ``fact_query``. + + Returns ``None`` when the backend did not stamp a score so the ranker + falls back to its neutral default instead of a fabricated value. + """ + sim = row.properties.get("semantic_similarity") + if isinstance(sim, bool): + return None + if isinstance(sim, (int, float)): + return float(sim) + return None + + def claim_environment(row: ClaimRow) -> str | None: env = row.environment if isinstance(env, str) and env.strip(): @@ -209,6 +223,7 @@ def row_in_anchor_set(row: ClaimRow, anchor_keys: Iterable[str]) -> bool: "claim_corroboration", "claim_environment", "claim_payload", + "claim_semantic_similarity", "coverage_status_from_count", "dedupe_claim_rows", "make_task_context", diff --git a/potpie/context-engine/application/readers/coding_preferences.py b/potpie/context-engine/application/readers/coding_preferences.py index 9e8eaf22f..d3ab6aecc 100644 --- a/potpie/context-engine/application/readers/coding_preferences.py +++ b/potpie/context-engine/application/readers/coding_preferences.py @@ -23,6 +23,7 @@ claim_candidate_key, claim_corroboration, claim_payload, + claim_semantic_similarity, coverage_status_from_count, dedupe_claim_rows, rank_candidates, @@ -64,7 +65,6 @@ def read(self, req: ReadRequest) -> ReadResponse: # Hard zero on overlap: skip — readers should not surface # rules that demonstrably don't apply. continue - sim = row.properties.get("semantic_similarity") candidates.append( Candidate( candidate_key=claim_candidate_key(row), @@ -73,9 +73,7 @@ def read(self, req: ReadRequest) -> ReadResponse: valid_at=row.valid_at, corroboration_count=claim_corroboration(row), scope_overlap=overlap if scope_keys else None, - semantic_similarity=float(sim) - if isinstance(sim, (int, float)) - else None, + semantic_similarity=claim_semantic_similarity(row), ) ) diff --git a/potpie/context-engine/application/readers/decisions.py b/potpie/context-engine/application/readers/decisions.py index b7d4bd712..3fd4b56a7 100644 --- a/potpie/context-engine/application/readers/decisions.py +++ b/potpie/context-engine/application/readers/decisions.py @@ -16,6 +16,7 @@ claim_candidate_key, claim_corroboration, claim_payload, + claim_semantic_similarity, coverage_status_from_count, dedupe_claim_rows, rank_candidates, @@ -48,7 +49,6 @@ def read(self, req: ReadRequest) -> ReadResponse: overlap = _scope_overlap(row, anchor_keys=anchor_keys) if anchor_keys and overlap == 0.0: continue - sim = row.properties.get("semantic_similarity") candidates.append( Candidate( candidate_key=claim_candidate_key(row), @@ -57,9 +57,7 @@ def read(self, req: ReadRequest) -> ReadResponse: valid_at=row.valid_at, corroboration_count=claim_corroboration(row), scope_overlap=overlap if anchor_keys else None, - semantic_similarity=float(sim) - if isinstance(sim, (int, float)) - else None, + semantic_similarity=claim_semantic_similarity(row), ) ) diff --git a/potpie/context-engine/application/readers/docs.py b/potpie/context-engine/application/readers/docs.py index 4fef8ce11..afd384505 100644 --- a/potpie/context-engine/application/readers/docs.py +++ b/potpie/context-engine/application/readers/docs.py @@ -16,6 +16,7 @@ claim_candidate_key, claim_corroboration, claim_payload, + claim_semantic_similarity, coverage_status_from_count, dedupe_claim_rows, rank_candidates, @@ -45,7 +46,6 @@ def read(self, req: ReadRequest) -> ReadResponse: overlap = _scope_overlap(row, anchor_keys=anchor_keys) if anchor_keys and overlap == 0.0: continue - sim = row.properties.get("semantic_similarity") candidates.append( Candidate( candidate_key=claim_candidate_key(row), @@ -54,9 +54,7 @@ def read(self, req: ReadRequest) -> ReadResponse: valid_at=row.valid_at, corroboration_count=claim_corroboration(row), scope_overlap=overlap if anchor_keys else None, - semantic_similarity=float(sim) - if isinstance(sim, (int, float)) - else None, + semantic_similarity=claim_semantic_similarity(row), ) ) diff --git a/potpie/context-engine/application/readers/features.py b/potpie/context-engine/application/readers/features.py index 267131357..eb4406cd8 100644 --- a/potpie/context-engine/application/readers/features.py +++ b/potpie/context-engine/application/readers/features.py @@ -16,6 +16,7 @@ claim_candidate_key, claim_corroboration, claim_payload, + claim_semantic_similarity, coverage_status_from_count, dedupe_claim_rows, rank_candidates, @@ -48,7 +49,6 @@ def read(self, req: ReadRequest) -> ReadResponse: overlap = _scope_overlap(row, anchor_keys=anchor_keys) if anchor_keys and overlap == 0.0: continue - sim = row.properties.get("semantic_similarity") candidates.append( Candidate( candidate_key=claim_candidate_key(row), @@ -57,9 +57,7 @@ def read(self, req: ReadRequest) -> ReadResponse: valid_at=row.valid_at, corroboration_count=claim_corroboration(row), scope_overlap=overlap if anchor_keys else None, - semantic_similarity=float(sim) - if isinstance(sim, (int, float)) - else None, + semantic_similarity=claim_semantic_similarity(row), ) ) diff --git a/potpie/context-engine/application/readers/infra_topology.py b/potpie/context-engine/application/readers/infra_topology.py index a240dc941..8a08f0bcd 100644 --- a/potpie/context-engine/application/readers/infra_topology.py +++ b/potpie/context-engine/application/readers/infra_topology.py @@ -11,10 +11,16 @@ When a harness records ``Service DEPLOYED_TO Environment`` with the environment stamped on the edge, the question "what env runs auth-svc?" returns a real edge instead of 0% coverage. + +When the read carries a free-text ``query``, discovered rows are scored +against it (``semantic_similarity``) so ranking is query-sensitive; the +traversal itself stays query-free so the same topology is discovered +either way. """ from __future__ import annotations +import dataclasses from dataclasses import dataclass from typing import Any, Iterable, Mapping @@ -25,6 +31,7 @@ claim_corroboration, claim_environment, claim_payload, + claim_semantic_similarity, coverage_status_from_count, dedupe_claim_rows, rank_candidates, @@ -92,6 +99,7 @@ def read(self, req: ReadRequest) -> ReadResponse: valid_at=row.valid_at, scope_overlap=overlap, corroboration_count=claim_corroboration(row), + semantic_similarity=claim_semantic_similarity(row), ) ) @@ -135,6 +143,7 @@ def _traverse( include_invalidated=req.include_invalidated, as_of=req.as_of, source_ref_in=req.source_refs, + fact_query=req.query, limit=max(req.max_items * 4, 16), ) ) @@ -219,7 +228,55 @@ def _traverse( next_frontier.add(row.subject_key) frontier = next_frontier - visited_anchors - return list(seen_rows.values()) + return self._stamp_query_similarity(req, list(seen_rows.values())) + + def _stamp_query_similarity( + self, req: ReadRequest, rows: list[ClaimRow] + ) -> list[ClaimRow]: + """Stamp ``semantic_similarity`` onto already-traversed rows. + + The traversal itself stays query-free: on vector backends a + ``fact_query`` turns ``find_claims`` into a top-k similarity search, + which would silently prune frontier edges and change *which* topology + is discovered. Scoring the discovered rows afterwards keeps discovery + query-insensitive while making ranking query-sensitive. + """ + if not req.query or not rows: + return rows + claim_keys = tuple(sorted({row.claim_key for row in rows if row.claim_key})) + if not claim_keys: + return rows + scored = self.claim_query.find_claims( + ClaimQueryFilter( + pot_id=req.pot_id, + predicate_in=_INFRA_PREDICATES, + claim_key_in=claim_keys, + include_invalidated=req.include_invalidated, + as_of=req.as_of, + source_ref_in=req.source_refs, + fact_query=req.query, + limit=len(claim_keys), + ) + ) + similarity_by_key: dict[str, float] = {} + for row in scored: + sim = claim_semantic_similarity(row) + if row.claim_key and sim is not None: + similarity_by_key[row.claim_key] = sim + + out: list[ClaimRow] = [] + for row in rows: + sim = similarity_by_key.get(row.claim_key) if row.claim_key else None + if sim is None: + # No stamp for this row (e.g. missing claim_key or the vector + # top-k skipped it): leave it unset so the ranker uses its + # neutral default instead of a fabricated score. + out.append(row) + continue + props = dict(row.properties) + props["semantic_similarity"] = sim + out.append(dataclasses.replace(row, properties=props)) + return out # --------------------------------------------------------------------------- diff --git a/potpie/context-engine/application/readers/owners.py b/potpie/context-engine/application/readers/owners.py index 2dc6437d9..6aca1feac 100644 --- a/potpie/context-engine/application/readers/owners.py +++ b/potpie/context-engine/application/readers/owners.py @@ -15,6 +15,7 @@ claim_candidate_key, claim_corroboration, claim_payload, + claim_semantic_similarity, coverage_status_from_count, dedupe_claim_rows, rank_candidates, @@ -47,7 +48,6 @@ def read(self, req: ReadRequest) -> ReadResponse: overlap = _scope_overlap(row, anchor_keys=anchor_keys) if anchor_keys and overlap == 0.0: continue - sim = row.properties.get("semantic_similarity") candidates.append( Candidate( candidate_key=claim_candidate_key(row), @@ -56,9 +56,7 @@ def read(self, req: ReadRequest) -> ReadResponse: valid_at=row.valid_at, corroboration_count=claim_corroboration(row), scope_overlap=overlap if anchor_keys else None, - semantic_similarity=float(sim) - if isinstance(sim, (int, float)) - else None, + semantic_similarity=claim_semantic_similarity(row), ) ) diff --git a/potpie/context-engine/application/readers/prior_bugs.py b/potpie/context-engine/application/readers/prior_bugs.py index 7be774820..a27b7eecf 100644 --- a/potpie/context-engine/application/readers/prior_bugs.py +++ b/potpie/context-engine/application/readers/prior_bugs.py @@ -23,6 +23,7 @@ ReadResponse, claim_candidate_key, claim_payload, + claim_semantic_similarity, coverage_status_from_count, dedupe_claim_rows, rank_candidates, @@ -88,7 +89,6 @@ def read(self, req: ReadRequest) -> ReadResponse: ) if anchor_keys and overlap == 0.0: continue - sim = row.properties.get("semantic_similarity") fix_key = _fix_key_for_row(row) verification_boost = verification_counts.get(fix_key, 0) if fix_key else 0 candidates.append( @@ -99,9 +99,7 @@ def read(self, req: ReadRequest) -> ReadResponse: valid_at=row.valid_at, corroboration_count=1 + verification_boost, scope_overlap=overlap if anchor_keys else None, - semantic_similarity=float(sim) - if isinstance(sim, (int, float)) - else None, + semantic_similarity=claim_semantic_similarity(row), ) ) diff --git a/potpie/context-engine/application/readers/timeline_reader.py b/potpie/context-engine/application/readers/timeline_reader.py index fb8ba88f5..276642b2e 100644 --- a/potpie/context-engine/application/readers/timeline_reader.py +++ b/potpie/context-engine/application/readers/timeline_reader.py @@ -24,6 +24,7 @@ ReadResponse, claim_corroboration, claim_payload, + claim_semantic_similarity, coverage_status_from_count, dedupe_claim_rows, rank_candidates, @@ -67,7 +68,6 @@ def read(self, req: ReadRequest) -> ReadResponse: overlap = ( 1.0 if (not anchor_keys) or row_in_anchor_set(row, anchor_keys) else 0.4 ) - sim = row.properties.get("semantic_similarity") candidates.append( Candidate( candidate_key=_activity_key(row), @@ -76,9 +76,7 @@ def read(self, req: ReadRequest) -> ReadResponse: valid_at=event_time, scope_overlap=overlap, corroboration_count=claim_corroboration(row), - semantic_similarity=float(sim) - if isinstance(sim, (int, float)) - else None, + semantic_similarity=claim_semantic_similarity(row), ) ) @@ -268,8 +266,7 @@ def sort_key(row: ClaimRow) -> tuple[int, int, float, float]: "PERFORMED": 1, "AUTHORED": 1, }.get(predicate, 0) - sim = row.properties.get("semantic_similarity") - similarity = float(sim) if isinstance(sim, (int, float)) else 0.0 + similarity = claim_semantic_similarity(row) or 0.0 event_time = _event_datetime(row) event_ts = event_time.timestamp() if event_time else 0.0 return (direct_anchor, predicate_priority, similarity, event_ts) diff --git a/potpie/context-engine/domain/graph_workbench_ontology.py b/potpie/context-engine/domain/graph_workbench_ontology.py index 3dae28938..e518d5404 100644 --- a/potpie/context-engine/domain/graph_workbench_ontology.py +++ b/potpie/context-engine/domain/graph_workbench_ontology.py @@ -639,6 +639,7 @@ def to_dict(self, *, include_examples: bool = False) -> dict[str, Any]: "direction", "environment", "include_unqualified_environment", + "query", ), "supported_filters": ( "service", @@ -647,6 +648,7 @@ def to_dict(self, *, include_examples: bool = False) -> dict[str, Any]: "direction", "environment", "include_unqualified_environment", + "query", ), "keywords": ( "service", diff --git a/potpie/context-engine/tests/conformance/test_graph_surface_lite_e2e.py b/potpie/context-engine/tests/conformance/test_graph_surface_lite_e2e.py index 1eae069e8..2edc99928 100644 --- a/potpie/context-engine/tests/conformance/test_graph_surface_lite_e2e.py +++ b/potpie/context-engine/tests/conformance/test_graph_surface_lite_e2e.py @@ -75,6 +75,94 @@ def test_read_backed_view_returns_data() -> None: assert env.subgraph_versions["_global"] >= 1 +def test_infra_resolve_ranks_by_task_query_end_to_end() -> None: + """End-to-end: mutate → context_resolve → query-sensitive infra ranking.""" + svc = _service(embedder=True) + svc.mutate( + SemanticMutationRequest.parse( + { + "pot_id": POT, + "operations": [ + { + "op": "link_entities", + "subgraph": "infra_topology", + "subject": {"key": "service:auth-svc", "type": "Service"}, + "predicate": "DEPENDS_ON", + "object": { + "key": "service:redis-sidecar", + "type": "Service", + }, + "truth": "source_observation", + "description": "auth depends on redis sidecar for session connection pooling", + "evidence": [ + { + "source_ref": "manifest:redis", + "authority": "repository_metadata", + } + ], + }, + { + "op": "link_entities", + "subgraph": "infra_topology", + "subject": {"key": "service:auth-svc", "type": "Service"}, + "predicate": "DEPENDS_ON", + "object": { + "key": "service:kafka-broker", + "type": "Service", + }, + "truth": "source_observation", + "description": "auth publishes login events to kafka broker", + "evidence": [ + { + "source_ref": "manifest:kafka", + "authority": "repository_metadata", + } + ], + }, + ], + } + ) + ) + env = svc.resolve( + ResolveRequest( + pot_id=POT, + task="redis connection pool for sessions", + include=("infra_topology",), + scope={"service": "auth-svc"}, + max_items=10, + ) + ) + infra_items = [item for item in env.items if item.include == "infra_topology"] + assert len(infra_items) == 2 + top, second = infra_items[0], infra_items[1] + assert top.payload["object_key"] == "service:redis-sidecar" + assert ( + top.breakdown["semantic_similarity"] > second.breakdown["semantic_similarity"] + ) + assert top.breakdown["semantic_similarity"] != 0.5 + + # Same ranking through the graph-read surface (``graph read --query``): + # the view contract accepts ``query`` and similarity reaches the breakdown. + result = svc.read( + GraphReadRequest( + pot_id=POT, + subgraph="infra_topology", + view="service_neighborhood", + scope={"service": "auth-svc"}, + query="redis connection pool for sessions", + depth=1, + limit=10, + ) + ) + assert result.ok + assert not result.unsupported + sims = { + item["entity_key"]: item["breakdown"]["semantic_similarity"] + for item in result.items + } + assert sims["service:redis-sidecar"] > sims["service:kafka-broker"] + + # 3. search-entities finds entities from a prior mutation def test_search_entities_finds_prior_mutation() -> None: svc = _service() diff --git a/potpie/context-engine/tests/unit/test_p9_readers.py b/potpie/context-engine/tests/unit/test_p9_readers.py index 1c8257498..f02beb9fd 100644 --- a/potpie/context-engine/tests/unit/test_p9_readers.py +++ b/potpie/context-engine/tests/unit/test_p9_readers.py @@ -23,7 +23,11 @@ PriorBugsReader, TimelineReader, ) -from application.readers._common import ReadRequest, dedupe_claim_rows +from application.readers._common import ( + ReadRequest, + claim_semantic_similarity, + dedupe_claim_rows, +) from domain.ports.claim_query import ClaimRow from domain.ranking import RankingService @@ -95,6 +99,16 @@ def test_dedupe_claim_rows_uses_claim_key_then_triple_and_sources() -> None: ] +def test_claim_semantic_similarity_ignores_booleans() -> None: + row = _row( + predicate="DEPENDS_ON", + subject_key="service:a", + object_key="service:b", + properties={"semantic_similarity": True}, + ) + assert claim_semantic_similarity(row) is None + + # --------------------------------------------------------------------------- # CodingPreferencesReader # --------------------------------------------------------------------------- @@ -320,6 +334,88 @@ def test_no_anchor_returns_neutral_overlap(self) -> None: # Unscoped: returns all infra predicates, all neutral overlap assert len(response.items) >= 2 + def _setup_semantic_store(self) -> InMemoryClaimQueryStore: + store = InMemoryClaimQueryStore() + store.add( + _row( + predicate="DEPENDS_ON", + subject_key="service:auth-svc", + object_key="datastore:redis-cache", + fact="auth-svc depends on redis cache for session connection pooling", + evidence_strength="deterministic", + ) + ) + store.add( + _row( + predicate="DEPENDS_ON", + subject_key="service:auth-svc", + object_key="service:kafka-broker", + fact="auth-svc publishes login events to the kafka broker", + evidence_strength="deterministic", + ) + ) + return store + + def test_query_makes_anchored_ranking_semantic(self) -> None: + store = self._setup_semantic_store() + reader = InfraTopologyReader(claim_query=store, ranker=RankingService()) + response = reader.read( + ReadRequest( + pot_id="pot-1", + scope={"services": ["auth-svc"]}, + query="redis connection pool for sessions", + ) + ) + assert len(response.items) == 2 + top, second = response.items + assert top.candidate.payload["object_key"] == "datastore:redis-cache" + assert ( + top.breakdown["semantic_similarity"] + > second.breakdown["semantic_similarity"] + ) + + def test_query_makes_unanchored_ranking_semantic(self) -> None: + store = self._setup_semantic_store() + reader = InfraTopologyReader(claim_query=store, ranker=RankingService()) + response = reader.read( + ReadRequest( + pot_id="pot-1", + scope={}, + query="redis connection pool for sessions", + ) + ) + assert response.items + top = response.items[0] + assert top.candidate.payload["object_key"] == "datastore:redis-cache" + sims = {r.breakdown["semantic_similarity"] for r in response.items} + assert len(sims) > 1 # not flat-scored + + def test_no_query_keeps_neutral_similarity(self) -> None: + store = self._setup_semantic_store() + reader = InfraTopologyReader(claim_query=store, ranker=RankingService()) + response = reader.read( + ReadRequest(pot_id="pot-1", scope={"services": ["auth-svc"]}) + ) + assert response.items + assert all(r.breakdown["semantic_similarity"] == 0.5 for r in response.items) + + def test_query_does_not_change_traversal_discovery(self) -> None: + store = self._setup_semantic_store() + reader = InfraTopologyReader(claim_query=store, ranker=RankingService()) + without_query = reader.read( + ReadRequest(pot_id="pot-1", scope={"services": ["auth-svc"]}) + ) + with_query = reader.read( + ReadRequest( + pot_id="pot-1", + scope={"services": ["auth-svc"]}, + query="redis connection pool for sessions", + ) + ) + assert {r.candidate.candidate_key for r in without_query.items} == { + r.candidate.candidate_key for r in with_query.items + } + # --------------------------------------------------------------------------- # FeaturesReader