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
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user