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
@@ -0,0 +1,48 @@
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from gewu.adapters.sqlite_node_removals import RemovalConflict, SQLiteNodeRemovalStore
|
||||
|
||||
|
||||
def test_removed_node_is_hidden_until_restored_and_restore_window_is_seven_days(tmp_path) -> None:
|
||||
store = SQLiteNodeRemovalStore(tmp_path / "node-removals.sqlite3")
|
||||
removed = store.request("dome", "node-a", "A node", "editor")
|
||||
|
||||
assert store.is_hidden("dome", "node-a")
|
||||
assert store.list_restorable("dome") == (removed,)
|
||||
restore_window = datetime.fromisoformat(removed.restore_until) - datetime.fromisoformat(
|
||||
removed.requested_at
|
||||
)
|
||||
assert restore_window == timedelta(days=7)
|
||||
|
||||
restored = store.restore("dome", "node-a", "editor")
|
||||
assert restored == removed
|
||||
assert not store.is_hidden("dome", "node-a")
|
||||
assert store.list_restorable("dome") == ()
|
||||
|
||||
|
||||
def test_expired_removal_stays_hidden_and_cannot_be_restored(tmp_path) -> None:
|
||||
store = SQLiteNodeRemovalStore(tmp_path / "node-removals.sqlite3")
|
||||
removed = store.request("dome", "node-a", "A node", "editor")
|
||||
with store._connect() as connection:
|
||||
connection.execute(
|
||||
"UPDATE node_removals SET restore_until=? WHERE removal_id=?",
|
||||
((datetime.now(UTC) - timedelta(seconds=1)).isoformat(), removed.removal_id),
|
||||
)
|
||||
|
||||
assert store.is_hidden("dome", "node-a")
|
||||
assert store.list_restorable("dome") == ()
|
||||
with pytest.raises(RemovalConflict, match="expired"):
|
||||
store.restore("dome", "node-a", "editor")
|
||||
|
||||
|
||||
def test_removal_survives_store_reopen_and_duplicate_removal_is_rejected(tmp_path) -> None:
|
||||
path = tmp_path / "node-removals.sqlite3"
|
||||
store = SQLiteNodeRemovalStore(path)
|
||||
store.request("dome", "node-a", "A node", "editor")
|
||||
reopened = SQLiteNodeRemovalStore(path)
|
||||
|
||||
assert reopened.is_hidden("dome", "node-a")
|
||||
with pytest.raises(RemovalConflict):
|
||||
reopened.request("dome", "node-a", "A node", "editor")
|
||||
@@ -11,6 +11,7 @@ import pytest
|
||||
import yaml
|
||||
|
||||
from gewu.adapters.git_repository import GitCanonicalRepository
|
||||
from gewu.adapters.sqlite_node_removals import SQLiteNodeRemovalStore
|
||||
from gewu.adapters.sqlite_search import SQLiteSearchIndex
|
||||
from gewu.application.identity import Principal
|
||||
from gewu.application.search import CanonicalSearchService, SearchError
|
||||
@@ -210,6 +211,7 @@ def test_assumption_and_limitation_claims_are_searchable_and_cited(tmp_path: Pat
|
||||
query="assumptionspecific",
|
||||
scope=Scope(projects=("dome-performance",), include_shared=False),
|
||||
budget_tokens=50_000,
|
||||
include_evidence=True,
|
||||
),
|
||||
principal=reader,
|
||||
)
|
||||
@@ -700,3 +702,31 @@ def test_neighbor_source_metadata_requires_an_ingested_for_scope(tmp_path: Path)
|
||||
neighbor = cast(dict[str, object], response.data[0]["neighbor"])
|
||||
assert neighbor["kind"] == "source"
|
||||
assert cast(dict[str, object], neighbor["record"])["revision_id"] == SOURCE_REVISION.revision_id
|
||||
|
||||
|
||||
def test_durable_node_removal_hides_from_search_and_returns_after_restore(tmp_path: Path) -> None:
|
||||
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
|
||||
_publish_nodes(repository, tmp_path / "publishing", (NODE,))
|
||||
removals = SQLiteNodeRemovalStore(tmp_path / "node-removals.sqlite3")
|
||||
index = SQLiteSearchIndex(
|
||||
tmp_path / "search.sqlite3", repository, node_removals_path=removals.path
|
||||
)
|
||||
service = CanonicalSearchService(
|
||||
index=index,
|
||||
project_ids_provider=lambda: PROJECTS.keys(),
|
||||
canonical_head=repository.head,
|
||||
)
|
||||
principal = Principal(
|
||||
"editor", "editor-key", frozenset({"editor"}), frozenset({NODE.project_id})
|
||||
)
|
||||
request = SearchRequest(
|
||||
query=NODE.title,
|
||||
scope=Scope(projects=(NODE.project_id,), include_shared=False),
|
||||
budget_tokens=5000,
|
||||
)
|
||||
|
||||
assert service.search(request, principal=principal).data
|
||||
removals.request(NODE.project_id, NODE.node_id, NODE.title, "editor")
|
||||
assert service.search(request, principal=principal).data == ()
|
||||
removals.restore(NODE.project_id, NODE.node_id, "editor")
|
||||
assert service.search(request, principal=principal).data
|
||||
Reference in new issue
Block a user