Wraps over-long lines, sorts imports, drops an unused local, and bumps the agent submodule for its own import-sort fix so make check passes.
This commit is contained in:
1 parent
78033bb830
commit
c492124fe1
12 files changed
+272
-68
No files matched your search
+1
-1
Submodule agents/gewu updated: 3ef2c5cc31...511e74d1a3.
@@ -13,6 +13,7 @@ from gewu.domain.contracts import (
|
|||||||
IngestReviewItem,
|
IngestReviewItem,
|
||||||
KnowledgeNode,
|
KnowledgeNode,
|
||||||
NeighborRequest,
|
NeighborRequest,
|
||||||
|
OnlineEvidence,
|
||||||
ProjectRecord,
|
ProjectRecord,
|
||||||
ProposalReceipt,
|
ProposalReceipt,
|
||||||
ProposalRevision,
|
ProposalRevision,
|
||||||
@@ -26,7 +27,6 @@ from gewu.domain.contracts import (
|
|||||||
ServiceDiagnostics,
|
ServiceDiagnostics,
|
||||||
SourceRevisionRecord,
|
SourceRevisionRecord,
|
||||||
TypedEdge,
|
TypedEdge,
|
||||||
OnlineEvidence,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ from gewu.application.search import (
|
|||||||
BrowseIndexResult,
|
BrowseIndexResult,
|
||||||
GraphIndexResult,
|
GraphIndexResult,
|
||||||
IndexedNode,
|
IndexedNode,
|
||||||
SourceRelatedNodeHit,
|
|
||||||
SourceNodesIndexResult,
|
|
||||||
SearchEdgeHit,
|
SearchEdgeHit,
|
||||||
SearchEdgeResult,
|
SearchEdgeResult,
|
||||||
SearchIndexResult,
|
SearchIndexResult,
|
||||||
SourceIndexResult,
|
SourceIndexResult,
|
||||||
|
SourceNodesIndexResult,
|
||||||
|
SourceRelatedNodeHit,
|
||||||
)
|
)
|
||||||
from gewu.application.serialization import safe_yaml_record
|
from gewu.application.serialization import safe_yaml_record
|
||||||
from gewu.domain.contracts import Confidence, KnowledgeNode, SourceRevisionRecord, TypedEdge
|
from gewu.domain.contracts import Confidence, KnowledgeNode, SourceRevisionRecord, TypedEdge
|
||||||
@@ -113,7 +113,8 @@ class SQLiteSearchIndex:
|
|||||||
)
|
)
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS indexed_citations ("
|
"CREATE TABLE IF NOT EXISTS indexed_citations ("
|
||||||
"node_id TEXT NOT NULL, project_id TEXT NOT NULL, source_revision_id TEXT NOT NULL, "
|
"node_id TEXT NOT NULL, project_id TEXT NOT NULL, "
|
||||||
|
"source_revision_id TEXT NOT NULL, "
|
||||||
"PRIMARY KEY(node_id, source_revision_id))"
|
"PRIMARY KEY(node_id, source_revision_id))"
|
||||||
)
|
)
|
||||||
connection.execute(
|
connection.execute(
|
||||||
@@ -700,7 +701,13 @@ class SQLiteSearchIndex:
|
|||||||
project_scope = tuple(sorted(set(project_ids) | ({"shared"} if include_shared else set())))
|
project_scope = tuple(sorted(set(project_ids) | ({"shared"} if include_shared else set())))
|
||||||
if not project_scope:
|
if not project_scope:
|
||||||
return BrowseIndexResult(
|
return BrowseIndexResult(
|
||||||
revision, (), 0, 0, 0, {k: {} for k in ("tags", "audience", "use_for", "domain")}, False
|
revision,
|
||||||
|
(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{k: {} for k in ("tags", "audience", "use_for", "domain")},
|
||||||
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
removal_filter = (
|
removal_filter = (
|
||||||
@@ -766,17 +773,37 @@ class SQLiteSearchIndex:
|
|||||||
match = self._match_expression(query)
|
match = self._match_expression(query)
|
||||||
|
|
||||||
if order == "title_asc":
|
if order == "title_asc":
|
||||||
order_by = "LOWER(json_extract(indexed_nodes.node_json, '$.title')) ASC, json_extract(indexed_nodes.node_json, '$.title') ASC, indexed_nodes.node_id ASC"
|
order_by = (
|
||||||
|
"LOWER(json_extract(indexed_nodes.node_json, '$.title')) ASC, "
|
||||||
|
"json_extract(indexed_nodes.node_json, '$.title') ASC, "
|
||||||
|
"indexed_nodes.node_id ASC"
|
||||||
|
)
|
||||||
elif order == "title_desc":
|
elif order == "title_desc":
|
||||||
order_by = "LOWER(json_extract(indexed_nodes.node_json, '$.title')) DESC, json_extract(indexed_nodes.node_json, '$.title') DESC, indexed_nodes.node_id DESC"
|
order_by = (
|
||||||
|
"LOWER(json_extract(indexed_nodes.node_json, '$.title')) DESC, "
|
||||||
|
"json_extract(indexed_nodes.node_json, '$.title') DESC, "
|
||||||
|
"indexed_nodes.node_id DESC"
|
||||||
|
)
|
||||||
elif order == "date_newest":
|
elif order == "date_newest":
|
||||||
order_by = "(json_extract(indexed_nodes.node_json, '$.as_of') IS NULL) ASC, json_extract(indexed_nodes.node_json, '$.as_of') DESC, indexed_nodes.node_id ASC"
|
order_by = (
|
||||||
|
"(json_extract(indexed_nodes.node_json, '$.as_of') IS NULL) ASC, "
|
||||||
|
"json_extract(indexed_nodes.node_json, '$.as_of') DESC, "
|
||||||
|
"indexed_nodes.node_id ASC"
|
||||||
|
)
|
||||||
elif order == "date_oldest":
|
elif order == "date_oldest":
|
||||||
order_by = "(json_extract(indexed_nodes.node_json, '$.as_of') IS NULL) ASC, json_extract(indexed_nodes.node_json, '$.as_of') ASC, indexed_nodes.node_id ASC"
|
order_by = (
|
||||||
|
"(json_extract(indexed_nodes.node_json, '$.as_of') IS NULL) ASC, "
|
||||||
|
"json_extract(indexed_nodes.node_json, '$.as_of') ASC, "
|
||||||
|
"indexed_nodes.node_id ASC"
|
||||||
|
)
|
||||||
elif order == "relevance" and match:
|
elif order == "relevance" and match:
|
||||||
order_by = "score ASC, indexed_nodes.node_id ASC"
|
order_by = "score ASC, indexed_nodes.node_id ASC"
|
||||||
else:
|
else:
|
||||||
order_by = "LOWER(json_extract(indexed_nodes.node_json, '$.title')) ASC, json_extract(indexed_nodes.node_json, '$.title') ASC, indexed_nodes.node_id ASC"
|
order_by = (
|
||||||
|
"LOWER(json_extract(indexed_nodes.node_json, '$.title')) ASC, "
|
||||||
|
"json_extract(indexed_nodes.node_json, '$.title') ASC, "
|
||||||
|
"indexed_nodes.node_id ASC"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with closing(self._connect()) as connection:
|
with closing(self._connect()) as connection:
|
||||||
@@ -794,7 +821,8 @@ class SQLiteSearchIndex:
|
|||||||
if project_ids:
|
if project_ids:
|
||||||
p_holders = ", ".join("?" for _ in project_ids)
|
p_holders = ", ".join("?" for _ in project_ids)
|
||||||
p_row = connection.execute(
|
p_row = connection.execute(
|
||||||
f"SELECT COUNT(*) FROM indexed_nodes WHERE project_id IN ({p_holders}){removal_filter}",
|
f"SELECT COUNT(*) FROM indexed_nodes "
|
||||||
|
f"WHERE project_id IN ({p_holders}){removal_filter}",
|
||||||
project_ids,
|
project_ids,
|
||||||
).fetchone()
|
).fetchone()
|
||||||
project_node_count = int(p_row[0]) if p_row else 0
|
project_node_count = int(p_row[0]) if p_row else 0
|
||||||
@@ -803,7 +831,8 @@ class SQLiteSearchIndex:
|
|||||||
shared_node_count = 0
|
shared_node_count = 0
|
||||||
if include_shared:
|
if include_shared:
|
||||||
s_row = connection.execute(
|
s_row = connection.execute(
|
||||||
f"SELECT COUNT(*) FROM indexed_nodes WHERE project_id='shared'{removal_filter}"
|
f"SELECT COUNT(*) FROM indexed_nodes "
|
||||||
|
f"WHERE project_id='shared'{removal_filter}"
|
||||||
).fetchone()
|
).fetchone()
|
||||||
shared_node_count = int(s_row[0]) if s_row else 0
|
shared_node_count = int(s_row[0]) if s_row else 0
|
||||||
|
|
||||||
@@ -825,7 +854,8 @@ class SQLiteSearchIndex:
|
|||||||
if match:
|
if match:
|
||||||
query_stmt = (
|
query_stmt = (
|
||||||
"SELECT indexed_nodes.node_json, bm25(node_fts) AS score "
|
"SELECT indexed_nodes.node_json, bm25(node_fts) AS score "
|
||||||
f"FROM node_fts JOIN indexed_nodes USING(node_id) {where_clause} AND node_fts MATCH ? "
|
f"FROM node_fts JOIN indexed_nodes USING(node_id) "
|
||||||
|
f"{where_clause} AND node_fts MATCH ? "
|
||||||
f"ORDER BY {order_by} LIMIT ? OFFSET ?"
|
f"ORDER BY {order_by} LIMIT ? OFFSET ?"
|
||||||
)
|
)
|
||||||
node_rows = connection.execute(
|
node_rows = connection.execute(
|
||||||
@@ -852,7 +882,8 @@ class SQLiteSearchIndex:
|
|||||||
for field in ("tags", "audience", "use_for", "domain"):
|
for field in ("tags", "audience", "use_for", "domain"):
|
||||||
facet_stmt = (
|
facet_stmt = (
|
||||||
"SELECT facet.value, COUNT(DISTINCT indexed_nodes.node_id) "
|
"SELECT facet.value, COUNT(DISTINCT indexed_nodes.node_id) "
|
||||||
f"FROM indexed_nodes, json_each(indexed_nodes.node_json, '$.' || ?) AS facet "
|
f"FROM indexed_nodes, "
|
||||||
|
f"json_each(indexed_nodes.node_json, '$.' || ?) AS facet "
|
||||||
f"WHERE indexed_nodes.project_id IN ({scope_holders}){removal_filter} "
|
f"WHERE indexed_nodes.project_id IN ({scope_holders}){removal_filter} "
|
||||||
"GROUP BY facet.value"
|
"GROUP BY facet.value"
|
||||||
)
|
)
|
||||||
@@ -884,7 +915,7 @@ class SQLiteSearchIndex:
|
|||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> SourceNodesIndexResult | None:
|
) -> SourceNodesIndexResult | None:
|
||||||
revision = self._rebuild_if_needed()
|
self._rebuild_if_needed()
|
||||||
project_scope = tuple(sorted(set(project_ids) | ({"shared"} if include_shared else set())))
|
project_scope = tuple(sorted(set(project_ids) | ({"shared"} if include_shared else set())))
|
||||||
if not project_scope:
|
if not project_scope:
|
||||||
return None
|
return None
|
||||||
@@ -904,7 +935,8 @@ class SQLiteSearchIndex:
|
|||||||
|
|
||||||
# Check source revision grant
|
# Check source revision grant
|
||||||
grant_row = connection.execute(
|
grant_row = connection.execute(
|
||||||
f"SELECT 1 FROM indexed_source_grants WHERE source_revision_id=? AND project_id IN ({scope_holders}) LIMIT 1",
|
f"SELECT 1 FROM indexed_source_grants WHERE source_revision_id=? "
|
||||||
|
f"AND project_id IN ({scope_holders}) LIMIT 1",
|
||||||
(source_revision_id, *project_scope),
|
(source_revision_id, *project_scope),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if grant_row is None:
|
if grant_row is None:
|
||||||
@@ -969,16 +1001,21 @@ class SQLiteSearchIndex:
|
|||||||
count_row = connection.execute(count_stmt, (*filter_params, match)).fetchone()
|
count_row = connection.execute(count_stmt, (*filter_params, match)).fetchone()
|
||||||
else:
|
else:
|
||||||
count_stmt = (
|
count_stmt = (
|
||||||
f"{cte} SELECT COUNT(*) FROM combined JOIN indexed_nodes USING(node_id) {where_clause}"
|
f"{cte} SELECT COUNT(*) FROM combined "
|
||||||
|
f"JOIN indexed_nodes USING(node_id) {where_clause}"
|
||||||
)
|
)
|
||||||
count_row = connection.execute(count_stmt, filter_params).fetchone()
|
count_row = connection.execute(count_stmt, filter_params).fetchone()
|
||||||
total = int(count_row[0]) if count_row else 0
|
total = int(count_row[0]) if count_row else 0
|
||||||
|
|
||||||
# Rows query
|
# Rows query
|
||||||
order_by = "LOWER(json_extract(indexed_nodes.node_json, '$.title')) ASC, indexed_nodes.node_id ASC"
|
order_by = (
|
||||||
|
"LOWER(json_extract(indexed_nodes.node_json, '$.title')) ASC, "
|
||||||
|
"indexed_nodes.node_id ASC"
|
||||||
|
)
|
||||||
if match:
|
if match:
|
||||||
row_stmt = (
|
row_stmt = (
|
||||||
f"{cte} SELECT indexed_nodes.node_json, combined.relation_basis FROM combined "
|
f"{cte} SELECT indexed_nodes.node_json, "
|
||||||
|
f"combined.relation_basis FROM combined "
|
||||||
f"JOIN indexed_nodes USING(node_id) JOIN node_fts USING(node_id) "
|
f"JOIN indexed_nodes USING(node_id) JOIN node_fts USING(node_id) "
|
||||||
f"{where_clause} AND node_fts MATCH ? ORDER BY {order_by} LIMIT ? OFFSET ?"
|
f"{where_clause} AND node_fts MATCH ? ORDER BY {order_by} LIMIT ? OFFSET ?"
|
||||||
)
|
)
|
||||||
@@ -987,8 +1024,10 @@ class SQLiteSearchIndex:
|
|||||||
).fetchall()
|
).fetchall()
|
||||||
else:
|
else:
|
||||||
row_stmt = (
|
row_stmt = (
|
||||||
f"{cte} SELECT indexed_nodes.node_json, combined.relation_basis FROM combined "
|
f"{cte} SELECT indexed_nodes.node_json, "
|
||||||
f"JOIN indexed_nodes USING(node_id) {where_clause} ORDER BY {order_by} LIMIT ? OFFSET ?"
|
f"combined.relation_basis FROM combined "
|
||||||
|
f"JOIN indexed_nodes USING(node_id) "
|
||||||
|
f"{where_clause} ORDER BY {order_by} LIMIT ? OFFSET ?"
|
||||||
)
|
)
|
||||||
rows = connection.execute(
|
rows = connection.execute(
|
||||||
row_stmt, (*filter_params, limit + 1, offset)
|
row_stmt, (*filter_params, limit + 1, offset)
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ from gewu.domain.contracts import (
|
|||||||
EdgeEndpoint,
|
EdgeEndpoint,
|
||||||
EvidenceReference,
|
EvidenceReference,
|
||||||
Finding,
|
Finding,
|
||||||
KnowledgeNode,
|
|
||||||
KnowledgeBrowseRequest,
|
|
||||||
KnowledgeBrowseEnvelope,
|
KnowledgeBrowseEnvelope,
|
||||||
|
KnowledgeBrowseRequest,
|
||||||
KnowledgeGraphEnvelope,
|
KnowledgeGraphEnvelope,
|
||||||
KnowledgeGraphRequest,
|
KnowledgeGraphRequest,
|
||||||
SourceRelatedNode,
|
KnowledgeNode,
|
||||||
SourceRelatedNodesEnvelope,
|
|
||||||
NeighborRequest,
|
NeighborRequest,
|
||||||
ReadEnvelope,
|
ReadEnvelope,
|
||||||
Scope,
|
Scope,
|
||||||
SearchRequest,
|
SearchRequest,
|
||||||
SourceListRequest,
|
SourceListRequest,
|
||||||
|
SourceRelatedNode,
|
||||||
|
SourceRelatedNodesEnvelope,
|
||||||
SourceRevisionRecord,
|
SourceRevisionRecord,
|
||||||
TypedEdge,
|
TypedEdge,
|
||||||
)
|
)
|
||||||
@@ -165,7 +165,9 @@ class SearchIndex(Protocol):
|
|||||||
|
|
||||||
def count_sources(self, *, project_ids: tuple[str, ...], include_shared: bool) -> int: ...
|
def count_sources(self, *, project_ids: tuple[str, ...], include_shared: bool) -> int: ...
|
||||||
|
|
||||||
def graph(self, *, project_ids: tuple[str, ...], include_shared: bool, max_nodes: int, max_edges: int) -> GraphIndexResult: ...
|
def graph(
|
||||||
|
self, *, project_ids: tuple[str, ...], include_shared: bool, max_nodes: int, max_edges: int
|
||||||
|
) -> GraphIndexResult: ...
|
||||||
|
|
||||||
def browse(
|
def browse(
|
||||||
self,
|
self,
|
||||||
@@ -616,8 +618,15 @@ class HybridSearchIndex:
|
|||||||
def count_sources(self, *, project_ids: tuple[str, ...], include_shared: bool) -> int:
|
def count_sources(self, *, project_ids: tuple[str, ...], include_shared: bool) -> int:
|
||||||
return self.lexical.count_sources(project_ids=project_ids, include_shared=include_shared)
|
return self.lexical.count_sources(project_ids=project_ids, include_shared=include_shared)
|
||||||
|
|
||||||
def graph(self, *, project_ids: tuple[str, ...], include_shared: bool, max_nodes: int, max_edges: int) -> GraphIndexResult:
|
def graph(
|
||||||
return self.lexical.graph(project_ids=project_ids, include_shared=include_shared, max_nodes=max_nodes, max_edges=max_edges)
|
self, *, project_ids: tuple[str, ...], include_shared: bool, max_nodes: int, max_edges: int
|
||||||
|
) -> GraphIndexResult:
|
||||||
|
return self.lexical.graph(
|
||||||
|
project_ids=project_ids,
|
||||||
|
include_shared=include_shared,
|
||||||
|
max_nodes=max_nodes,
|
||||||
|
max_edges=max_edges,
|
||||||
|
)
|
||||||
|
|
||||||
def browse(self, **kwargs: Any) -> BrowseIndexResult:
|
def browse(self, **kwargs: Any) -> BrowseIndexResult:
|
||||||
return self.lexical.browse(**kwargs)
|
return self.lexical.browse(**kwargs)
|
||||||
@@ -709,7 +718,9 @@ class CanonicalSearchService:
|
|||||||
state = "current" if index_revision == canonical_revision else "lagging"
|
state = "current" if index_revision == canonical_revision else "lagging"
|
||||||
return canonical_revision, index_revision, state, schema_version
|
return canonical_revision, index_revision, state, schema_version
|
||||||
|
|
||||||
def browse(self, request: KnowledgeBrowseRequest, *, principal: Principal) -> KnowledgeBrowseEnvelope:
|
def browse(
|
||||||
|
self, request: KnowledgeBrowseRequest, *, principal: Principal
|
||||||
|
) -> KnowledgeBrowseEnvelope:
|
||||||
project_ids = self._resolve_scope(request.scope, principal)
|
project_ids = self._resolve_scope(request.scope, principal)
|
||||||
fingerprint = hashlib.sha256(canonical_json({
|
fingerprint = hashlib.sha256(canonical_json({
|
||||||
"projects": project_ids,
|
"projects": project_ids,
|
||||||
@@ -723,7 +734,9 @@ class CanonicalSearchService:
|
|||||||
"to": request.as_of_to.isoformat() if request.as_of_to else None,
|
"to": request.as_of_to.isoformat() if request.as_of_to else None,
|
||||||
"stale": request.stale_only, "order": request.order, "limit": request.limit,
|
"stale": request.stale_only, "order": request.order, "limit": request.limit,
|
||||||
})).hexdigest()
|
})).hexdigest()
|
||||||
offset, expected_revision = self._cursor_offset(request.cursor, fingerprint, max_offset=100_000)
|
offset, expected_revision = self._cursor_offset(
|
||||||
|
request.cursor, fingerprint, max_offset=100_000
|
||||||
|
)
|
||||||
lexical = self.index.lexical if isinstance(self.index, HybridSearchIndex) else self.index
|
lexical = self.index.lexical if isinstance(self.index, HybridSearchIndex) else self.index
|
||||||
result = lexical.browse(
|
result = lexical.browse(
|
||||||
query=request.query,
|
query=request.query,
|
||||||
@@ -761,7 +774,12 @@ class CanonicalSearchService:
|
|||||||
canonical_revision = self.canonical_head()
|
canonical_revision = self.canonical_head()
|
||||||
warnings: tuple[Finding, ...] = ()
|
warnings: tuple[Finding, ...] = ()
|
||||||
if result.index_revision != canonical_revision:
|
if result.index_revision != canonical_revision:
|
||||||
warnings = (Finding(code="INDEX_LAG", message="Browse data reflects an older accepted revision."),)
|
warnings = (
|
||||||
|
Finding(
|
||||||
|
code="INDEX_LAG",
|
||||||
|
message="Browse data reflects an older accepted revision.",
|
||||||
|
),
|
||||||
|
)
|
||||||
return KnowledgeBrowseEnvelope(
|
return KnowledgeBrowseEnvelope(
|
||||||
request_id="browse-" + secrets.token_hex(12),
|
request_id="browse-" + secrets.token_hex(12),
|
||||||
resolved_scope=Scope(projects=project_ids, include_shared=request.scope.include_shared),
|
resolved_scope=Scope(projects=project_ids, include_shared=request.scope.include_shared),
|
||||||
@@ -824,7 +842,12 @@ class CanonicalSearchService:
|
|||||||
canonical_revision = self.canonical_head()
|
canonical_revision = self.canonical_head()
|
||||||
warnings: tuple[Finding, ...] = ()
|
warnings: tuple[Finding, ...] = ()
|
||||||
if result.index_revision != canonical_revision:
|
if result.index_revision != canonical_revision:
|
||||||
warnings = (Finding(code="INDEX_LAG", message="Source nodes reflect an older accepted revision."),)
|
warnings = (
|
||||||
|
Finding(
|
||||||
|
code="INDEX_LAG",
|
||||||
|
message="Source nodes reflect an older accepted revision.",
|
||||||
|
),
|
||||||
|
)
|
||||||
return SourceRelatedNodesEnvelope(
|
return SourceRelatedNodesEnvelope(
|
||||||
request_id="srcnodes-" + secrets.token_hex(12),
|
request_id="srcnodes-" + secrets.token_hex(12),
|
||||||
resolved_scope=Scope(projects=(project_id,), include_shared=include_shared),
|
resolved_scope=Scope(projects=(project_id,), include_shared=include_shared),
|
||||||
@@ -972,7 +995,9 @@ class CanonicalSearchService:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def graph(self, request: KnowledgeGraphRequest, *, principal: Principal) -> KnowledgeGraphEnvelope:
|
def graph(
|
||||||
|
self, request: KnowledgeGraphRequest, *, principal: Principal
|
||||||
|
) -> KnowledgeGraphEnvelope:
|
||||||
project_ids = self._resolve_scope(request.scope, principal)
|
project_ids = self._resolve_scope(request.scope, principal)
|
||||||
if request.center_node_id is None:
|
if request.center_node_id is None:
|
||||||
result = self.index.graph(
|
result = self.index.graph(
|
||||||
@@ -995,7 +1020,8 @@ class CanonicalSearchService:
|
|||||||
if request.max_nodes > 1:
|
if request.max_nodes > 1:
|
||||||
neighbors = self.index.neighbors(
|
neighbors = self.index.neighbors(
|
||||||
node_id=origin.node_id, edge_types=(), project_ids=project_ids,
|
node_id=origin.node_id, edge_types=(), project_ids=project_ids,
|
||||||
include_shared=request.scope.include_shared, limit=min(50, request.max_nodes - 1),
|
include_shared=request.scope.include_shared,
|
||||||
|
limit=min(50, request.max_nodes - 1),
|
||||||
)
|
)
|
||||||
for hit in neighbors.edges:
|
for hit in neighbors.edges:
|
||||||
endpoint = hit.neighbor_endpoint
|
endpoint = hit.neighbor_endpoint
|
||||||
@@ -1007,12 +1033,20 @@ class CanonicalSearchService:
|
|||||||
index_revision=neighbors.index_revision if neighbors else scoped.index_revision,
|
index_revision=neighbors.index_revision if neighbors else scoped.index_revision,
|
||||||
nodes=tuple(nodes_by_id.values()),
|
nodes=tuple(nodes_by_id.values()),
|
||||||
edges=tuple(edges[:request.max_edges]),
|
edges=tuple(edges[:request.max_edges]),
|
||||||
truncated=(neighbors.has_more if neighbors else False) or len(edges) > request.max_edges,
|
truncated=(
|
||||||
|
(neighbors.has_more if neighbors else False)
|
||||||
|
or len(edges) > request.max_edges
|
||||||
|
),
|
||||||
)
|
)
|
||||||
canonical_revision = self.canonical_head()
|
canonical_revision = self.canonical_head()
|
||||||
warnings: tuple[Finding, ...] = ()
|
warnings: tuple[Finding, ...] = ()
|
||||||
if result.index_revision != canonical_revision:
|
if result.index_revision != canonical_revision:
|
||||||
warnings = (Finding(code="INDEX_LAG", message="Graph data reflects an older accepted revision."),)
|
warnings = (
|
||||||
|
Finding(
|
||||||
|
code="INDEX_LAG",
|
||||||
|
message="Graph data reflects an older accepted revision.",
|
||||||
|
),
|
||||||
|
)
|
||||||
return KnowledgeGraphEnvelope(
|
return KnowledgeGraphEnvelope(
|
||||||
request_id="graph-" + secrets.token_hex(12),
|
request_id="graph-" + secrets.token_hex(12),
|
||||||
resolved_scope=Scope(projects=project_ids, include_shared=request.scope.include_shared),
|
resolved_scope=Scope(projects=project_ids, include_shared=request.scope.include_shared),
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ from gewu.domain.contracts import (
|
|||||||
GitCommitId,
|
GitCommitId,
|
||||||
KnowledgeBrowseEnvelope,
|
KnowledgeBrowseEnvelope,
|
||||||
KnowledgeGraphEnvelope,
|
KnowledgeGraphEnvelope,
|
||||||
SourceRelatedNodesEnvelope,
|
|
||||||
ProjectRecord,
|
ProjectRecord,
|
||||||
ReadEnvelope,
|
ReadEnvelope,
|
||||||
RecordId,
|
RecordId,
|
||||||
ServiceDiagnostics,
|
ServiceDiagnostics,
|
||||||
|
SourceRelatedNodesEnvelope,
|
||||||
)
|
)
|
||||||
from gewu.transports.accounts_http import register_account_routes
|
from gewu.transports.accounts_http import register_account_routes
|
||||||
from gewu.transports.admin_http import register_admin_routes
|
from gewu.transports.admin_http import register_admin_routes
|
||||||
|
|||||||
@@ -490,7 +490,12 @@ class OnlineEvidence(StrictRecord):
|
|||||||
def public_http_url(cls, value: str) -> str:
|
def public_http_url(cls, value: str) -> str:
|
||||||
parsed = urlsplit(value)
|
parsed = urlsplit(value)
|
||||||
hostname = parsed.hostname
|
hostname = parsed.hostname
|
||||||
if parsed.scheme not in {"http", "https"} or not hostname or parsed.username or parsed.password:
|
if (
|
||||||
|
parsed.scheme not in {"http", "https"}
|
||||||
|
or not hostname
|
||||||
|
or parsed.username
|
||||||
|
or parsed.password
|
||||||
|
):
|
||||||
raise ValueError("online evidence requires an HTTP(S) URL without embedded credentials")
|
raise ValueError("online evidence requires an HTTP(S) URL without embedded credentials")
|
||||||
if hostname.lower() == "localhost" or hostname.lower().endswith((".localhost", ".local")):
|
if hostname.lower() == "localhost" or hostname.lower().endswith((".localhost", ".local")):
|
||||||
raise ValueError("web references must use a public hostname")
|
raise ValueError("web references must use a public hostname")
|
||||||
@@ -830,14 +835,20 @@ class KnowledgeBrowseRequest(StrictRecord):
|
|||||||
as_of_from: date | None = None
|
as_of_from: date | None = None
|
||||||
as_of_to: date | None = None
|
as_of_to: date | None = None
|
||||||
stale_only: bool = False
|
stale_only: bool = False
|
||||||
order: Literal["title_asc", "title_desc", "date_newest", "date_oldest", "relevance"] = "title_asc"
|
order: Literal[
|
||||||
|
"title_asc", "title_desc", "date_newest", "date_oldest", "relevance"
|
||||||
|
] = "title_asc"
|
||||||
limit: int = Field(default=20, ge=1, le=50)
|
limit: int = Field(default=20, ge=1, le=50)
|
||||||
cursor: str | None = Field(default=None, max_length=512)
|
cursor: str | None = Field(default=None, max_length=512)
|
||||||
budget_tokens: int = Field(ge=256, le=50_000)
|
budget_tokens: int = Field(ge=256, le=50_000)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def validate_browse(self) -> KnowledgeBrowseRequest:
|
def validate_browse(self) -> KnowledgeBrowseRequest:
|
||||||
if self.as_of_from is not None and self.as_of_to is not None and self.as_of_from > self.as_of_to:
|
if (
|
||||||
|
self.as_of_from is not None
|
||||||
|
and self.as_of_to is not None
|
||||||
|
and self.as_of_from > self.as_of_to
|
||||||
|
):
|
||||||
raise ValueError("as_of_from must not be later than as_of_to")
|
raise ValueError("as_of_from must not be later than as_of_to")
|
||||||
if self.order == "relevance" and not self.query:
|
if self.order == "relevance" and not self.query:
|
||||||
raise ValueError("relevance ordering requires a search query")
|
raise ValueError("relevance ordering requires a search query")
|
||||||
|
|||||||
@@ -211,7 +211,11 @@ def _validate_node_enrichment(
|
|||||||
for field in ("node_id", "project_id", "node_type", "created", "lifecycle"):
|
for field in ("node_id", "project_id", "node_type", "created", "lifecycle"):
|
||||||
if getattr(previous, field) != getattr(updated, field):
|
if getattr(previous, field) != getattr(updated, field):
|
||||||
findings.append(
|
findings.append(
|
||||||
_finding("NODE_ENRICHMENT_IDENTITY_CHANGED", f"Enrichment cannot change {field}", path)
|
_finding(
|
||||||
|
"NODE_ENRICHMENT_IDENTITY_CHANGED",
|
||||||
|
f"Enrichment cannot change {field}",
|
||||||
|
path,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if updated.updated <= previous.updated:
|
if updated.updated <= previous.updated:
|
||||||
findings.append(
|
findings.append(
|
||||||
@@ -230,7 +234,11 @@ def _validate_node_enrichment(
|
|||||||
new_items = {claim.claim_id: claim for claim in getattr(updated, field)}
|
new_items = {claim.claim_id: claim for claim in getattr(updated, field)}
|
||||||
if not old_items.keys() <= new_items.keys():
|
if not old_items.keys() <= new_items.keys():
|
||||||
findings.append(
|
findings.append(
|
||||||
_finding("NODE_ENRICHMENT_REMOVED_CLAIM", "Enrichment cannot remove accepted claims", f"{path}.{field}")
|
_finding(
|
||||||
|
"NODE_ENRICHMENT_REMOVED_CLAIM",
|
||||||
|
"Enrichment cannot remove accepted claims",
|
||||||
|
f"{path}.{field}",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
for claim_id, old_claim in old_items.items():
|
for claim_id, old_claim in old_items.items():
|
||||||
@@ -241,27 +249,58 @@ def _validate_node_enrichment(
|
|||||||
or old_claim.evidence_type != new_claim.evidence_type
|
or old_claim.evidence_type != new_claim.evidence_type
|
||||||
):
|
):
|
||||||
findings.append(
|
findings.append(
|
||||||
_finding("NODE_ENRICHMENT_CHANGED_CLAIM", "Enrichment cannot alter an accepted assertion", claim_path)
|
_finding(
|
||||||
|
"NODE_ENRICHMENT_CHANGED_CLAIM",
|
||||||
|
"Enrichment cannot alter an accepted assertion",
|
||||||
|
claim_path,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if not set(old_claim.online_evidence) <= set(new_claim.online_evidence):
|
if not set(old_claim.online_evidence) <= set(new_claim.online_evidence):
|
||||||
findings.append(
|
findings.append(
|
||||||
_finding("NODE_ENRICHMENT_REMOVED_ONLINE_EVIDENCE", "Enrichment cannot remove prior online evidence", claim_path)
|
_finding(
|
||||||
|
"NODE_ENRICHMENT_REMOVED_ONLINE_EVIDENCE",
|
||||||
|
"Enrichment cannot remove prior online evidence",
|
||||||
|
claim_path,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
old_evidence = set(old_claim.evidence)
|
old_evidence = set(old_claim.evidence)
|
||||||
if not old_evidence <= set(new_claim.evidence):
|
if not old_evidence <= set(new_claim.evidence):
|
||||||
findings.append(
|
findings.append(
|
||||||
_finding("NODE_ENRICHMENT_REMOVED_EVIDENCE", "Enrichment cannot remove accepted evidence", claim_path)
|
_finding(
|
||||||
|
"NODE_ENRICHMENT_REMOVED_EVIDENCE",
|
||||||
|
"Enrichment cannot remove accepted evidence",
|
||||||
|
claim_path,
|
||||||
)
|
)
|
||||||
if new_claim.evidence != old_claim.evidence and new_claim.assessment.assessor_id != proposer_id:
|
)
|
||||||
|
if (
|
||||||
|
new_claim.evidence != old_claim.evidence
|
||||||
|
and new_claim.assessment.assessor_id != proposer_id
|
||||||
|
):
|
||||||
findings.append(
|
findings.append(
|
||||||
_finding("ASSESSOR_ATTRIBUTION_MISMATCH", "New evidence requires an assessment by its proposer", claim_path)
|
_finding(
|
||||||
|
"ASSESSOR_ATTRIBUTION_MISMATCH",
|
||||||
|
"New evidence requires an assessment by its proposer",
|
||||||
|
claim_path,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if not set(previous.tags) <= set(updated.tags):
|
if not set(previous.tags) <= set(updated.tags):
|
||||||
findings.append(_finding("NODE_ENRICHMENT_REMOVED_FACET", "Enrichment cannot remove existing tags", f"{path}.tags"))
|
findings.append(
|
||||||
|
_finding(
|
||||||
|
"NODE_ENRICHMENT_REMOVED_FACET",
|
||||||
|
"Enrichment cannot remove existing tags",
|
||||||
|
f"{path}.tags",
|
||||||
|
)
|
||||||
|
)
|
||||||
for field in ("audience", "domain", "use_for"):
|
for field in ("audience", "domain", "use_for"):
|
||||||
if not set(getattr(previous, field)) <= set(getattr(updated, field)):
|
if not set(getattr(previous, field)) <= set(getattr(updated, field)):
|
||||||
findings.append(_finding("NODE_ENRICHMENT_REMOVED_FACET", "Enrichment cannot remove existing facets", f"{path}.{field}"))
|
findings.append(
|
||||||
|
_finding(
|
||||||
|
"NODE_ENRICHMENT_REMOVED_FACET",
|
||||||
|
"Enrichment cannot remove existing facets",
|
||||||
|
f"{path}.{field}",
|
||||||
|
)
|
||||||
|
)
|
||||||
return tuple(findings)
|
return tuple(findings)
|
||||||
|
|
||||||
|
|
||||||
@@ -378,11 +417,17 @@ def validate_proposal(
|
|||||||
elif operation.operation == "update_node":
|
elif operation.operation == "update_node":
|
||||||
if previous_node is None:
|
if previous_node is None:
|
||||||
findings.append(
|
findings.append(
|
||||||
_finding("NODE_NOT_FOUND", "Update target is not an accepted node", f"{path}.target_id")
|
_finding(
|
||||||
|
"NODE_NOT_FOUND",
|
||||||
|
"Update target is not an accepted node",
|
||||||
|
f"{path}.target_id",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
findings.extend(
|
findings.extend(
|
||||||
_validate_node_enrichment(previous_node, node, proposal.proposer_id, f"{path}.node")
|
_validate_node_enrichment(
|
||||||
|
previous_node, node, proposal.proposer_id, f"{path}.node"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if node.node_type.value == "entity":
|
if node.node_type.value == "entity":
|
||||||
possible_duplicates = [
|
possible_duplicates = [
|
||||||
@@ -444,7 +489,10 @@ def validate_proposal(
|
|||||||
and old_claim.evidence == claim.evidence
|
and old_claim.evidence == claim.evidence
|
||||||
and old_claim.assessment == claim.assessment
|
and old_claim.assessment == claim.assessment
|
||||||
)
|
)
|
||||||
if claim.assessment.assessor_id != proposal.proposer_id and not unchanged_prior_assessment:
|
if (
|
||||||
|
claim.assessment.assessor_id != proposal.proposer_id
|
||||||
|
and not unchanged_prior_assessment
|
||||||
|
):
|
||||||
findings.append(
|
findings.append(
|
||||||
_finding(
|
_finding(
|
||||||
"ASSESSOR_ATTRIBUTION_MISMATCH",
|
"ASSESSOR_ATTRIBUTION_MISMATCH",
|
||||||
@@ -873,7 +921,6 @@ def validate_proposal(
|
|||||||
"EDGE_SOURCE_MISSING",
|
"EDGE_SOURCE_MISSING",
|
||||||
"EDGE_PROJECT_MISSING",
|
"EDGE_PROJECT_MISSING",
|
||||||
"EDGE_TYPE_MISMATCH",
|
"EDGE_TYPE_MISMATCH",
|
||||||
"NODE_NOT_FOUND",
|
|
||||||
"POLICY_WEAKENING",
|
"POLICY_WEAKENING",
|
||||||
"OUT_OF_SCOPE",
|
"OUT_OF_SCOPE",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
|
|||||||
from gewu.domain.contracts import (
|
from gewu.domain.contracts import (
|
||||||
AutoAcceptPolicy,
|
AutoAcceptPolicy,
|
||||||
Confidence,
|
Confidence,
|
||||||
NeighborRequest,
|
|
||||||
KnowledgeBrowseRequest,
|
KnowledgeBrowseRequest,
|
||||||
KnowledgeGraphRequest,
|
KnowledgeGraphRequest,
|
||||||
|
NeighborRequest,
|
||||||
NodeType,
|
NodeType,
|
||||||
ProjectRecord,
|
ProjectRecord,
|
||||||
RecordId,
|
RecordId,
|
||||||
|
|||||||
@@ -528,7 +528,10 @@ def create_mcp_server(
|
|||||||
stale_only: bool = False,
|
stale_only: bool = False,
|
||||||
include_evidence: bool = True,
|
include_evidence: bool = True,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Search accepted knowledge in scope. Set include_evidence=false for compact claim summaries; the default includes full citations."""
|
(
|
||||||
|
"Search accepted knowledge in scope. Set include_evidence=false "
|
||||||
|
"for compact claim summaries; the default includes full citations."
|
||||||
|
)
|
||||||
|
|
||||||
principal = _principal(ctx, identity_provider)
|
principal = _principal(ctx, identity_provider)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ def test_review_store_round_trips_jev_facet_arrays_and_idempotent_create(tmp_pat
|
|||||||
{
|
{
|
||||||
"facet": "tags",
|
"facet": "tags",
|
||||||
"value": "spatial sound",
|
"value": "spatial sound",
|
||||||
"definition": "Audio techniques that place sound in immersive spatial configurations.",
|
"definition": (
|
||||||
|
"Audio techniques that place sound in immersive "
|
||||||
|
"spatial configurations."
|
||||||
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -165,10 +165,20 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
|||||||
full_row = full_result.json()["data"][0]
|
full_row = full_result.json()["data"][0]
|
||||||
assert full_row["evidence_included"] is True
|
assert full_row["evidence_included"] is True
|
||||||
assert full_row["node"]["claims"][0]["evidence"]
|
assert full_row["node"]["claims"][0]["evidence"]
|
||||||
assert full_result.json()["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
|
assert (
|
||||||
|
full_result.json()["citations"][0]["quote"]
|
||||||
|
== NODE.claims[0].evidence[0].quote
|
||||||
|
)
|
||||||
browse = await http.post(
|
browse = await http.post(
|
||||||
"/v1/knowledge/browse",
|
"/v1/knowledge/browse",
|
||||||
json={"scope": {"projects": ["dome-performance"], "include_projects": [], "include_shared": False}, "limit": 2},
|
json={
|
||||||
|
"scope": {
|
||||||
|
"projects": ["dome-performance"],
|
||||||
|
"include_projects": [],
|
||||||
|
"include_shared": False,
|
||||||
|
},
|
||||||
|
"limit": 2,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert browse.status_code == 200, browse.text
|
assert browse.status_code == 200, browse.text
|
||||||
browse_body = browse.json()
|
browse_body = browse.json()
|
||||||
@@ -182,14 +192,29 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
|||||||
assert browse_body["facets"]["tags"]
|
assert browse_body["facets"]["tags"]
|
||||||
browse_page_two = await http.post(
|
browse_page_two = await http.post(
|
||||||
"/v1/knowledge/browse",
|
"/v1/knowledge/browse",
|
||||||
json={"scope": {"projects": ["dome-performance"], "include_projects": [], "include_shared": False}, "limit": 2, "cursor": browse_body["next_cursor"]},
|
json={
|
||||||
|
"scope": {
|
||||||
|
"projects": ["dome-performance"],
|
||||||
|
"include_projects": [],
|
||||||
|
"include_shared": False,
|
||||||
|
},
|
||||||
|
"limit": 2,
|
||||||
|
"cursor": browse_body["next_cursor"],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert browse_page_two.status_code == 200
|
assert browse_page_two.status_code == 200
|
||||||
assert len(browse_page_two.json()["nodes"]) == 2
|
assert len(browse_page_two.json()["nodes"]) == 2
|
||||||
assert browse_page_two.json()["next_cursor"] is None
|
assert browse_page_two.json()["next_cursor"] is None
|
||||||
filtered_browse = await http.post(
|
filtered_browse = await http.post(
|
||||||
"/v1/knowledge/browse",
|
"/v1/knowledge/browse",
|
||||||
json={"scope": {"projects": ["dome-performance"], "include_projects": [], "include_shared": False}, "node_types": ["problem"]},
|
json={
|
||||||
|
"scope": {
|
||||||
|
"projects": ["dome-performance"],
|
||||||
|
"include_projects": [],
|
||||||
|
"include_shared": False,
|
||||||
|
},
|
||||||
|
"node_types": ["problem"],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert filtered_browse.status_code == 200
|
assert filtered_browse.status_code == 200
|
||||||
assert filtered_browse.json()["filtered_node_count"] == 1
|
assert filtered_browse.json()["filtered_node_count"] == 1
|
||||||
@@ -197,7 +222,13 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
|||||||
assert filtered_browse.json()["project_node_count"] == 4
|
assert filtered_browse.json()["project_node_count"] == 4
|
||||||
graph = await http.post(
|
graph = await http.post(
|
||||||
"/v1/knowledge/graph",
|
"/v1/knowledge/graph",
|
||||||
json={"scope": {"projects": ["dome-performance"], "include_projects": [], "include_shared": False}},
|
json={
|
||||||
|
"scope": {
|
||||||
|
"projects": ["dome-performance"],
|
||||||
|
"include_projects": [],
|
||||||
|
"include_shared": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert graph.status_code == 200, graph.text
|
assert graph.status_code == 200, graph.text
|
||||||
assert len(graph.json()["nodes"]) == 4
|
assert len(graph.json()["nodes"]) == 4
|
||||||
@@ -205,7 +236,14 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
|||||||
assert graph.json()["truncated"] is False
|
assert graph.json()["truncated"] is False
|
||||||
neighborhood = await http.post(
|
neighborhood = await http.post(
|
||||||
"/v1/knowledge/graph",
|
"/v1/knowledge/graph",
|
||||||
json={"scope": {"projects": ["dome-performance"], "include_projects": [], "include_shared": False}, "center_node_id": NODE.node_id},
|
json={
|
||||||
|
"scope": {
|
||||||
|
"projects": ["dome-performance"],
|
||||||
|
"include_projects": [],
|
||||||
|
"include_shared": False,
|
||||||
|
},
|
||||||
|
"center_node_id": NODE.node_id,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert neighborhood.status_code == 200, neighborhood.text
|
assert neighborhood.status_code == 200, neighborhood.text
|
||||||
assert neighborhood.json()["center_node_id"] == NODE.node_id
|
assert neighborhood.json()["center_node_id"] == NODE.node_id
|
||||||
@@ -213,19 +251,41 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
|||||||
assert len(neighborhood.json()["nodes"]) == 3
|
assert len(neighborhood.json()["nodes"]) == 3
|
||||||
center_only = await http.post(
|
center_only = await http.post(
|
||||||
"/v1/knowledge/graph",
|
"/v1/knowledge/graph",
|
||||||
json={"scope": {"projects": ["dome-performance"], "include_projects": [], "include_shared": False}, "center_node_id": NODE.node_id, "max_nodes": 1},
|
json={
|
||||||
|
"scope": {
|
||||||
|
"projects": ["dome-performance"],
|
||||||
|
"include_projects": [],
|
||||||
|
"include_shared": False,
|
||||||
|
},
|
||||||
|
"center_node_id": NODE.node_id,
|
||||||
|
"max_nodes": 1,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert center_only.status_code == 200
|
assert center_only.status_code == 200
|
||||||
assert [item["node_id"] for item in center_only.json()["nodes"]] == [NODE.node_id]
|
assert [item["node_id"] for item in center_only.json()["nodes"]] == [NODE.node_id]
|
||||||
assert center_only.json()["edges"] == []
|
assert center_only.json()["edges"] == []
|
||||||
denied_neighborhood = await http.post(
|
denied_neighborhood = await http.post(
|
||||||
"/v1/knowledge/graph",
|
"/v1/knowledge/graph",
|
||||||
json={"scope": {"projects": ["other-project"], "include_projects": [], "include_shared": False}, "center_node_id": NODE.node_id},
|
json={
|
||||||
|
"scope": {
|
||||||
|
"projects": ["other-project"],
|
||||||
|
"include_projects": [],
|
||||||
|
"include_shared": False,
|
||||||
|
},
|
||||||
|
"center_node_id": NODE.node_id,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert denied_neighborhood.status_code == 404
|
assert denied_neighborhood.status_code == 404
|
||||||
capped_graph = await http.post(
|
capped_graph = await http.post(
|
||||||
"/v1/knowledge/graph",
|
"/v1/knowledge/graph",
|
||||||
json={"scope": {"projects": ["dome-performance"], "include_projects": [], "include_shared": False}, "max_nodes": 1},
|
json={
|
||||||
|
"scope": {
|
||||||
|
"projects": ["dome-performance"],
|
||||||
|
"include_projects": [],
|
||||||
|
"include_shared": False,
|
||||||
|
},
|
||||||
|
"max_nodes": 1,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
assert capped_graph.status_code == 200
|
assert capped_graph.status_code == 200
|
||||||
assert len(capped_graph.json()["nodes"]) == 1
|
assert len(capped_graph.json()["nodes"]) == 1
|
||||||
@@ -245,7 +305,11 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
|||||||
assert source_nodes_resp.status_code == 200, source_nodes_resp.text
|
assert source_nodes_resp.status_code == 200, source_nodes_resp.text
|
||||||
source_nodes_data = source_nodes_resp.json()
|
source_nodes_data = source_nodes_resp.json()
|
||||||
assert source_nodes_data["total"] >= 1
|
assert source_nodes_data["total"] >= 1
|
||||||
assert source_nodes_data["nodes"][0]["relation_basis"] in ("citation", "edge", "both")
|
assert source_nodes_data["nodes"][0]["relation_basis"] in (
|
||||||
|
"citation",
|
||||||
|
"edge",
|
||||||
|
"both",
|
||||||
|
)
|
||||||
|
|
||||||
assert listed_sources.json()["filtered_count"] == 2
|
assert listed_sources.json()["filtered_count"] == 2
|
||||||
assert listed_sources.json()["data"][0]["source"]["revision_id"] == (
|
assert listed_sources.json()["data"][0]["source"]["revision_id"] == (
|
||||||
|
|||||||
@@ -283,7 +283,10 @@ def test_existing_node_can_gain_facets_without_replacing_its_assertion() -> None
|
|||||||
|
|
||||||
existing = NODE
|
existing = NODE
|
||||||
enriched = NODE.model_copy(
|
enriched = NODE.model_copy(
|
||||||
update={"updated": NOW + timedelta(minutes=1), "tags": (*NODE.tags, "interactive-storytelling")}
|
update={
|
||||||
|
"updated": NOW + timedelta(minutes=1),
|
||||||
|
"tags": (*NODE.tags, "interactive-storytelling"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
draft = ProposalRevision(
|
draft = ProposalRevision(
|
||||||
proposal_id="proposal-enrich-facets",
|
proposal_id="proposal-enrich-facets",
|
||||||
|
|||||||
Reference in new issue
Block a user