Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ All notable changes to vouch are documented here. Format follows
## [Unreleased]

### Fixed
- `graph.find_neighbors()` no longer leaks edges to excluded neighbors.
superseded/archived/redacted claims and missing nodes were already filtered
out of the returned `nodes` list, but the edge pointing at them was still
included -- `kb.neighbors` (mcp, jsonl, and cli surfaces all share this
code path) could hand back an edge whose `target` referenced a claim id
the response itself said didn't exist.
- `lifecycle.contradict()` no longer lets a claim contradict itself. calling
it with the same claim id on both sides previously wrote a self-loop
`contradicts` reference and flipped the claim to `contested` with no
Expand Down
35 changes: 17 additions & 18 deletions src/vouch/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,23 @@ def find_neighbors(
for current in frontier:
for edge in _edges_from_node(store, current, rel_types=rel_filter):
other = edge.target if edge.source == current else edge.source
if other not in visited:
try:
kind = _node_kind(store, other)
except ArtifactNotFoundError:
continue
if not _neighbor_ok(store, other, kind):
continue
visited.add(other)
next_frontier.append(other)
nodes.append({
"id": other,
"kind": kind,
"distance": dist,
"via": current,
"relation": edge.relation,
"summary": _summary_for(store, kind, other),
})
ekey = (edge.source, edge.target, edge.relation)
if ekey not in seen_edges:
seen_edges.add(ekey)
Expand All @@ -185,24 +202,6 @@ def find_neighbors(
"relation": edge.relation,
"relation_id": edge.relation_id,
})
if other in visited:
continue
try:
kind = _node_kind(store, other)
except ArtifactNotFoundError:
continue
if not _neighbor_ok(store, other, kind):
continue
visited.add(other)
next_frontier.append(other)
nodes.append({
"id": other,
"kind": kind,
"distance": dist,
"via": current,
"relation": edge.relation,
"summary": _summary_for(store, kind, other),
})
if len(nodes) >= max_nodes:
break
if len(nodes) >= max_nodes:
Expand Down
4 changes: 4 additions & 0 deletions tests/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def test_find_neighbors_excludes_superseded_claims(store: KBStore) -> None:
result = graph.find_neighbors(store, "new", depth=1)
assert {n["id"] for n in result["nodes"]} == set()
assert "old" not in {n["id"] for n in result["nodes"]}
# the edge to the excluded neighbor must not leak either -- a client
# that trusts `nodes` as the visible set would otherwise see a "supersedes"
# edge pointing at a claim id it was never told exists.
assert result["edges"] == []


def test_find_neighbors_unknown_node_raises(store: KBStore) -> None:
Expand Down
Loading