427 lines
23 KiB
Python
427 lines
23 KiB
Python
import copy
|
|
import json
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from yuqei_sdk.draft_semantic_review import (
|
|
ADVISORY_FIELDS, RELATION_CATEGORIES, DraftSemanticReviewOutput,
|
|
DraftSemanticReviewRequest, assess_draft_semantic_review,
|
|
draft_semantic_review_output_schema,
|
|
)
|
|
|
|
|
|
def sha(value):
|
|
import hashlib
|
|
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True,
|
|
separators=(",", ":"), allow_nan=False)
|
|
return hashlib.sha256(text.encode()).hexdigest()
|
|
|
|
|
|
def request():
|
|
base = "甲方应在十日内书面通知。违约方赔偿实际损失或侵权所得。"
|
|
advisories = {"assumptions": [], "risk_notes": ["正文仅约定赔偿实际损失。"],
|
|
"change_explanations": ["保留责任条款。"], "quality_findings": []}
|
|
candidate = "甲方应在十日内书面通知。违约方赔偿实际损失或侵权所得。"
|
|
return DraftSemanticReviewRequest.model_validate({"schema_version": "draft-semantic-review-request.v1",
|
|
"candidate_id": "candidate-1", "candidate_body_sha256": sha(candidate), "input_revision": 2,
|
|
"base_document": base, "candidate_document": candidate,
|
|
"draft_request_sha256": "1" * 64,
|
|
"facts": [{"field": "通知期限", "role": "body_fact", "value": "十日"}],
|
|
"review_targets": [{"id": "base-0001", "text": base, "start": 0, "end": len(base)}],
|
|
"candidate_review_targets": [{"id": "candidate-0001", "text": candidate, "start": 0, "end": len(candidate)}],
|
|
"selected_clauses": [{"id": "notice", "title": "通知", "source_excerpt": "甲方应在十日内书面通知。"}],
|
|
"not_selected_clause_ids": ["optional"], "pending_clause_ids": [], "conflict_clause_ids": [],
|
|
"drafting_instructions": "不得缩短通知期限。",
|
|
"change_declarations": {"schema_version": "draft-change-declarations.v1", "items": []},
|
|
"verified_change_declaration_ids": [],
|
|
"change_assessment_sha256": "2" * 64, "candidate_advisories": advisories,
|
|
"candidate_advisories_sha256": sha(advisories),
|
|
"template": {"id": "tpl", "content_sha256": "3" * 64, "input_schema_sha256": "4" * 64,
|
|
"input_fields": ["通知期限"]},
|
|
"playbook_sha256": "5" * 64, "prompt_execution_identity_sha256": "6" * 64})
|
|
|
|
|
|
def binding(req):
|
|
return {"candidate_id": req.candidate_id, "candidate_body_sha256": req.candidate_body_sha256,
|
|
"input_revision": req.input_revision, "draft_request_sha256": req.draft_request_sha256,
|
|
"change_assessment_sha256": req.change_assessment_sha256,
|
|
"candidate_advisories_sha256": req.candidate_advisories_sha256,
|
|
"template_id": req.template.id, "template_content_sha256": req.template.content_sha256,
|
|
"input_schema_sha256": req.template.input_schema_sha256, "playbook_sha256": req.playbook_sha256,
|
|
"prompt_execution_identity_sha256": req.prompt_execution_identity_sha256}
|
|
|
|
|
|
def output(req, **updates):
|
|
value = {"schema_version": "draft-semantic-review.v1", "status": "passed",
|
|
"request_binding": binding(req),
|
|
"coverage": {"relation_categories": list(RELATION_CATEGORIES),
|
|
"advisory_fields": list(ADVISORY_FIELDS),
|
|
"target_checks": [{"target_id": "base-0001", "base_excerpt": req.base_document,
|
|
"base_occurrence": None, "candidate_excerpt": req.base_document,
|
|
"candidate_occurrence": None, "finding_ids": []}],
|
|
"candidate_checks": [{"target_id": "candidate-0001",
|
|
"candidate_excerpt": req.candidate_document, "candidate_occurrence": None,
|
|
"finding_ids": []}],
|
|
"advisory_checks": [
|
|
{"field": "risk_notes", "index": 0,
|
|
"exact_excerpt": req.candidate_advisories.risk_notes[0], "metadata_finding_ids": []},
|
|
{"field": "change_explanations", "index": 0,
|
|
"exact_excerpt": req.candidate_advisories.change_explanations[0], "metadata_finding_ids": []}],
|
|
"statement": "逐类检查正文关系与候选说明。"},
|
|
"findings": [], "repair_instructions": [], "metadata_findings": [],
|
|
"limitations": ["模型复核不是法律意见。"]}
|
|
value.update(updates)
|
|
return DraftSemanticReviewOutput.model_validate(value)
|
|
|
|
|
|
def finding(**updates):
|
|
value = {"id": "finding-1", "category": "deadline", "disposition": "preserved", "severity": "info",
|
|
"base_excerpt": "十日内", "base_occurrence": None, "candidate_excerpt": "十日内",
|
|
"candidate_occurrence": None, "fact_fields": ["通知期限"], "clause_ids": ["notice"],
|
|
"change_declaration_ids": [],
|
|
"instruction_excerpt": "不得缩短通知期限", "explanation": "期限被保留。", "question_field": None}
|
|
value.update(updates)
|
|
return value
|
|
|
|
|
|
def test_passed_requires_complete_inspectable_coverage_even_with_no_findings():
|
|
req = request()
|
|
assert assess_draft_semantic_review(req, output(req)).status == "valid"
|
|
out = output(req)
|
|
out.coverage.relation_categories.pop()
|
|
assessment = assess_draft_semantic_review(req, out)
|
|
assert assessment.status == "invalid" and "relation_coverage_incomplete" in assessment.errors
|
|
assert assessment.semantic_correctness_verified is False
|
|
assert assessment.legal_correctness_verified is False
|
|
assert assessment.authorizes_adoption is False
|
|
|
|
|
|
@pytest.mark.parametrize("field", ["candidate_body_sha256", "draft_request_sha256", "candidate_advisories_sha256"])
|
|
def test_forged_request_binding_is_invalid(field):
|
|
req = request(); out = output(req)
|
|
setattr(out.request_binding, field, "f" * 64)
|
|
assert "request_binding_mismatch" in assess_draft_semantic_review(req, out).errors
|
|
|
|
|
|
def test_request_recomputes_candidate_and_advisory_hashes():
|
|
payload = request().model_dump(mode="json")
|
|
payload["candidate_body_sha256"] = "0" * 64
|
|
with pytest.raises(ValidationError): DraftSemanticReviewRequest.model_validate(payload)
|
|
payload = request().model_dump(mode="json")
|
|
payload["candidate_advisories"]["risk_notes"][0] = "篡改"
|
|
with pytest.raises(ValidationError): DraftSemanticReviewRequest.model_validate(payload)
|
|
|
|
|
|
@pytest.mark.parametrize("updates,error", [
|
|
({"fact_fields": ["未知"]}, "fact_reference_invalid"),
|
|
({"clause_ids": ["optional"]}, "clause_reference_invalid"),
|
|
({"instruction_excerpt": "不存在"}, "instruction_excerpt_not_found"),
|
|
])
|
|
def test_unknown_refs_and_instruction_are_invalid(updates, error):
|
|
req = request(); assessment = assess_draft_semantic_review(req, output(req, findings=[finding(**updates)]))
|
|
assert error in assessment.errors
|
|
|
|
|
|
def test_empty_instruction_cannot_be_used_as_evidence():
|
|
req = request(); req.drafting_instructions = ""
|
|
assessment = assess_draft_semantic_review(req, output(req, findings=[finding()]))
|
|
assert "instruction_excerpt_not_found" in assessment.errors
|
|
|
|
|
|
def test_repeated_excerpt_requires_occurrence_and_then_locates_half_open_range():
|
|
req = request(); req.base_document = "十日内;十日内。"
|
|
req.review_targets = [{"id": "base-0001", "text": req.base_document,
|
|
"start": 0, "end": len(req.base_document)}]
|
|
req = DraftSemanticReviewRequest.model_validate(req.model_dump(mode="json"))
|
|
ambiguous = assess_draft_semantic_review(req, output(req, findings=[finding()]))
|
|
assert "base_excerpt_ambiguous" in ambiguous.errors
|
|
resolved = assess_draft_semantic_review(req, output(req, findings=[finding(base_occurrence=2)]))
|
|
assert resolved.findings[0].errors == [] and resolved.findings[0].base_range.start == 4
|
|
|
|
|
|
def test_metadata_index_exact_excerpt_and_candidate_location_are_bound():
|
|
req = request()
|
|
item = {"id": "metadata-1", "field": "risk_notes", "index": 0, "exact_excerpt": "错误概括",
|
|
"candidate_excerpt": "实际损失或侵权所得", "candidate_occurrence": None,
|
|
"disposition": "inaccurate", "explanation": "遗漏替代救济。"}
|
|
assessment = assess_draft_semantic_review(req, output(req, status="blocked",
|
|
findings=[finding(severity="error", disposition="unsupported_change")], metadata_findings=[item]))
|
|
assert "metadata_exact_excerpt_mismatch" in assessment.errors
|
|
|
|
|
|
@pytest.mark.parametrize("status,findings,repairs,error", [
|
|
("passed", [finding(disposition="reversed", severity="error")], [], "passed_status_inconsistent"),
|
|
("needs_information", [], [], "needs_information_without_question"),
|
|
("repairable", [finding(disposition="omitted", severity="error")], [], "repairable_without_complete_instructions"),
|
|
("blocked", [finding()], [], "blocked_without_error_finding"),
|
|
])
|
|
def test_provider_cannot_self_sign_an_inconsistent_status(status, findings, repairs, error):
|
|
req = request()
|
|
assert error in assess_draft_semantic_review(req,
|
|
output(req, status=status, findings=findings, repair_instructions=repairs)).errors
|
|
|
|
|
|
def test_needs_information_question_must_reference_template_field():
|
|
req = request()
|
|
invalid = output(req, status="needs_information", findings=[finding(
|
|
disposition="ambiguous", severity="warning", question_field="未知字段")])
|
|
assessment = assess_draft_semantic_review(req, invalid)
|
|
assert "question_field_invalid" in assessment.errors
|
|
|
|
|
|
def test_provider_schema_is_strict_required_nullable_and_runtime_rejects_invalid_shape():
|
|
schema = draft_semantic_review_output_schema()
|
|
for definition in schema["$defs"].values():
|
|
if "properties" in definition:
|
|
assert set(definition["required"]) == set(definition["properties"])
|
|
assert definition["additionalProperties"] is False
|
|
finding_schema = schema["$defs"]["DraftSemanticFinding"]
|
|
assert any(item.get("type") == "null" for item in finding_schema["properties"]["base_excerpt"]["anyOf"])
|
|
with pytest.raises(ValidationError):
|
|
DraftSemanticReviewOutput.model_validate({"schema_version": "draft-semantic-review.v1", "status": "passed"})
|
|
|
|
|
|
def test_finding_reference_runtime_rejects_case_changed_aliases_without_schema_drift():
|
|
schema = draft_semantic_review_output_schema()
|
|
for definition, field in (
|
|
("DraftSemanticTargetCheck", "finding_ids"),
|
|
("DraftSemanticCandidateCheck", "finding_ids"),
|
|
("DraftSemanticRepairInstruction", "finding_ids"),
|
|
("DraftSemanticAdvisoryCheck", "metadata_finding_ids"),
|
|
("DraftSemanticRepairInstruction", "metadata_finding_ids"),
|
|
):
|
|
assert schema["$defs"][definition]["properties"][field]["items"] == {
|
|
"type": "string"
|
|
}
|
|
|
|
req = request()
|
|
payload = output(req, findings=[finding(id="find-001")]).model_dump(mode="json")
|
|
payload["coverage"]["target_checks"][0]["finding_ids"] = ["F-001"]
|
|
payload["coverage"]["candidate_checks"][0]["finding_ids"] = ["F-001"]
|
|
with pytest.raises(ValidationError):
|
|
DraftSemanticReviewOutput.model_validate(payload)
|
|
|
|
metadata_payload = output(request()).model_dump(mode="json")
|
|
metadata_payload["coverage"]["advisory_checks"][0]["metadata_finding_ids"] = ["M-001"]
|
|
with pytest.raises(ValidationError):
|
|
DraftSemanticReviewOutput.model_validate(metadata_payload)
|
|
|
|
|
|
def test_lowercase_but_unknown_finding_references_remain_fail_closed():
|
|
req = request()
|
|
out = output(req, findings=[finding(id="find-001")])
|
|
out.coverage.target_checks[0].finding_ids = ["find-002"]
|
|
out.coverage.candidate_checks[0].finding_ids = ["find-002"]
|
|
|
|
assessment = assess_draft_semantic_review(req, out)
|
|
|
|
assert assessment.status == "invalid"
|
|
assert "target_check_finding_invalid" in assessment.errors
|
|
assert "candidate_check_finding_invalid" in assessment.errors
|
|
|
|
|
|
def test_exact_case_sensitive_finding_references_remain_valid():
|
|
req = request()
|
|
out = output(req, findings=[finding(id="find-001")])
|
|
out.coverage.target_checks[0].finding_ids = ["find-001"]
|
|
out.coverage.candidate_checks[0].finding_ids = ["find-001"]
|
|
|
|
assessment = assess_draft_semantic_review(req, out)
|
|
|
|
assert assessment.status == "valid"
|
|
assert "target_check_finding_invalid" not in assessment.errors
|
|
assert "candidate_check_finding_invalid" not in assessment.errors
|
|
|
|
|
|
def test_passed_cannot_omit_or_forge_target_and_advisory_item_coverage():
|
|
req = request(); out = output(req)
|
|
out.coverage.target_checks = []
|
|
assert "semantic_review_dto_invalid" in assess_draft_semantic_review(req, out).errors
|
|
out = output(req)
|
|
out.coverage.advisory_checks[0].exact_excerpt = "伪造说明"
|
|
assert "advisory_check_excerpt_invalid" in assess_draft_semantic_review(req, out).errors
|
|
|
|
|
|
def test_request_targets_must_exactly_cover_frozen_base():
|
|
payload = request().model_dump(mode="json")
|
|
payload["review_targets"][0]["end"] -= 1
|
|
with pytest.raises(ValidationError): DraftSemanticReviewRequest.model_validate(payload)
|
|
|
|
|
|
def test_empty_excerpt_and_duplicate_metadata_location_are_rejected():
|
|
with pytest.raises(ValidationError):
|
|
DraftSemanticReviewOutput.model_validate(output(request()).model_dump(mode="json") | {
|
|
"findings": [finding(base_excerpt="")]})
|
|
req = request()
|
|
item = {"id": "metadata-1", "field": "risk_notes", "index": 0, "exact_excerpt": req.candidate_advisories.risk_notes[0],
|
|
"candidate_excerpt": "实际损失", "candidate_occurrence": None,
|
|
"disposition": "inaccurate", "explanation": "概括范围不完整。"}
|
|
assessment = assess_draft_semantic_review(req, output(req, status="blocked",
|
|
metadata_findings=[item, copy.deepcopy(item)]))
|
|
assert "metadata_finding_duplicate" in assessment.errors
|
|
|
|
|
|
def test_passed_rejects_ambiguous_without_question_and_repairs_reject_unknown_ids():
|
|
req = request()
|
|
ambiguous = output(req, findings=[finding(disposition="ambiguous", severity="warning")])
|
|
assert "passed_status_inconsistent" in assess_draft_semantic_review(req, ambiguous).errors
|
|
repair = {"finding_ids": ["unknown"], "metadata_finding_ids": [], "action": "修正", "constraints": ["不新增事实"]}
|
|
blocked = output(req, status="blocked", findings=[finding(severity="error")], repair_instructions=[repair])
|
|
assert "repair_instruction_reference_invalid" in assess_draft_semantic_review(req, blocked).errors
|
|
|
|
|
|
def test_blank_coverage_excerpt_and_zero_length_range_are_rejected():
|
|
req = request(); payload = output(req).model_dump(mode="json")
|
|
payload["coverage"]["target_checks"][0]["candidate_excerpt"] = " "
|
|
with pytest.raises(ValidationError): DraftSemanticReviewOutput.model_validate(payload)
|
|
from yuqei_sdk.draft_semantic_review import DraftSemanticRange
|
|
with pytest.raises(ValidationError): DraftSemanticRange(start=2, end=2)
|
|
|
|
|
|
def test_candidate_side_addition_cannot_be_omitted_from_coverage():
|
|
req = request(); out = output(req)
|
|
out.coverage.candidate_checks = []
|
|
assessment = assess_draft_semantic_review(req, out)
|
|
assert "semantic_review_dto_invalid" in assessment.errors
|
|
|
|
|
|
def test_supported_change_requires_verified_grounded_declaration_and_can_pass():
|
|
req = request()
|
|
addition = "并承担补救责任。"
|
|
req.candidate_document += addition
|
|
req.candidate_body_sha256 = sha(req.candidate_document)
|
|
req.candidate_review_targets[0].text = req.candidate_document
|
|
req.candidate_review_targets[0].end = len(req.candidate_document)
|
|
declaration = {"id": "change-1", "target_id": "base-0001", "operation": "insert",
|
|
"before_excerpt": None, "after_excerpt": addition, "before_occurrence": None,
|
|
"after_occurrence": None, "preceding_anchor": None, "following_anchor": None,
|
|
"basis": [{"type": "user_instruction", "ref": None}], "summary": "按指令补充",
|
|
"pending_confirmation": False}
|
|
from yuqei_sdk.draft_change_evidence import DraftChangeDeclaration
|
|
req.change_declarations.items = [DraftChangeDeclaration.model_validate(declaration)]
|
|
req.verified_change_declaration_ids = ["change-1"]
|
|
req = DraftSemanticReviewRequest.model_validate(req.model_dump(mode="json"))
|
|
supported = finding(disposition="supported_change", candidate_excerpt=addition,
|
|
change_declaration_ids=["change-1"])
|
|
assert assess_draft_semantic_review(req, output(req, findings=[supported])).status == "valid"
|
|
req.drafting_instructions = ""
|
|
assert "supported_change_ungrounded" in assess_draft_semantic_review(
|
|
req, output(req, findings=[supported])).errors
|
|
|
|
|
|
def test_metadata_only_repair_and_cross_advisory_reference_rules():
|
|
req = request()
|
|
metadata = {"id": "meta-risk", "field": "risk_notes", "index": 0,
|
|
"exact_excerpt": req.candidate_advisories.risk_notes[0], "candidate_excerpt": "实际损失",
|
|
"candidate_occurrence": None, "disposition": "inaccurate", "explanation": "概括不完整。"}
|
|
repair = {"finding_ids": [], "metadata_finding_ids": ["meta-risk"],
|
|
"action": "修正风险说明", "constraints": ["不得改变正文"]}
|
|
out = output(req, status="repairable", metadata_findings=[metadata], repair_instructions=[repair])
|
|
out.coverage.advisory_checks[0].metadata_finding_ids = ["meta-risk"]
|
|
assert assess_draft_semantic_review(req, out).status == "valid"
|
|
crossed = copy.deepcopy(out)
|
|
crossed.coverage.advisory_checks[0].metadata_finding_ids = []
|
|
crossed.coverage.advisory_checks[1].metadata_finding_ids = ["meta-risk"]
|
|
assert "advisory_check_finding_unrelated" in assess_draft_semantic_review(req, crossed).errors
|
|
|
|
|
|
def test_explicit_grounded_whole_target_deletion_can_pass_without_candidate_excerpt():
|
|
req = request()
|
|
first, second = "删除此句。", "保留此句。"
|
|
req.base_document, req.candidate_document = first + second, second
|
|
req.candidate_body_sha256 = sha(second)
|
|
req.review_targets = [
|
|
{"id": "base-0001", "text": first, "start": 0, "end": len(first)},
|
|
{"id": "base-0002", "text": second, "start": len(first), "end": len(first + second)},
|
|
]
|
|
req.candidate_review_targets = [
|
|
{"id": "candidate-0001", "text": second, "start": 0, "end": len(second)}]
|
|
from yuqei_sdk.draft_change_evidence import DraftChangeDeclaration
|
|
req.change_declarations.items = [DraftChangeDeclaration.model_validate({
|
|
"id": "delete-1", "target_id": "base-0001", "operation": "delete",
|
|
"before_excerpt": first, "basis": [{"type": "user_instruction"}],
|
|
"summary": "按用户明确要求删除", "pending_confirmation": False})]
|
|
req.verified_change_declaration_ids = ["delete-1"]
|
|
req = DraftSemanticReviewRequest.model_validate(req.model_dump(mode="json"))
|
|
review = output(req, findings=[finding(id="delete-finding", disposition="supported_change",
|
|
category="duty", base_excerpt=first, candidate_excerpt=None, fact_fields=[], clause_ids=[],
|
|
change_declaration_ids=["delete-1"], explanation="用户明确要求删除。")])
|
|
review.coverage.target_checks = [
|
|
{"target_id": "base-0001", "base_excerpt": first, "base_occurrence": None,
|
|
"candidate_excerpt": None, "candidate_occurrence": None, "finding_ids": ["delete-finding"]},
|
|
{"target_id": "base-0002", "base_excerpt": second, "base_occurrence": None,
|
|
"candidate_excerpt": second, "candidate_occurrence": None, "finding_ids": []},
|
|
]
|
|
review.coverage.candidate_checks = [{"target_id": "candidate-0001", "candidate_excerpt": second,
|
|
"candidate_occurrence": None, "finding_ids": []}]
|
|
review = DraftSemanticReviewOutput.model_validate(review.model_dump(mode="json"))
|
|
assert assess_draft_semantic_review(req, review).status == "valid"
|
|
|
|
|
|
def test_unknown_or_pending_declaration_reference_is_stable_invalid():
|
|
req = request()
|
|
unknown = output(req, findings=[finding(disposition="supported_change",
|
|
change_declaration_ids=["unknown"])])
|
|
assessment = assess_draft_semantic_review(req, unknown)
|
|
assert assessment.status == "invalid"
|
|
assert "change_declaration_reference_invalid" in assessment.errors
|
|
from yuqei_sdk.draft_change_evidence import DraftChangeDeclaration
|
|
req.change_declarations.items = [DraftChangeDeclaration.model_validate({
|
|
"id": "pending-1", "target_id": "base-0001", "operation": "insert",
|
|
"after_excerpt": "侵权所得", "basis": [{"type": "user_instruction"}],
|
|
"summary": "待确认", "pending_confirmation": True})]
|
|
req.verified_change_declaration_ids = ["pending-1"]
|
|
req = DraftSemanticReviewRequest.model_validate(req.model_dump(mode="json"))
|
|
pending = output(req, findings=[finding(disposition="supported_change",
|
|
change_declaration_ids=["pending-1"])])
|
|
assert "supported_change_ungrounded" in assess_draft_semantic_review(req, pending).errors
|
|
|
|
|
|
def test_actual_candidate_diff_needs_related_finding_but_synonymous_rewrite_can_be_preserved():
|
|
req = request()
|
|
req.candidate_document += "新增内容。"
|
|
req.candidate_body_sha256 = sha(req.candidate_document)
|
|
req.candidate_review_targets[0].text = req.candidate_document
|
|
req.candidate_review_targets[0].end = len(req.candidate_document)
|
|
req = DraftSemanticReviewRequest.model_validate(req.model_dump(mode="json"))
|
|
assessment = assess_draft_semantic_review(req, output(req))
|
|
assert "candidate_diff_after_unreviewed" in assessment.errors
|
|
|
|
req = request()
|
|
req.base_document, req.candidate_document = "甲方应书面通知。", "甲方须书面通知。"
|
|
req.candidate_body_sha256 = sha(req.candidate_document)
|
|
req.review_targets = [{"id": "base-0001", "text": req.base_document,
|
|
"start": 0, "end": len(req.base_document)}]
|
|
req.candidate_review_targets = [{"id": "candidate-0001", "text": req.candidate_document,
|
|
"start": 0, "end": len(req.candidate_document)}]
|
|
req = DraftSemanticReviewRequest.model_validate(req.model_dump(mode="json"))
|
|
preserved = finding(disposition="preserved", base_excerpt="应", candidate_excerpt="须",
|
|
fact_fields=[], clause_ids=[], instruction_excerpt=None)
|
|
review = output(req, findings=[preserved])
|
|
review.coverage.target_checks[0].candidate_excerpt = req.candidate_document
|
|
assert assess_draft_semantic_review(req, review).status == "valid"
|
|
|
|
|
|
def test_one_character_finding_does_not_cover_long_replace_but_two_fragments_can_union():
|
|
req = request()
|
|
req.base_document, req.candidate_document = "甲乙丙丁", "甲XY丁"
|
|
req.candidate_body_sha256 = sha(req.candidate_document)
|
|
req.review_targets = [{"id": "base-0001", "text": req.base_document,
|
|
"start": 0, "end": len(req.base_document)}]
|
|
req.candidate_review_targets = [{"id": "candidate-0001", "text": req.candidate_document,
|
|
"start": 0, "end": len(req.candidate_document)}]
|
|
req = DraftSemanticReviewRequest.model_validate(req.model_dump(mode="json"))
|
|
first = finding(id="first", disposition="preserved", base_excerpt="乙", candidate_excerpt="X",
|
|
fact_fields=[], clause_ids=[], instruction_excerpt=None)
|
|
review = output(req, findings=[first])
|
|
review.coverage.target_checks[0].candidate_excerpt = req.candidate_document
|
|
assessment = assess_draft_semantic_review(req, review)
|
|
assert "candidate_diff_before_unreviewed" in assessment.errors
|
|
assert "candidate_diff_after_unreviewed" in assessment.errors
|
|
second = finding(id="second", disposition="preserved", base_excerpt="丙", candidate_excerpt="Y",
|
|
fact_fields=[], clause_ids=[], instruction_excerpt=None)
|
|
review = output(req, findings=[first, second])
|
|
review.coverage.target_checks[0].candidate_excerpt = req.candidate_document
|
|
assert assess_draft_semantic_review(req, review).status == "valid"
|