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,
|
||||
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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"] == (
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user