feat: online verification, node removals, and knowledge browser
CI / quality (push) Canceled after 0s
CI / quality (push) Canceled after 0s
Adds claim-bound online evidence records and citation verification scripts, node removal transport/adapter, retrieval benchmark and owner-review tooling, GraphCanvas/SourceDetail knowledge browser UI, and the ingest handoff helper. Bumps the agents/gewu pack to the online-verification branch.
This commit is contained in:
1 parent
a3156f4e08
commit
78033bb830
83 files changed
+13476
-1051
No files matched your search
@@ -40,6 +40,7 @@ def test_local_factory_composes_private_persistent_read_service(
|
||||
"/health",
|
||||
"/v1/diagnostics",
|
||||
"/v1/admin/projects",
|
||||
"/v1/admin/projects/{project_id}",
|
||||
"/v1/admin/credentials",
|
||||
"/v1/admin/credentials/{credential_id}",
|
||||
"/v1/admin/events",
|
||||
@@ -56,16 +57,26 @@ def test_local_factory_composes_private_persistent_read_service(
|
||||
"/v1/projects/{project_id}/sources/{revision_id}",
|
||||
"/v1/projects/{project_id}/sources/{revision_id}/artifact",
|
||||
"/v1/projects/{project_id}/sources/{revision_id}/extractions/{extraction_id}/blocks/{block_id}",
|
||||
"/v1/projects/{project_id}/sources/{revision_id}/nodes",
|
||||
"/v1/neighbors",
|
||||
"/v1/projects",
|
||||
"/v1/search",
|
||||
"/v1/knowledge/browse",
|
||||
"/v1/knowledge/graph",
|
||||
"/v1/sources",
|
||||
"/v1/session",
|
||||
"/auth/owner-session",
|
||||
"/v1/ingest-reviews",
|
||||
"/v1/ingest-reviews/{review_id}/decision",
|
||||
"/v1/projects/{project_id}/node-removals",
|
||||
"/v1/projects/{project_id}/nodes/{node_id}/removal",
|
||||
"/v1/projects/{project_id}/nodes/{node_id}/restore-removal",
|
||||
}
|
||||
assert (data_root / "knowledge.git" / "HEAD").is_file()
|
||||
assert (data_root / "identity.sqlite3").is_file()
|
||||
assert (data_root / "search.sqlite3").is_file()
|
||||
assert (data_root / "node-removals.sqlite3").is_file()
|
||||
assert (data_root / "node-removals.sqlite3").stat().st_mode & 0o077 == 0
|
||||
assert (data_root / "search.sqlite3").stat().st_mode & 0o077 == 0
|
||||
assert (data_root.stat().st_mode & 0o777) == 0o700
|
||||
|
||||
|
||||
@@ -257,9 +257,7 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
{"proposal_json": prepared_payload["proposal_json"][:-1]},
|
||||
)
|
||||
assert not truncated_evaluation.is_error, truncated_evaluation.content
|
||||
truncated_evaluation_payload = json.loads(
|
||||
_text_payload(truncated_evaluation)
|
||||
)
|
||||
truncated_evaluation_payload = json.loads(_text_payload(truncated_evaluation))
|
||||
assert truncated_evaluation_payload["proposal_id"] == (
|
||||
prepared_proposal.proposal_id
|
||||
)
|
||||
@@ -277,9 +275,7 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
"proposal_evaluate", {"proposal_json": '{"schema_version":'}
|
||||
)
|
||||
assert not malformed_evaluation.is_error, malformed_evaluation.content
|
||||
malformed_evaluation_payload = json.loads(
|
||||
_text_payload(malformed_evaluation)
|
||||
)
|
||||
malformed_evaluation_payload = json.loads(_text_payload(malformed_evaluation))
|
||||
assert malformed_evaluation_payload["status"] == "invalid"
|
||||
assert malformed_evaluation_payload["findings"][0]["code"] == (
|
||||
"PROPOSAL_JSON_INVALID"
|
||||
@@ -298,8 +294,7 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
invalid_payload = json.loads(_text_payload(invalid_result))
|
||||
assert invalid_payload["status"] == "invalid"
|
||||
assert any(
|
||||
issue["path"].endswith("locator")
|
||||
and "PDF locators" in issue["message"]
|
||||
issue["path"].endswith("locator") and "PDF locators" in issue["message"]
|
||||
for issue in invalid_payload["issues"]
|
||||
)
|
||||
invalid_review = await mcp.call_tool(
|
||||
@@ -313,6 +308,41 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
"extraction_id",
|
||||
"relevance_rationale",
|
||||
}
|
||||
project_only_token = identities.provision(
|
||||
credential_id="project-only-source-agent-key",
|
||||
principal_id="agent-project-only",
|
||||
roles=("agent",),
|
||||
project_ids=("dome-performance",),
|
||||
)
|
||||
project_only_draft = json.loads(json.dumps(draft))
|
||||
project_only_draft["proposal_id"] = "proposal-project-only-source"
|
||||
project_only_draft["scope"]["include_shared"] = False
|
||||
project_only_draft["proposer_id"] = "agent-project-only"
|
||||
project_only_draft["operations"] = [project_only_draft["operations"][0]]
|
||||
async with httpx2.AsyncClient(
|
||||
transport=httpx2.ASGITransport(app=app),
|
||||
base_url="http://localhost",
|
||||
headers={"Authorization": f"Bearer {project_only_token}"},
|
||||
) as project_only_http:
|
||||
async with Client(
|
||||
streamable_http_client(
|
||||
"http://localhost/mcp", http_client=project_only_http
|
||||
)
|
||||
) as project_only_mcp:
|
||||
project_source = await project_only_mcp.call_tool(
|
||||
"proposal_prepare",
|
||||
{"proposal_json": json.dumps(project_only_draft)},
|
||||
)
|
||||
assert not project_source.is_error, project_source.content
|
||||
assert json.loads(_text_payload(project_source))["status"] == "prepared"
|
||||
shared_draft = json.loads(json.dumps(project_only_draft))
|
||||
shared_draft["proposal_id"] = "proposal-project-only-shared-source"
|
||||
shared_draft["operations"][0]["source"]["ingested_for"] = ["shared"]
|
||||
shared_source = await project_only_mcp.call_tool(
|
||||
"proposal_prepare",
|
||||
{"proposal_json": json.dumps(shared_draft)},
|
||||
)
|
||||
assert shared_source.is_error
|
||||
denied_lens = await mcp.call_tool(
|
||||
"get_project_context", {"project_id": "interactive-video"}
|
||||
)
|
||||
@@ -387,6 +417,11 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
}
|
||||
],
|
||||
}
|
||||
invalid_run = await mcp.call_tool("run_create", {"request_json": "{}"})
|
||||
assert not invalid_run.is_error
|
||||
invalid_run_result = json.loads(_text_payload(invalid_run))
|
||||
assert invalid_run_result["status"] == "invalid"
|
||||
assert "run_id" in {issue["path"] for issue in invalid_run_result["issues"]}
|
||||
created_run = await mcp.call_tool(
|
||||
"run_create", {"request_json": json.dumps(run_request)}
|
||||
)
|
||||
@@ -403,6 +438,23 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
)
|
||||
assert not saved_run.is_error
|
||||
assert json.loads(_text_payload(saved_run))["revision"] == 1
|
||||
invalid_checkpoint = await mcp.call_tool(
|
||||
"run_checkpoint",
|
||||
{
|
||||
"request_json": json.dumps(
|
||||
{
|
||||
**checkpoint,
|
||||
"expected_revision": 1,
|
||||
"coverage": {"skipped": ["pending source"]},
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
assert not invalid_checkpoint.is_error
|
||||
invalid_result = json.loads(_text_payload(invalid_checkpoint))
|
||||
assert invalid_result["status"] == "invalid"
|
||||
assert invalid_result["issues"][0]["path"] == "coverage.skipped[0]"
|
||||
assert "pending source" not in _text_payload(invalid_checkpoint)
|
||||
read_run = await mcp.call_tool("run_status", {"run_id": "mcp-run-pilot"})
|
||||
assert not read_run.is_error
|
||||
assert json.loads(_text_payload(read_run))["revision"] == 1
|
||||
@@ -647,6 +699,7 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
)
|
||||
assert not html_block_result.is_error
|
||||
html_block = json.loads(_text_payload(html_block_result))
|
||||
assert html_block["normalization_profile"] == "unicode-nfc-lf-v1"
|
||||
assert "Exact HTML statement." in html_block["normalized_text"]
|
||||
assert "original_text" not in html_block
|
||||
assert "normalization_map" not in html_block
|
||||
@@ -663,6 +716,9 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
)
|
||||
assert not mapped_html_result.is_error
|
||||
mapped_html = json.loads(_text_payload(mapped_html_result))
|
||||
assert (
|
||||
mapped_html["normalization_profile"] == html_block["normalization_profile"]
|
||||
)
|
||||
assert mapped_html["original_text"]
|
||||
assert len(mapped_html["normalization_map"]) == len(
|
||||
mapped_html["normalized_text"]
|
||||
@@ -705,6 +761,7 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
)
|
||||
assert not pdf_block_result.is_error
|
||||
pdf_block = json.loads(_text_payload(pdf_block_result))
|
||||
assert pdf_block["normalization_profile"] == "unicode-nfc-lf-v1"
|
||||
assert pdf_block["page"] == 1
|
||||
assert "Archived PDF evidence." in pdf_block["normalized_text"]
|
||||
assert "normalization_map" not in pdf_block
|
||||
|
||||
@@ -127,7 +127,7 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
||||
unbuilt_state = diagnostic_unbuilt.json()
|
||||
assert unbuilt_state["index_state"] == "not_built"
|
||||
assert unbuilt_state["index_revision"] is None
|
||||
assert unbuilt_state["index_schema_version"] == 4
|
||||
assert unbuilt_state["index_schema_version"] == 5
|
||||
result = await http.post(
|
||||
"/v1/search",
|
||||
json={
|
||||
@@ -138,6 +138,7 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
||||
},
|
||||
"limit": 10,
|
||||
"budget_tokens": 5000,
|
||||
"include_evidence": False,
|
||||
},
|
||||
)
|
||||
assert result.status_code == 200, result.text
|
||||
@@ -145,7 +146,90 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
||||
assert envelope["retrieval_mode"] == "keyword"
|
||||
assert envelope["index_revision"] == repository.head()
|
||||
assert envelope["data"][0]["node"]["node_id"] == NODE.node_id
|
||||
assert envelope["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
|
||||
assert envelope["data"][0]["evidence_included"] is False
|
||||
compact_claim = envelope["data"][0]["node"]["claims"][0]
|
||||
assert "evidence" not in compact_claim
|
||||
assert compact_claim["evidence_count"] == len(NODE.claims[0].evidence)
|
||||
assert envelope["citations"] == []
|
||||
full_result = await http.post(
|
||||
"/v1/search",
|
||||
json={
|
||||
"query": "400 visitors",
|
||||
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
||||
"limit": 10,
|
||||
"budget_tokens": 5000,
|
||||
"include_evidence": True,
|
||||
},
|
||||
)
|
||||
assert full_result.status_code == 200, full_result.text
|
||||
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
|
||||
browse = await http.post(
|
||||
"/v1/knowledge/browse",
|
||||
json={"scope": {"projects": ["dome-performance"], "include_projects": [], "include_shared": False}, "limit": 2},
|
||||
)
|
||||
assert browse.status_code == 200, browse.text
|
||||
browse_body = browse.json()
|
||||
assert browse_body["project_node_count"] == 4
|
||||
assert browse_body["filtered_node_count"] == 4
|
||||
assert len(browse_body["nodes"]) == 2
|
||||
assert browse_body["next_cursor"]
|
||||
assert [item["title"] for item in browse_body["nodes"]] == sorted(
|
||||
item["title"] for item in browse_body["nodes"]
|
||||
)
|
||||
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"]},
|
||||
)
|
||||
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"]},
|
||||
)
|
||||
assert filtered_browse.status_code == 200
|
||||
assert filtered_browse.json()["filtered_node_count"] == 1
|
||||
assert filtered_browse.json()["nodes"][0]["node_type"] == "problem"
|
||||
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}},
|
||||
)
|
||||
assert graph.status_code == 200, graph.text
|
||||
assert len(graph.json()["nodes"]) == 4
|
||||
assert len(graph.json()["edges"]) == 2
|
||||
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},
|
||||
)
|
||||
assert neighborhood.status_code == 200, neighborhood.text
|
||||
assert neighborhood.json()["center_node_id"] == NODE.node_id
|
||||
assert NODE.node_id in {item["node_id"] for item in neighborhood.json()["nodes"]}
|
||||
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},
|
||||
)
|
||||
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},
|
||||
)
|
||||
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},
|
||||
)
|
||||
assert capped_graph.status_code == 200
|
||||
assert len(capped_graph.json()["nodes"]) == 1
|
||||
assert capped_graph.json()["truncated"] is True
|
||||
listed_sources = await http.post(
|
||||
"/v1/sources",
|
||||
json={
|
||||
@@ -155,6 +239,15 @@ def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None
|
||||
},
|
||||
)
|
||||
assert listed_sources.status_code == 200, listed_sources.text
|
||||
source_nodes_resp = await http.get(
|
||||
f"/v1/projects/dome-performance/sources/{SOURCE_REVISION.revision_id}/nodes"
|
||||
)
|
||||
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 listed_sources.json()["filtered_count"] == 2
|
||||
assert listed_sources.json()["data"][0]["source"]["revision_id"] == (
|
||||
SOURCE_REVISION.revision_id
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user