From dca703a04a267b7085693a0ca7f2a4a3cb2d9f4a Mon Sep 17 00:00:00 2001 From: Yash Krishan Date: Tue, 7 Jul 2026 13:20:12 +0530 Subject: [PATCH 1/2] Fix infra topology ranking to use query semantic similarity. Wire InfraTopologyReader into the existing fact_query scoring path without changing traversal discovery, allow graph read --query on the infra view, and extract shared claim_semantic_similarity for all readers. Co-authored-by: Cursor --- ...7-07-infra-topology-semantic-similarity.md | 95 ++++++++++++++++++ .../application/readers/_common.py | 15 +++ .../application/readers/coding_preferences.py | 6 +- .../application/readers/decisions.py | 6 +- .../application/readers/docs.py | 6 +- .../application/readers/features.py | 6 +- .../application/readers/infra_topology.py | 59 ++++++++++- .../application/readers/owners.py | 6 +- .../application/readers/prior_bugs.py | 6 +- .../application/readers/timeline_reader.py | 9 +- .../domain/graph_workbench_ontology.py | 2 + .../test_graph_surface_lite_e2e.py | 88 +++++++++++++++++ .../tests/unit/test_p9_readers.py | 98 ++++++++++++++++++- 13 files changed, 370 insertions(+), 32 deletions(-) create mode 100644 docs/history/2026-07-07-infra-topology-semantic-similarity.md diff --git a/docs/history/2026-07-07-infra-topology-semantic-similarity.md b/docs/history/2026-07-07-infra-topology-semantic-similarity.md new file mode 100644 index 000000000..dc76ec5cf --- /dev/null +++ b/docs/history/2026-07-07-infra-topology-semantic-similarity.md @@ -0,0 +1,95 @@ +# Fix: infra_topology reader ignores semantic similarity in ranking + +**Date:** 2026-07-07 +**Area:** `potpie/context-engine/application/readers/infra_topology.py`, `_common.py`, sibling readers + +## Problem + +`InfraTopologyReader` produced ranking candidates without `semantic_similarity`: + +- It never passed `fact_query=req.query` to `ClaimQueryPort.find_claims`, so backends + never stamped `row.properties["semantic_similarity"]` on infra claim rows. +- It never copied that property into `Candidate.semantic_similarity`. + +The shared `RankingService` substitutes a neutral default of `0.5` for a missing +similarity — and similarity is the highest-weighted factor (1.3). Consequences: + +1. All infra claims clustered at a flat semantic score regardless of the query. +2. Flat-scored infra claims (often `deterministic` strength = 1.0) could outrank + genuinely query-relevant results from other families (timeline, prior_bugs, ...) + when the `EnvelopeBuilder` merges and sorts all families by score. + +Every other reader (timeline, docs, owners, features, decisions, prior_bugs, +coding_preferences) already wires the query through and reads the stamped score. + +## Fix + +1. **Unanchored path** (no scope anchors → single `find_claims`): pass + `fact_query=req.query`, identical to the sibling-reader pattern. The backend + orders by similarity and stamps the score. +2. **Anchored path** (BFS traversal): the traversal stays query-free on purpose. + On vector backends (FalkorDB + embedder) a `fact_query` turns `find_claims` + into an ANN top-k search; frontier edges outside the top-k would vanish and + silently prune the walk. Instead, after the traversal completes, one follow-up + `find_claims(claim_key_in=, fact_query=req.query)` stamps + similarity onto the already-discovered rows. Topology discovery is unchanged; + only scoring becomes query-sensitive. +3. **Candidate wiring**: `Candidate.semantic_similarity` is now populated from the + stamped property. +4. **Refactor**: the `sim = row.properties.get("semantic_similarity")` + + isinstance-check snippet was copy-pasted across seven readers; extracted into + `claim_semantic_similarity(row)` in `application/readers/_common.py` and all + readers now use it. + +## Edge cases considered + +- **No query** (`req.query is None`): no follow-up query, no stamping; the ranker + keeps the neutral 0.5 default. Behavior identical to before the fix. +- **Rows without `claim_key`**: excluded from the follow-up lookup; they keep a + neutral similarity rather than crashing or mis-mapping. +- **Hard filters on the follow-up lookup**: `predicate_in`, `source_ref_in`, + `include_invalidated`, and `as_of` are forwarded so semantic stamping stays + aligned with the traversal's structural filters. +- **Vector backend returns partial stamps** (ANN top-k misses some claims): the + missing rows simply stay neutral (0.5) — graceful degradation, no ordering + distortion of the traversal itself. +- **Traversal behavior**: hop queries are untouched, so which edges are + discovered (depth, direction, environment filtering, dedupe) is byte-for-byte + the same as before. +- **`bool` similarity values**: rejected in the shared helper even though Python + treats `bool` as an `int`; this prevents fabricated 0.0/1.0 scores if a bad + backend/test row ever stamps a boolean. + +## Tests + +Added to `tests/unit/test_p9_readers.py` (`TestInfraTopologyReader`): + +- query present → infra candidates carry differentiated `semantic_similarity` in + the score breakdown, and the query-relevant claim ranks first (anchored path). +- query present, unanchored path → same, via the direct `fact_query` route. +- no query → breakdown keeps the neutral 0.5 (regression guard). +- query present → traversal discovers the same edge set as without a query + (topology discovery must stay query-insensitive). + +Added to `tests/conformance/test_graph_surface_lite_e2e.py`: + +- `test_infra_resolve_ranks_by_task_query_end_to_end` — mutate two infra edges + through `DefaultGraphService`, then `context_resolve` with a task query; asserts + the query-relevant dependency ranks first with a non-flat semantic score. This + exercises the real agent read trunk (orchestrator → envelope), not just the + reader in isolation. + +Follow-up (same change set): `graph read --subgraph infra_topology --view +service_neighborhood` used to declare `query` as an unsupported filter, so the +CLI/workbench read path rejected the query before it ever reached the reader — +while `DefaultGraphService.read()` already forwards `request.query` into the +orchestrator. Added `query` to the view's `supported_filters` and +`optional_scope` in `graph_workbench_ontology.py` (matching the timeline, +debugging, decisions, features, and knowledge views, which all declare it). +The e2e test asserts both paths: `context_resolve` with a task query and +`graph read` with `--query` produce query-differentiated similarity scores. + +## Notes + +- `coding_rules.md` was requested as the style reference but does not exist in + this workspace; existing P9 reader conventions were followed instead. 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 From 4aff1754fd8eb6a8e11a2636ed241a086b5135a7 Mon Sep 17 00:00:00 2001 From: Yash Krishan Date: Tue, 7 Jul 2026 13:22:18 +0530 Subject: [PATCH 2/2] Remove history doc from PR; code and tests only. Co-authored-by: Cursor --- ...7-07-infra-topology-semantic-similarity.md | 95 ------------------- 1 file changed, 95 deletions(-) delete mode 100644 docs/history/2026-07-07-infra-topology-semantic-similarity.md diff --git a/docs/history/2026-07-07-infra-topology-semantic-similarity.md b/docs/history/2026-07-07-infra-topology-semantic-similarity.md deleted file mode 100644 index dc76ec5cf..000000000 --- a/docs/history/2026-07-07-infra-topology-semantic-similarity.md +++ /dev/null @@ -1,95 +0,0 @@ -# Fix: infra_topology reader ignores semantic similarity in ranking - -**Date:** 2026-07-07 -**Area:** `potpie/context-engine/application/readers/infra_topology.py`, `_common.py`, sibling readers - -## Problem - -`InfraTopologyReader` produced ranking candidates without `semantic_similarity`: - -- It never passed `fact_query=req.query` to `ClaimQueryPort.find_claims`, so backends - never stamped `row.properties["semantic_similarity"]` on infra claim rows. -- It never copied that property into `Candidate.semantic_similarity`. - -The shared `RankingService` substitutes a neutral default of `0.5` for a missing -similarity — and similarity is the highest-weighted factor (1.3). Consequences: - -1. All infra claims clustered at a flat semantic score regardless of the query. -2. Flat-scored infra claims (often `deterministic` strength = 1.0) could outrank - genuinely query-relevant results from other families (timeline, prior_bugs, ...) - when the `EnvelopeBuilder` merges and sorts all families by score. - -Every other reader (timeline, docs, owners, features, decisions, prior_bugs, -coding_preferences) already wires the query through and reads the stamped score. - -## Fix - -1. **Unanchored path** (no scope anchors → single `find_claims`): pass - `fact_query=req.query`, identical to the sibling-reader pattern. The backend - orders by similarity and stamps the score. -2. **Anchored path** (BFS traversal): the traversal stays query-free on purpose. - On vector backends (FalkorDB + embedder) a `fact_query` turns `find_claims` - into an ANN top-k search; frontier edges outside the top-k would vanish and - silently prune the walk. Instead, after the traversal completes, one follow-up - `find_claims(claim_key_in=, fact_query=req.query)` stamps - similarity onto the already-discovered rows. Topology discovery is unchanged; - only scoring becomes query-sensitive. -3. **Candidate wiring**: `Candidate.semantic_similarity` is now populated from the - stamped property. -4. **Refactor**: the `sim = row.properties.get("semantic_similarity")` + - isinstance-check snippet was copy-pasted across seven readers; extracted into - `claim_semantic_similarity(row)` in `application/readers/_common.py` and all - readers now use it. - -## Edge cases considered - -- **No query** (`req.query is None`): no follow-up query, no stamping; the ranker - keeps the neutral 0.5 default. Behavior identical to before the fix. -- **Rows without `claim_key`**: excluded from the follow-up lookup; they keep a - neutral similarity rather than crashing or mis-mapping. -- **Hard filters on the follow-up lookup**: `predicate_in`, `source_ref_in`, - `include_invalidated`, and `as_of` are forwarded so semantic stamping stays - aligned with the traversal's structural filters. -- **Vector backend returns partial stamps** (ANN top-k misses some claims): the - missing rows simply stay neutral (0.5) — graceful degradation, no ordering - distortion of the traversal itself. -- **Traversal behavior**: hop queries are untouched, so which edges are - discovered (depth, direction, environment filtering, dedupe) is byte-for-byte - the same as before. -- **`bool` similarity values**: rejected in the shared helper even though Python - treats `bool` as an `int`; this prevents fabricated 0.0/1.0 scores if a bad - backend/test row ever stamps a boolean. - -## Tests - -Added to `tests/unit/test_p9_readers.py` (`TestInfraTopologyReader`): - -- query present → infra candidates carry differentiated `semantic_similarity` in - the score breakdown, and the query-relevant claim ranks first (anchored path). -- query present, unanchored path → same, via the direct `fact_query` route. -- no query → breakdown keeps the neutral 0.5 (regression guard). -- query present → traversal discovers the same edge set as without a query - (topology discovery must stay query-insensitive). - -Added to `tests/conformance/test_graph_surface_lite_e2e.py`: - -- `test_infra_resolve_ranks_by_task_query_end_to_end` — mutate two infra edges - through `DefaultGraphService`, then `context_resolve` with a task query; asserts - the query-relevant dependency ranks first with a non-flat semantic score. This - exercises the real agent read trunk (orchestrator → envelope), not just the - reader in isolation. - -Follow-up (same change set): `graph read --subgraph infra_topology --view -service_neighborhood` used to declare `query` as an unsupported filter, so the -CLI/workbench read path rejected the query before it ever reached the reader — -while `DefaultGraphService.read()` already forwards `request.query` into the -orchestrator. Added `query` to the view's `supported_filters` and -`optional_scope` in `graph_workbench_ontology.py` (matching the timeline, -debugging, decisions, features, and knowledge views, which all declare it). -The e2e test asserts both paths: `context_resolve` with a task query and -`graph read` with `--query` produce query-differentiated similarity scores. - -## Notes - -- `coding_rules.md` was requested as the style reference but does not exist in - this workspace; existing P9 reader conventions were followed instead.