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

+179 -9
View File
@@ -45,11 +45,22 @@ def _validate(candidate: ProposalRevision) -> ValidationReport:
)
def test_supported_exact_citation_and_both_bundle_policies_are_eligible() -> None:
def test_supported_exact_citation_and_project_policy_are_eligible() -> None:
report = _validate(proposal())
assert report.status == "eligible", report.findings
assert report.eligible_for_auto_accept
assert set(report.affected_policy_ids) == {"dome-policy", "shared"}
assert set(report.affected_policy_ids) == {"dome-policy"}
def test_project_only_source_registration_does_not_write_shared_knowledge() -> None:
candidate = proposal(
node=None,
scope=Scope(projects=("dome-performance",), include_shared=False),
)
report = _validate(candidate)
assert report.status == "eligible", report.findings
assert report.eligible_for_auto_accept
assert set(report.affected_policy_ids) == {"dome-policy"}
def test_proposal_digest_preparation_fills_all_dependent_claim_digests() -> None:
@@ -186,11 +197,7 @@ def test_cross_project_source_bundle_requires_every_affected_policy() -> None:
current_base_commit=BASE_COMMIT,
)
assert eligible.status == "eligible", eligible.findings
assert set(eligible.affected_policy_ids) == {
"dome-policy",
"video-policy",
"shared",
}
assert set(eligible.affected_policy_ids) == {"dome-policy", "video-policy"}
restrictive_video_policy = POLICIES["video-policy"].model_copy(
update={"automatic_source_registration": False}
@@ -269,6 +276,77 @@ def test_node_and_assessor_attribution_cannot_impersonate_another_agent() -> Non
assert "AGENT_ATTRIBUTION_MISMATCH" in {finding.code for finding in node_report.findings}
def test_existing_node_can_gain_facets_without_replacing_its_assertion() -> None:
from datetime import timedelta
from tests.domain.proposal_fixtures import NOW
existing = NODE
enriched = NODE.model_copy(
update={"updated": NOW + timedelta(minutes=1), "tags": (*NODE.tags, "interactive-storytelling")}
)
draft = ProposalRevision(
proposal_id="proposal-enrich-facets",
revision=1,
initiating_project_id="dome-performance",
scope=Scope(projects=("dome-performance",), include_shared=False),
base_commit=BASE_COMMIT,
payload_digest="0" * 64,
proposer_id="agent-pilot",
run_id="run-pilot-2",
operations=(
ProposalOperation(operation="update_node", target_id=NODE.node_id, node=enriched),
),
)
candidate = draft.model_copy(update={"payload_digest": proposal_payload_digest(draft)})
report = validate_proposal(
candidate,
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={NODE.node_id: existing},
current_base_commit=BASE_COMMIT,
)
assert report.status == "eligible", report.findings
def test_existing_node_enrichment_rejects_removed_evidence() -> None:
from datetime import timedelta
from tests.domain.proposal_fixtures import NOW
changed_claim = CLAIM.model_copy(update={"text": "The accepted count was changed."})
enriched = NODE.model_copy(
update={"updated": NOW + timedelta(minutes=1), "claims": (changed_claim,)}
)
draft = ProposalRevision(
proposal_id="proposal-enrich-removal",
revision=1,
initiating_project_id="dome-performance",
scope=Scope(projects=("dome-performance",), include_shared=False),
base_commit=BASE_COMMIT,
payload_digest="0" * 64,
proposer_id="agent-pilot",
run_id="run-pilot-2",
operations=(
ProposalOperation(operation="update_node", target_id=NODE.node_id, node=enriched),
),
)
candidate = draft.model_copy(update={"payload_digest": proposal_payload_digest(draft)})
report = validate_proposal(
candidate,
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={NODE.node_id: NODE},
current_base_commit=BASE_COMMIT,
)
assert report.status == "invalid"
assert "NODE_ENRICHMENT_CHANGED_CLAIM" in {finding.code for finding in report.findings}
def test_entity_name_collision_requires_review_without_creating_a_link() -> None:
entity = NODE.model_copy(
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
@@ -443,6 +521,69 @@ def test_changed_assertion_and_old_assessment_are_rejected() -> None:
assert "ASSESSMENT_STALE" in {finding.code for finding in report.findings}
def test_exact_evidence_quote_does_not_validate_mutated_table_number() -> None:
"""A structurally valid quote cannot establish that a changed claim is true."""
original_text = "The installation hosted 400 visitors during the 2026 season."
mutated_text = "The installation hosted 999 visitors during the 2026 season."
digest = assertion_digest(original_text)
evidence = EVIDENCE.model_copy(
update={
"assertion_path": "claims[0].text",
"assertion_digest": digest,
}
)
claim = CLAIM.model_copy(
update={
"text": original_text,
"evidence": (evidence,),
"assessment": CLAIM.assessment.model_copy(
update={
"assertion_digest": digest,
"evidence_digests": (evidence_digest(evidence),),
}
),
}
)
candidate = proposal(
node=NODE.model_copy(update={"claims": (claim,)}),
source=None,
)
candidate = candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
structurally_valid = _validate(candidate)
mutated_claim = claim.model_copy(
update={
"text": mutated_text,
"evidence": (
evidence.model_copy(update={"assertion_digest": assertion_digest(mutated_text)}),
),
"assessment": claim.assessment.model_copy(
update={
"assertion_digest": assertion_digest(mutated_text),
"evidence_digests": (
evidence_digest(
evidence.model_copy(
update={"assertion_digest": assertion_digest(mutated_text)}
)
),
),
}
),
}
)
mutant = proposal(node=NODE.model_copy(update={"claims": (mutated_claim,)}), source=None)
mutant = mutant.model_copy(update={"payload_digest": proposal_payload_digest(mutant)})
structurally_valid_mutant = _validate(mutant)
assert structurally_valid.status == "eligible", structurally_valid.findings
assert structurally_valid_mutant.status == "needs-review"
assert "NUMERIC_CLAIM_UNGROUNDED" in {
finding.code for finding in structurally_valid_mutant.findings
}
assert evidence.quote == EVIDENCE.quote
assert "999" in mutated_claim.text and "999" not in evidence.quote
def test_assumption_and_limitation_claims_use_the_same_evidence_contract() -> None:
assumption_text = CLAIM.text
assumption_digest = assertion_digest(assumption_text)
@@ -510,12 +651,17 @@ def test_assumption_and_limitation_claims_use_the_same_evidence_contract() -> No
def test_missing_policy_and_explicit_scope_escape_do_not_accept() -> None:
no_shared_policy = {key: value for key, value in POLICIES.items() if key != "shared"}
candidate = proposal()
shared_source = SOURCE_REVISION.model_copy(update={"ingested_for": ("shared",)})
candidate = proposal(
node=None,
source=shared_source,
scope=Scope(projects=("dome-performance",), include_shared=True),
)
report = validate_proposal(
candidate,
projects=PROJECTS,
policies=no_shared_policy,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
sources={},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
current_base_commit=BASE_COMMIT,
@@ -621,6 +767,30 @@ def test_relationship_evidence_is_bound_to_the_edge_and_exact_source_span() -> N
assert "EVIDENCE_ASSERTION_MISMATCH" in {finding.code for finding in invalid_report.findings}
def test_proposal_preparation_accepts_json_edge_evidence_arrays() -> None:
draft = proposal().model_dump(mode="json")
draft.pop("payload_digest")
draft["operations"].append(
{
"operation": "add_edge",
"edge": {
"edge_id": "edge-preparation-test",
"edge_type": "supports",
"source": {"kind": "node", "id": NODE.node_id},
"target": {"kind": "node", "id": "dome:other-node"},
"evidence": [EVIDENCE.model_dump(mode="json")],
},
}
)
prepared = prepare_proposal_digests(draft)
edge = next(item.edge for item in prepared.operations if item.edge is not None)
assert edge is not None
assert edge.evidence[0].assertion_path == "edge"
assert edge.evidence[0].assertion_digest == edge_assertion_digest(edge)
def test_contradiction_bundle_preserves_both_supported_claims() -> None:
import hashlib