diff --git a/agents/gewu b/agents/gewu index 3ef2c5c..511e74d 160000 --- a/agents/gewu +++ b/agents/gewu @@ -1 +1 @@ -Subproject commit 3ef2c5cc31c138cbd733f90f97c3539b86d154cf +Subproject commit 511e74d1a3cf08f321203ec63c9fe74d9c9d0525 diff --git a/scripts/export_schemas.py b/scripts/export_schemas.py index c086931..cdfc2d5 100644 --- a/scripts/export_schemas.py +++ b/scripts/export_schemas.py @@ -13,6 +13,7 @@ from gewu.domain.contracts import ( IngestReviewItem, KnowledgeNode, NeighborRequest, + OnlineEvidence, ProjectRecord, ProposalReceipt, ProposalRevision, @@ -26,7 +27,6 @@ from gewu.domain.contracts import ( ServiceDiagnostics, SourceRevisionRecord, TypedEdge, - OnlineEvidence, ) ROOT = Path(__file__).resolve().parents[1] diff --git a/src/gewu/adapters/sqlite_search.py b/src/gewu/adapters/sqlite_search.py index 2c94566..01dbc04 100644 --- a/src/gewu/adapters/sqlite_search.py +++ b/src/gewu/adapters/sqlite_search.py @@ -19,12 +19,12 @@ from gewu.application.search import ( BrowseIndexResult, GraphIndexResult, IndexedNode, - SourceRelatedNodeHit, - SourceNodesIndexResult, SearchEdgeHit, SearchEdgeResult, SearchIndexResult, SourceIndexResult, + SourceNodesIndexResult, + SourceRelatedNodeHit, ) from gewu.application.serialization import safe_yaml_record from gewu.domain.contracts import Confidence, KnowledgeNode, SourceRevisionRecord, TypedEdge @@ -113,7 +113,8 @@ class SQLiteSearchIndex: ) connection.execute( "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))" ) connection.execute( @@ -700,7 +701,13 @@ class SQLiteSearchIndex: project_scope = tuple(sorted(set(project_ids) | ({"shared"} if include_shared else set()))) if not project_scope: 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 = ( @@ -766,17 +773,37 @@ class SQLiteSearchIndex: match = self._match_expression(query) 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": - 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": - 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": - 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: order_by = "score ASC, indexed_nodes.node_id ASC" 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: with closing(self._connect()) as connection: @@ -794,7 +821,8 @@ class SQLiteSearchIndex: if project_ids: p_holders = ", ".join("?" for _ in project_ids) 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, ).fetchone() project_node_count = int(p_row[0]) if p_row else 0 @@ -803,7 +831,8 @@ class SQLiteSearchIndex: shared_node_count = 0 if include_shared: 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() shared_node_count = int(s_row[0]) if s_row else 0 @@ -825,7 +854,8 @@ class SQLiteSearchIndex: if match: query_stmt = ( "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 ?" ) node_rows = connection.execute( @@ -852,7 +882,8 @@ class SQLiteSearchIndex: for field in ("tags", "audience", "use_for", "domain"): facet_stmt = ( "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} " "GROUP BY facet.value" ) @@ -884,7 +915,7 @@ class SQLiteSearchIndex: limit: int = 20, offset: int = 0, ) -> SourceNodesIndexResult | None: - revision = self._rebuild_if_needed() + self._rebuild_if_needed() project_scope = tuple(sorted(set(project_ids) | ({"shared"} if include_shared else set()))) if not project_scope: return None @@ -904,7 +935,8 @@ class SQLiteSearchIndex: # Check source revision grant 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), ).fetchone() if grant_row is None: @@ -969,16 +1001,21 @@ class SQLiteSearchIndex: count_row = connection.execute(count_stmt, (*filter_params, match)).fetchone() else: 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() total = int(count_row[0]) if count_row else 0 # 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: 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"{where_clause} AND node_fts MATCH ? ORDER BY {order_by} LIMIT ? OFFSET ?" ) @@ -987,8 +1024,10 @@ class SQLiteSearchIndex: ).fetchall() else: row_stmt = ( - f"{cte} SELECT indexed_nodes.node_json, combined.relation_basis FROM combined " - f"JOIN indexed_nodes USING(node_id) {where_clause} ORDER BY {order_by} LIMIT ? OFFSET ?" + f"{cte} SELECT indexed_nodes.node_json, " + 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( row_stmt, (*filter_params, limit + 1, offset) diff --git a/src/gewu/application/search.py b/src/gewu/application/search.py index c66492e..ce8950d 100644 --- a/src/gewu/application/search.py +++ b/src/gewu/application/search.py @@ -21,18 +21,18 @@ from gewu.domain.contracts import ( EdgeEndpoint, EvidenceReference, Finding, - KnowledgeNode, - KnowledgeBrowseRequest, KnowledgeBrowseEnvelope, + KnowledgeBrowseRequest, KnowledgeGraphEnvelope, KnowledgeGraphRequest, - SourceRelatedNode, - SourceRelatedNodesEnvelope, + KnowledgeNode, NeighborRequest, ReadEnvelope, Scope, SearchRequest, SourceListRequest, + SourceRelatedNode, + SourceRelatedNodesEnvelope, SourceRevisionRecord, TypedEdge, ) @@ -165,7 +165,9 @@ class SearchIndex(Protocol): 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( self, @@ -616,8 +618,15 @@ class HybridSearchIndex: 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) - def graph(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 graph( + 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: return self.lexical.browse(**kwargs) @@ -709,7 +718,9 @@ class CanonicalSearchService: state = "current" if index_revision == canonical_revision else "lagging" 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) fingerprint = hashlib.sha256(canonical_json({ "projects": project_ids, @@ -723,7 +734,9 @@ class CanonicalSearchService: "to": request.as_of_to.isoformat() if request.as_of_to else None, "stale": request.stale_only, "order": request.order, "limit": request.limit, })).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 result = lexical.browse( query=request.query, @@ -761,7 +774,12 @@ class CanonicalSearchService: canonical_revision = self.canonical_head() warnings: tuple[Finding, ...] = () 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( request_id="browse-" + secrets.token_hex(12), resolved_scope=Scope(projects=project_ids, include_shared=request.scope.include_shared), @@ -824,7 +842,12 @@ class CanonicalSearchService: canonical_revision = self.canonical_head() warnings: tuple[Finding, ...] = () 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( request_id="srcnodes-" + secrets.token_hex(12), 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) if request.center_node_id is None: result = self.index.graph( @@ -995,7 +1020,8 @@ class CanonicalSearchService: if request.max_nodes > 1: neighbors = self.index.neighbors( 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: endpoint = hit.neighbor_endpoint @@ -1007,12 +1033,20 @@ class CanonicalSearchService: index_revision=neighbors.index_revision if neighbors else scoped.index_revision, nodes=tuple(nodes_by_id.values()), 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() warnings: tuple[Finding, ...] = () 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( request_id="graph-" + secrets.token_hex(12), resolved_scope=Scope(projects=project_ids, include_shared=request.scope.include_shared), diff --git a/src/gewu/bootstrap/app.py b/src/gewu/bootstrap/app.py index dbc5bec..2573da0 100644 --- a/src/gewu/bootstrap/app.py +++ b/src/gewu/bootstrap/app.py @@ -50,11 +50,11 @@ from gewu.domain.contracts import ( GitCommitId, KnowledgeBrowseEnvelope, KnowledgeGraphEnvelope, - SourceRelatedNodesEnvelope, ProjectRecord, ReadEnvelope, RecordId, ServiceDiagnostics, + SourceRelatedNodesEnvelope, ) from gewu.transports.accounts_http import register_account_routes from gewu.transports.admin_http import register_admin_routes diff --git a/src/gewu/domain/contracts.py b/src/gewu/domain/contracts.py index cc5279e..6273378 100644 --- a/src/gewu/domain/contracts.py +++ b/src/gewu/domain/contracts.py @@ -490,7 +490,12 @@ class OnlineEvidence(StrictRecord): def public_http_url(cls, value: str) -> str: parsed = urlsplit(value) 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") if hostname.lower() == "localhost" or hostname.lower().endswith((".localhost", ".local")): raise ValueError("web references must use a public hostname") @@ -830,14 +835,20 @@ class KnowledgeBrowseRequest(StrictRecord): as_of_from: date | None = None as_of_to: date | None = None 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) cursor: str | None = Field(default=None, max_length=512) budget_tokens: int = Field(ge=256, le=50_000) @model_validator(mode="after") 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") if self.order == "relevance" and not self.query: raise ValueError("relevance ordering requires a search query") diff --git a/src/gewu/domain/validation.py b/src/gewu/domain/validation.py index 0e830a4..e0eee81 100644 --- a/src/gewu/domain/validation.py +++ b/src/gewu/domain/validation.py @@ -211,7 +211,11 @@ def _validate_node_enrichment( for field in ("node_id", "project_id", "node_type", "created", "lifecycle"): if getattr(previous, field) != getattr(updated, field): 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: findings.append( @@ -230,7 +234,11 @@ def _validate_node_enrichment( new_items = {claim.claim_id: claim for claim in getattr(updated, field)} if not old_items.keys() <= new_items.keys(): 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 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 ): 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 if not set(old_claim.online_evidence) <= set(new_claim.online_evidence): 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) if not old_evidence <= set(new_claim.evidence): 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( - _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): - 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"): 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) @@ -378,11 +417,17 @@ def validate_proposal( elif operation.operation == "update_node": if previous_node is None: 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: 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": possible_duplicates = [ @@ -444,7 +489,10 @@ def validate_proposal( and old_claim.evidence == claim.evidence 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( _finding( "ASSESSOR_ATTRIBUTION_MISMATCH", @@ -873,7 +921,6 @@ def validate_proposal( "EDGE_SOURCE_MISSING", "EDGE_PROJECT_MISSING", "EDGE_TYPE_MISMATCH", - "NODE_NOT_FOUND", "POLICY_WEAKENING", "OUT_OF_SCOPE", } diff --git a/src/gewu/transports/http_models.py b/src/gewu/transports/http_models.py index 6d7e1fd..f1577a4 100644 --- a/src/gewu/transports/http_models.py +++ b/src/gewu/transports/http_models.py @@ -10,9 +10,9 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida from gewu.domain.contracts import ( AutoAcceptPolicy, Confidence, - NeighborRequest, KnowledgeBrowseRequest, KnowledgeGraphRequest, + NeighborRequest, NodeType, ProjectRecord, RecordId, diff --git a/src/gewu/transports/mcp_server.py b/src/gewu/transports/mcp_server.py index f65b4d1..8ec97ff 100644 --- a/src/gewu/transports/mcp_server.py +++ b/src/gewu/transports/mcp_server.py @@ -528,7 +528,10 @@ def create_mcp_server( stale_only: bool = False, include_evidence: bool = True, ) -> 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) try: diff --git a/tests/adapters/test_sqlite_ingest_reviews.py b/tests/adapters/test_sqlite_ingest_reviews.py index 5c0fb40..2e2b2f3 100644 --- a/tests/adapters/test_sqlite_ingest_reviews.py +++ b/tests/adapters/test_sqlite_ingest_reviews.py @@ -31,7 +31,10 @@ def test_review_store_round_trips_jev_facet_arrays_and_idempotent_create(tmp_pat { "facet": "tags", "value": "spatial sound", - "definition": "Audio techniques that place sound in immersive spatial configurations.", + "definition": ( + "Audio techniques that place sound in immersive " + "spatial configurations." + ), }, ), ) diff --git a/tests/api/test_search_api.py b/tests/api/test_search_api.py index 7f13402..8de655e 100644 --- a/tests/api/test_search_api.py +++ b/tests/api/test_search_api.py @@ -165,10 +165,20 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None full_row = full_result.json()["data"][0] assert full_row["evidence_included"] is True 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( "/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 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"] browse_page_two = await http.post( "/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 len(browse_page_two.json()["nodes"]) == 2 assert browse_page_two.json()["next_cursor"] is None filtered_browse = await http.post( "/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.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 graph = await http.post( "/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 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 neighborhood = await http.post( "/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.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 center_only = await http.post( "/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 [item["node_id"] for item in center_only.json()["nodes"]] == [NODE.node_id] assert center_only.json()["edges"] == [] denied_neighborhood = await http.post( "/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 capped_graph = await http.post( "/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 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 source_nodes_data = source_nodes_resp.json() 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()["data"][0]["source"]["revision_id"] == ( diff --git a/tests/domain/test_validation.py b/tests/domain/test_validation.py index 158b521..3f6301a 100644 --- a/tests/domain/test_validation.py +++ b/tests/domain/test_validation.py @@ -283,7 +283,10 @@ def test_existing_node_can_gain_facets_without_replacing_its_assertion() -> None existing = NODE 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( proposal_id="proposal-enrich-facets",