release: freeze RC-05-RC-08 closeout candidate
This commit is contained in:
@@ -7,9 +7,10 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any, Literal
|
||||
import re
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_serializer, model_validator
|
||||
from pydantic import AfterValidator, BaseModel, ConfigDict, Field, model_serializer, model_validator
|
||||
|
||||
from .contract_drafting import DraftQualityFinding
|
||||
from .draft_change_evidence import DraftChangeDeclarations, _diff_ranges
|
||||
@@ -17,6 +18,23 @@ from .draft_change_evidence import DraftChangeDeclarations, _diff_ranges
|
||||
DRAFT_SEMANTIC_REVIEW_POLICY = "draft-semantic-review.v1"
|
||||
RELATION_CATEGORIES = ("party", "duty", "trigger", "deadline", "written_form", "exception", "liability", "remedy")
|
||||
ADVISORY_FIELDS = ("assumptions", "risk_notes", "change_explanations", "quality_findings")
|
||||
SEMANTIC_ENTITY_ID_PATTERN = r"^[a-z][a-z0-9_-]{0,63}$"
|
||||
SemanticEntityId = Annotated[
|
||||
str,
|
||||
Field(pattern=SEMANTIC_ENTITY_ID_PATTERN),
|
||||
]
|
||||
|
||||
|
||||
def _validate_semantic_entity_reference(value: str) -> str:
|
||||
if re.fullmatch(SEMANTIC_ENTITY_ID_PATTERN, value) is None:
|
||||
raise ValueError("Semantic entity references must use canonical lowercase identifiers.")
|
||||
return value
|
||||
|
||||
|
||||
SemanticEntityReference = Annotated[
|
||||
str,
|
||||
AfterValidator(_validate_semantic_entity_reference),
|
||||
]
|
||||
|
||||
|
||||
def _canonical(value: Any) -> str:
|
||||
@@ -174,7 +192,7 @@ class DraftSemanticTargetCheck(_Strict):
|
||||
base_occurrence: int | None = Field(ge=1, le=1000)
|
||||
candidate_excerpt: str | None = Field(max_length=4000)
|
||||
candidate_occurrence: int | None = Field(ge=1, le=1000)
|
||||
finding_ids: list[str] = Field(max_length=100)
|
||||
finding_ids: list[SemanticEntityReference] = Field(max_length=100)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def nonblank(self):
|
||||
@@ -187,7 +205,7 @@ class DraftSemanticCandidateCheck(_Strict):
|
||||
target_id: str
|
||||
candidate_excerpt: str = Field(min_length=1, max_length=4000)
|
||||
candidate_occurrence: int | None = Field(ge=1, le=1000)
|
||||
finding_ids: list[str] = Field(max_length=100)
|
||||
finding_ids: list[SemanticEntityReference] = Field(max_length=100)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def nonblank(self):
|
||||
@@ -200,7 +218,7 @@ class DraftSemanticAdvisoryCheck(_Strict):
|
||||
field: Literal["assumptions", "risk_notes", "change_explanations", "quality_findings"]
|
||||
index: int = Field(ge=0, le=99)
|
||||
exact_excerpt: str = Field(min_length=1, max_length=10000)
|
||||
metadata_finding_ids: list[str] = Field(max_length=100)
|
||||
metadata_finding_ids: list[SemanticEntityReference] = Field(max_length=100)
|
||||
|
||||
|
||||
class DraftSemanticCoverage(_Strict):
|
||||
@@ -213,7 +231,7 @@ class DraftSemanticCoverage(_Strict):
|
||||
|
||||
|
||||
class DraftSemanticFinding(_Strict):
|
||||
id: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,63}$")
|
||||
id: SemanticEntityId
|
||||
category: Literal["party", "duty", "trigger", "deadline", "written_form", "exception", "liability", "remedy"]
|
||||
disposition: Literal["preserved", "supported_change", "omitted", "reversed", "unsupported_change", "ambiguous"]
|
||||
severity: Literal["info", "warning", "error"]
|
||||
@@ -238,8 +256,8 @@ class DraftSemanticFinding(_Strict):
|
||||
|
||||
|
||||
class DraftSemanticRepairInstruction(_Strict):
|
||||
finding_ids: list[str] = Field(max_length=50)
|
||||
metadata_finding_ids: list[str] = Field(max_length=50)
|
||||
finding_ids: list[SemanticEntityReference] = Field(max_length=50)
|
||||
metadata_finding_ids: list[SemanticEntityReference] = Field(max_length=50)
|
||||
action: str = Field(min_length=1, max_length=2000)
|
||||
constraints: list[str] = Field(min_length=1, max_length=50)
|
||||
|
||||
@@ -251,7 +269,7 @@ class DraftSemanticRepairInstruction(_Strict):
|
||||
|
||||
|
||||
class DraftSemanticMetadataFinding(_Strict):
|
||||
id: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,63}$")
|
||||
id: SemanticEntityId
|
||||
field: Literal["assumptions", "risk_notes", "change_explanations", "quality_findings"]
|
||||
index: int = Field(ge=0, le=99)
|
||||
exact_excerpt: str = Field(min_length=1, max_length=10000)
|
||||
@@ -262,7 +280,7 @@ class DraftSemanticMetadataFinding(_Strict):
|
||||
|
||||
|
||||
class DraftRepairResolutionCheck(_Strict):
|
||||
finding_id: str = Field(pattern=r"^[a-z][a-z0-9_-]{0,63}$")
|
||||
finding_id: SemanticEntityReference = Field(pattern=SEMANTIC_ENTITY_ID_PATTERN)
|
||||
status: Literal["resolved", "unresolved", "uncertain"]
|
||||
candidate_excerpt: str = Field(min_length=1, max_length=4000)
|
||||
candidate_occurrence: int | None = Field(ge=1, le=1000)
|
||||
|
||||
@@ -58,6 +58,17 @@ def semantic_wire_output_schema(
|
||||
node = schema["$defs"][definition]
|
||||
node["properties"].pop(leaf)
|
||||
node["required"] = [name for name in node["required"] if name != leaf]
|
||||
identifier_pattern = schema["$defs"]["DraftSemanticFinding"]["properties"]["id"]["pattern"]
|
||||
for definition, field in (
|
||||
("DraftSemanticTargetCheck", "finding_ids"),
|
||||
("DraftSemanticCandidateCheck", "finding_ids"),
|
||||
("DraftSemanticRepairInstruction", "finding_ids"),
|
||||
("DraftSemanticAdvisoryCheck", "metadata_finding_ids"),
|
||||
("DraftSemanticRepairInstruction", "metadata_finding_ids"),
|
||||
):
|
||||
schema["$defs"][definition]["properties"][field]["items"]["pattern"] = (
|
||||
identifier_pattern
|
||||
)
|
||||
return schema
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from test_support.external_qa import require_external_qa
|
||||
import json
|
||||
from pathlib import Path
|
||||
from yuqei_sdk.contract_intake import (ContractIntakeRequest, ContractIntakeOutput,
|
||||
@@ -77,6 +78,10 @@ def test_ambiguous_money_rejects_empty_or_invalid_candidates(value_json):
|
||||
assert validate_intake_output(request(), result)
|
||||
|
||||
def test_postfix_regression_replays_run01_failure_without_mutating_artifact():
|
||||
require_external_qa(
|
||||
"template-prompt-audit-20260828/tp06-11-intake/run-01/INTAKE-003-ambiguous-injection.json",
|
||||
"template-prompt-audit-20260828/tp06-11-intake/frozen-v1.json",
|
||||
)
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
artifact_path=root/"docs/qa/template-prompt-audit-20260828/tp06-11-intake/run-01/INTAKE-003-ambiguous-injection.json"
|
||||
frozen_path=root/"docs/qa/template-prompt-audit-20260828/tp06-11-intake/frozen-v1.json"
|
||||
@@ -88,6 +93,10 @@ def test_postfix_regression_replays_run01_failure_without_mutating_artifact():
|
||||
assert artifact_path.read_bytes()==before and artifact["assessment"]["passed"] is False
|
||||
|
||||
def test_question_completion_replays_known_postfix_missing_failure_without_rewriting_evidence():
|
||||
require_external_qa(
|
||||
"template-prompt-audit-20260828/tp06-12/intake-postfix-run-01/INTAKE-002-missing.json",
|
||||
"template-prompt-audit-20260828/tp06-11-intake/frozen-v1.json",
|
||||
)
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
artifact_path=root/"docs/qa/template-prompt-audit-20260828/tp06-12/intake-postfix-run-01/INTAKE-002-missing.json"
|
||||
frozen_path=root/"docs/qa/template-prompt-audit-20260828/tp06-11-intake/frozen-v1.json"
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
from test_support.external_qa import require_external_qa
|
||||
|
||||
from yuqei_sdk.contract_drafting import (
|
||||
ContractDraftOutput, ContractDraftRequest, draft_digest, draft_output_schema,
|
||||
@@ -87,6 +88,10 @@ def test_whole_paragraph_replace_accepts_internal_pure_insert_or_delete(candidat
|
||||
|
||||
|
||||
def test_saved_real_whole_paragraph_replacements_bind_to_their_internal_edits():
|
||||
require_external_qa(
|
||||
"multiscene-closeout-20260910/draft-timeout-diagnostic-real-v1/01-draft-before.json",
|
||||
"multiscene-closeout-20260910/draft-timeout-diagnostic-real-v1/01-draft-after.json",
|
||||
)
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
evidence_dir = root / "docs/qa/multiscene-closeout-20260910/draft-timeout-diagnostic-real-v1"
|
||||
before = json.loads((evidence_dir / "01-draft-before.json").read_text(encoding="utf-8"))
|
||||
|
||||
@@ -182,6 +182,58 @@ def test_provider_schema_is_strict_required_nullable_and_runtime_rejects_invalid
|
||||
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 = []
|
||||
|
||||
@@ -144,6 +144,36 @@ def test_v2_schema_removes_only_three_mechanical_leaves_and_fixes_version():
|
||||
assert "candidate_excerpt" in schema["$defs"]["DraftSemanticTargetCheck"]["properties"]
|
||||
|
||||
|
||||
def test_v2_schema_and_decoder_reject_case_changed_finding_aliases():
|
||||
req = request()
|
||||
schema = semantic_wire_output_schema(req, WIRE_V2)
|
||||
identifier_pattern = schema["$defs"]["DraftSemanticFinding"]["properties"]["id"]["pattern"]
|
||||
assert schema["$defs"]["DraftSemanticTargetCheck"]["properties"]["finding_ids"]["items"]["pattern"] \
|
||||
== identifier_pattern
|
||||
assert schema["$defs"]["DraftSemanticCandidateCheck"]["properties"]["finding_ids"]["items"]["pattern"] \
|
||||
== identifier_pattern
|
||||
|
||||
payload = wire(req)
|
||||
payload["findings"] = [{
|
||||
"id": "find-001", "category": "duty", "disposition": "preserved", "severity": "info",
|
||||
"base_excerpt": req.base_document, "base_occurrence": 1,
|
||||
"candidate_excerpt": req.candidate_document, "candidate_occurrence": 1,
|
||||
"fact_fields": ["付款安排"], "clause_ids": [], "change_declaration_ids": [],
|
||||
"instruction_excerpt": "保留付款义务。", "explanation": "付款义务已保留。",
|
||||
"question_field": None,
|
||||
}]
|
||||
payload["coverage"]["target_checks"][0]["finding_ids"] = ["F-001"]
|
||||
payload["coverage"]["candidate_checks"][0]["finding_ids"] = ["F-001"]
|
||||
with pytest.raises(ValueError):
|
||||
decode_semantic_wire(req, json.dumps(payload, ensure_ascii=False), WIRE_V2)
|
||||
|
||||
payload["coverage"]["target_checks"][0]["finding_ids"] = ["find-001"]
|
||||
payload["coverage"]["candidate_checks"][0]["finding_ids"] = ["find-001"]
|
||||
decoded = decode_semantic_wire(req, json.dumps(payload, ensure_ascii=False), WIRE_V2)
|
||||
assert decoded.coverage.target_checks[0].finding_ids == ["find-001"]
|
||||
assert decoded.coverage.candidate_checks[0].finding_ids == ["find-001"]
|
||||
|
||||
|
||||
def test_v2_hydrates_only_exact_frozen_leaf_values_and_preserves_judgment():
|
||||
req = request()
|
||||
payload = wire(req)
|
||||
|
||||
@@ -17,6 +17,43 @@ def test_real_catalog_has_bounded_batch_a_and_complete_batch_b_coverage():
|
||||
assert entry["complete"] is (len(entry["cases"]) == 3)
|
||||
|
||||
|
||||
def test_catalog_embeds_variant_and_hash_provenance_without_external_evidence():
|
||||
catalog_path = Path(coverage_module.__file__).with_name("scene_review_coverage.json")
|
||||
catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
|
||||
expected_variants = {"normal", "missing-key-fact", "opposite-adversarial"}
|
||||
|
||||
def is_sha256(value):
|
||||
return (
|
||||
isinstance(value, str)
|
||||
and len(value) == 64
|
||||
and set(value) <= set("0123456789abcdef")
|
||||
)
|
||||
|
||||
assert catalog["schema_version"] == "scene-review-coverage-catalog.v1"
|
||||
assert len(catalog["entries"]) == 30
|
||||
for template_id, entry in catalog["entries"].items():
|
||||
assert entry["template_id"] == template_id
|
||||
assert set(entry["required_variants"]) == expected_variants
|
||||
assert set(entry["passed_variants"]) | set(entry["missing_variants"]) == expected_variants
|
||||
assert set(entry["passed_variants"]).isdisjoint(entry["missing_variants"])
|
||||
assert entry["source_hashes"]
|
||||
assert all(is_sha256(value) for value in entry["source_hashes"].values())
|
||||
assert is_sha256(entry["template_content_sha256"])
|
||||
for prompt_key in ("draft_prompt", "review_prompt"):
|
||||
assert is_sha256(entry[prompt_key]["content_sha256"])
|
||||
for case in entry["cases"]:
|
||||
assert case["variant"] in entry["passed_variants"]
|
||||
for hash_key in (
|
||||
"request_sha256",
|
||||
"artifact_raw_sha256",
|
||||
"artifact_canonical_sha256",
|
||||
"template_content_sha256",
|
||||
):
|
||||
assert is_sha256(case[hash_key])
|
||||
for prompt_key in ("draft_prompt", "review_prompt"):
|
||||
assert is_sha256(case[prompt_key]["content_sha256"])
|
||||
|
||||
|
||||
def test_lookup_returns_defensive_copies():
|
||||
first = coverage_module.scene_review_coverage("TPL-TRANSPORT-ORDINARY-001")
|
||||
assert first is not None
|
||||
|
||||
Reference in New Issue
Block a user