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