feat: online verification, node removals, and knowledge browser
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:
vasily committed 2026-10-16 20:02:29 +08:00
1 parent a3156f4e08
commit 78033bb830
83 files changed
+13476 -1051

No files matched your search

+142
View File
@@ -5,6 +5,7 @@ import sys
import threading
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from datetime import timedelta
from pathlib import Path
import pyotp
@@ -154,6 +155,147 @@ def test_acceptance_commits_records_and_retry_returns_original_receipt(tmp_path:
assert error.value.code == "IDEMPOTENCY_CONFLICT"
def test_second_source_enriches_stable_node_and_preserves_history(tmp_path: Path) -> None:
service, repository, artifact_store, source, extraction = _service(tmp_path)
first = service.submit(_proposal(repository, source), actor=ACTOR)
assert first.receipt is not None
source_bytes = b"An independent attendance log records 400 visitors over the 2026 season."
upload = artifact_store.begin(
filename="independent-attendance.md",
media_type="text/markdown",
byte_length=len(source_bytes),
expected_sha256=hashlib.sha256(source_bytes).hexdigest(),
now=103,
)
artifact_store.put_chunk(
upload.upload_id,
offset=0,
content=source_bytes,
chunk_sha256=hashlib.sha256(source_bytes).hexdigest(),
now=104,
)
artifact_store.finalize(upload.upload_id, now=105)
second_source = source.model_copy(
update={
"source_id": "shared:independent-attendance",
"revision_id": "shared:independent-attendance:r1",
"artifact_sha256": hashlib.sha256(source_bytes).hexdigest(),
"byte_length": len(source_bytes),
"title": "Independent seasonal attendance log",
"authors": ("Independent observer",),
}
)
second_extraction = TextMarkdownExtractor().extract(
second_source.revision_id, source_bytes, "text/markdown"
)
block = second_extraction.blocks[0]
quote = block.normalized_text
old_evidence = NODE.claims[0].evidence[0]
new_evidence = old_evidence.model_copy(
update={
"source_revision_id": second_source.revision_id,
"extraction_id": second_extraction.extraction_id,
"quote": quote,
"locator": old_evidence.locator.model_copy(
update={"block_id": block.block_id, "start": 0, "end": len(quote)}
),
}
)
assessment = NODE.claims[0].assessment.model_copy(
update={
"assessment_id": "assessment-independent-attendance",
"evidence_digests": (evidence_digest(old_evidence), evidence_digest(new_evidence)),
"assessor_id": "agent-independent",
"run_id": "run-independent",
"method": "independent source cross-check",
"rationale": "The independently recorded log reports the same count and season.",
}
)
enriched_claim = NODE.claims[0].model_copy(
update={"evidence": (old_evidence, new_evidence), "assessment": assessment}
)
from tests.domain.proposal_fixtures import NOW
enriched = NODE.model_copy(
update={
"updated": NOW + timedelta(minutes=1),
"agent_id": "agent-independent",
"run_id": "run-independent",
"model_id": "fixture-second-agent",
"claims": (enriched_claim,),
"tags": (*NODE.tags, "independent-source-supported"),
}
)
context = ValidationContext(
projects=PROJECTS,
policies=POLICIES,
sources={source.revision_id: source, second_source.revision_id: second_source},
extractions={
extraction.extraction_id: extraction,
second_extraction.extraction_id: second_extraction,
},
existing_nodes={NODE.node_id: NODE},
)
update_service = AcceptanceService(
repository=repository,
artifact_store=artifact_store,
workflow_root=tmp_path / "private-data" / "staging",
context_provider=lambda: context,
actor_is_current=lambda _actor: True,
)
proposal_draft = ProposalRevision(
proposal_id="proposal-second-source-enrichment",
revision=1,
initiating_project_id=NODE.project_id,
scope=Scope(projects=(NODE.project_id,), include_shared=True),
base_commit=repository.head(),
payload_digest="0" * 64,
proposer_id="agent-independent",
run_id="run-independent",
operations=(
ProposalOperation(operation="register_source", source=second_source),
ProposalOperation(
operation="update_node", target_id=NODE.node_id, node=enriched
),
),
)
proposal_update = proposal_draft.model_copy(
update={"payload_digest": proposal_payload_digest(proposal_draft)}
)
result = update_service.submit(
proposal_update,
actor=ActorContext("agent-independent", "agent-key-2"),
)
assert result.receipt is not None, result.validation.findings
assert result.receipt.accepted_node_ids == (NODE.node_id,)
node_path = (
f"projects/{hashlib.sha256(NODE.project_id.encode()).hexdigest()}/nodes/"
f"{hashlib.sha256(NODE.node_id.encode()).hexdigest()}.yaml"
)
old_record = repository.read_file(first.receipt.commit_id, node_path)
new_record = repository.read_file(result.receipt.commit_id, node_path)
assert old_record is not None and new_record is not None
assert safe_yaml_record(old_record, KnowledgeNode) == NODE
accepted = safe_yaml_record(new_record, KnowledgeNode)
assert accepted.node_id == NODE.node_id
assert accepted.claims[0].claim_id == NODE.claims[0].claim_id
assert len(accepted.claims[0].evidence) == 2
assert {item.source_revision_id for item in accepted.claims[0].evidence} == {
source.revision_id,
second_source.revision_id,
}
before_retry = repository.head()
retry = update_service.submit(
proposal_update,
actor=ActorContext("agent-independent", "agent-key-2"),
)
assert retry.receipt == result.receipt
assert repository.head() == before_retry
def test_policy_exception_requires_exact_owner_decision_in_manifest(tmp_path: Path) -> None:
service, repository, _artifact_store, source, extraction = _service(tmp_path)
identities = SQLiteIdentityStore(
+38 -2
View File
@@ -9,12 +9,48 @@ import pytest
import yaml
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.application.registry import LocalContextProvider, LocalProjectRegistry
from gewu.domain.contracts import AutoAcceptPolicy, ProjectRecord, ProjectStatus
from gewu.application.registry import (
MAX_CONTEXT_RECORD_BYTES,
LocalContextProvider,
LocalProjectRegistry,
RegistryError,
)
from gewu.domain.contracts import AutoAcceptPolicy, ExtractionBlock, ProjectRecord, ProjectStatus
from gewu.domain.validation import extraction_digest
from tests.domain.proposal_fixtures import EXTRACTION, NODE, POLICIES, PROJECTS
def test_context_loads_large_normalization_maps_with_a_separate_bound(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
text = "a" * 100_000
block = ExtractionBlock(
block_id="long-block",
ordinal=0,
original_text=text,
normalized_text=text,
normalization_map=tuple((index, index + 1) for index in range(len(text))),
)
candidate = EXTRACTION.model_copy(update={"blocks": (block,)})
extraction = candidate.model_copy(update={"extraction_id": extraction_digest(candidate)})
raw = extraction.model_dump_json().encode()
assert len(raw) > MAX_CONTEXT_RECORD_BYTES
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
repository.publish_files(
expected_head=repository.head(),
files={f"extractions/{extraction.extraction_id}.json": raw},
message="Publish long normalized extraction",
index_directory=tmp_path / "publishing",
)
provider = LocalContextProvider(repository, tmp_path / "registry.json")
assert provider().extractions[extraction.extraction_id] == extraction
monkeypatch.setattr("gewu.application.registry.MAX_CONTEXT_EXTRACTION_BYTES", len(raw) - 1)
with pytest.raises(RegistryError, match="Canonical extraction is unavailable or too large"):
provider()
def test_project_registry_create_and_archive_are_atomic_and_idempotent(tmp_path: Path) -> None:
registry_file = tmp_path / "control" / "registry.json"
registry = LocalProjectRegistry(