Files
ai-contract-platform/yuqei-infra/scripts/multiscene_eval_plan.py
T

497 lines
26 KiB
Python

"""Build and validate the immutable W4 multiscene evaluation plan."""
from __future__ import annotations
from datetime import UTC, datetime
from hashlib import sha256
import json
import os
import re
import subprocess
from pathlib import Path, PurePath
from typing import Any, Mapping
from yuqei_contract_api.contract_templates import template_document_kind
from yuqei_contract_api.drafting_workspace import (
DraftingCandidateRequest, DraftingCreateRequest, DraftingEvaluationCreateRequest,
DraftingWorkspaceStore,
)
from yuqei_contract_api.in_memory_repository import InMemoryContractRepository
from yuqei_sdk.contract_drafting import ContractDraftRequest, draft_digest
from multiscene_eval_cases import build_cases
from multiscene_supplemental_cases import build_supplemental_cases
ROOT = Path(__file__).resolve().parents[2]
SCHEMA_VERSION = "multiscene-evaluation-plan.v2"
CANDIDATE_SCHEMA_VERSION = "multiscene-candidate-binding.v1"
RELEASE_CONTROLS_SCHEMA_VERSION = "multiscene-release-controls.v1"
REQUIRED_IMAGE_ROLES = (
"contract-web", "contract-api", "ai-platform-api", "ai-worker",
)
CANARY_CASE_IDS = (
"base--TPL-AGENCY-DISTRIBUTION-001--normal",
"base--TPL-DEMAND-LETTER-001--normal",
"base--TPL-PURCHASE-GOODS-001--normal",
"scene--TPL-AMENDMENT-GENERAL-001--opposite-adversarial",
"specialist-trade_procurement-first",
"intake-tpl-purchase-goods-001",
)
_SHA256_RE = re.compile(r"[0-9a-f]{64}\Z")
_COMMIT_RE = re.compile(r"[0-9a-f]{40}\Z")
_TAG_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]{0,127}\Z")
EXPECTED_OPERATIONS = {
"draft": 120, "semantic_review": 30, "repair_draft": 8,
"repair_review": 8, "specialist_review": 106, "intake": 8,
}
EXPECTED_BUDGET = 280
PIN_ROOTS = (
"yuqei-ai-sdk-python/src/yuqei_sdk",
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api",
"yuqei-contract-platform/services/contract-api/src/yuqei_contract_api",
)
PIN_FILES = (
"yuqei-infra/scripts/multiscene_eval_cases.py",
"yuqei-infra/scripts/multiscene_supplemental_cases.py",
"yuqei-infra/scripts/multiscene_call_ledger.py",
"yuqei-infra/scripts/multiscene_eval_plan.py",
"yuqei-infra/scripts/multiscene_eval_runner.py",
"yuqei-infra/scripts/run-core-drafting-smoke.py",
"yuqei-contract-platform/services/contract-api/tests/test_semantic_review_cross_service_flow.py",
"yuqei-ai-sdk-python/pyproject.toml",
"yuqei-ai-platform/services/ai-platform-api/pyproject.toml",
"yuqei-contract-platform/services/contract-api/pyproject.toml",
)
PIN_SUFFIXES = {".py", ".json", ".md", ".toml", ".yaml", ".yml"}
def _hash_bytes(value: bytes) -> str:
return sha256(value).hexdigest()
def _canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True,
separators=(",", ":"), allow_nan=False)
def _strict_json(raw: bytes, label: str) -> Any:
def pairs(items):
result = {}
for key, value in items:
if key in result:
raise ValueError(f"{label} contains a duplicate JSON key")
result[key] = value
return result
def constant(value):
raise ValueError(f"{label} contains non-finite JSON value {value}")
try:
return json.loads(raw, object_pairs_hook=pairs, parse_constant=constant)
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise ValueError(f"{label} is not valid strict JSON") from error
def _require_sha(value: object, label: str) -> str:
if not isinstance(value, str) or not _SHA256_RE.fullmatch(value):
raise ValueError(f"{label} must be a lowercase SHA-256 value")
return value
def _artifact_bytes(path: Path, root: Path, label: str) -> bytes:
supplied = Path(path)
if not supplied.is_absolute():
raise ValueError(f"{label} path must be absolute")
absolute = supplied.absolute()
current = absolute
while True:
junction = getattr(current, "is_junction", lambda: False)
if current.is_symlink() or junction():
raise ValueError(f"{label} path must not traverse links or junctions")
if current.parent == current:
break
current = current.parent
if not absolute.is_file() or absolute.resolve().is_relative_to(root.resolve()):
raise ValueError(f"{label} must be a regular file outside the source worktree")
return absolute.read_bytes()
def _git(root: Path, *args: str, binary: bool = False):
environment = {**os.environ, "GIT_CONFIG_NOSYSTEM": "1",
"GIT_CONFIG_GLOBAL": os.devnull, "GIT_OPTIONAL_LOCKS": "0",
"GIT_NO_REPLACE_OBJECTS": "1"}
completed = subprocess.run(
["git", "-C", str(root), *args], capture_output=True,
text=not binary, check=False, env=environment,
)
if completed.returncode:
error = completed.stderr if isinstance(completed.stderr, str) else completed.stderr.decode("utf-8", "replace")
raise ValueError(f"git candidate verification failed: {error.strip()}")
return completed.stdout if binary else completed.stdout.strip()
def _verify_git_candidate(root: Path, commit: str, tag: str, stage: str) -> None:
if not _COMMIT_RE.fullmatch(commit):
raise ValueError("candidate commit must be a full lowercase SHA-1 object ID")
if (not _TAG_RE.fullmatch(tag) or ".." in tag or "//" in tag
or tag.endswith((".", "/")) or tag.endswith(".lock")):
raise ValueError("candidate tag is unsafe")
head = _git(root, "rev-parse", "--verify", "HEAD^{commit}")
tagged = _git(root, "rev-parse", "--verify", f"refs/tags/{tag}^{{commit}}")
if head != commit or tagged != commit:
raise ValueError(f"candidate HEAD/tag mismatch {stage}")
if _git(root, "status", "--porcelain=v1", "-z", "--untracked-files=all"):
raise ValueError(f"candidate worktree is not clean {stage}")
def _source_manifest(payload: Any, commit: str) -> tuple[dict[str, str], str]:
expected = {"schema_version", "candidate", "files", "summaries",
"claims_not_made", "manifest_payload_sha256"}
if not isinstance(payload, dict) or set(payload) != expected:
raise ValueError("source manifest shape is invalid")
if payload["schema_version"] != "yuqei.release-source-manifest.v1":
raise ValueError("unsupported source manifest schema")
embedded = _require_sha(payload["manifest_payload_sha256"], "source manifest payload SHA-256")
projection = {key: value for key, value in payload.items()
if key != "manifest_payload_sha256"}
if _hash_bytes(_canonical(projection).encode("utf-8")) != embedded:
raise ValueError("source manifest embedded payload SHA-256 mismatch")
candidate = payload["candidate"]
if (not isinstance(candidate, dict) or candidate.get("commit") != commit
or candidate.get("worktree_was_clean") is not True
or candidate.get("identity_policy") != "full_commit_oid_equals_HEAD"):
raise ValueError("source manifest candidate identity mismatch")
files = payload["files"]
if not isinstance(files, dict) or not isinstance(files.get("items"), list):
raise ValueError("source manifest file inventory is invalid")
if files.get("count") != len(files["items"]):
raise ValueError("source manifest file count is inconsistent")
index: dict[str, str] = {}
for row in files["items"]:
if not isinstance(row, dict) or set(row) != {
"path", "mode", "git_blob_oid", "size", "sha256",
}:
raise ValueError("source manifest file row is invalid")
relative = row["path"]
if (not isinstance(relative, str) or relative.startswith(("/", "\\"))
or ".." in PurePath(relative).parts or relative in index):
raise ValueError("source manifest contains an unsafe or duplicate path")
index[relative] = _require_sha(row["sha256"], f"source manifest {relative}")
return index, embedded
def _image_identities(payload: Any, commit: str, tag: str) -> tuple[list[dict[str, str]], str, str]:
if not isinstance(payload, dict):
raise ValueError("image identities must be an object")
schema = payload.get("schema_version")
if schema == "yuqei.rc06-image-identities.v1":
if set(payload) != {"schema_version", "commit", "tag", "images"}:
raise ValueError("image identities shape is invalid")
document_commit, document_tag, images = payload["commit"], payload["tag"], payload["images"]
elif schema == "yuqei.installable-release-manifest.v1":
candidate = payload.get("candidate")
if not isinstance(candidate, dict):
raise ValueError("installable image candidate is missing")
document_commit, document_tag, images = candidate.get("commit"), candidate.get("tag"), payload.get("images")
else:
raise ValueError("unsupported image identities schema")
if document_commit != commit or document_tag != tag:
raise ValueError("image identities candidate does not match commit/tag")
if not isinstance(images, list) or len(images) != len(REQUIRED_IMAGE_ROLES):
raise ValueError("image identities must contain four application images")
normalized, roles, manifests = [], set(), set()
for row in images:
if not isinstance(row, dict):
raise ValueError("image identity row is invalid")
if (schema == "yuqei.rc06-image-identities.v1"
and set(row) != {"role", "repository", "manifest_digest", "config_digest"}):
raise ValueError("compact image identity row shape is invalid")
role = row.get("role")
repository = row.get("repository") if schema == "yuqei.rc06-image-identities.v1" else (row.get("image_name") or row.get("registry_reference"))
manifest, config = row.get("manifest_digest"), row.get("config_digest")
if (not isinstance(role, str) or not isinstance(repository, str) or not repository
or not isinstance(manifest, str) or not manifest.startswith("sha256:")
or not isinstance(config, str) or not config.startswith("sha256:")):
raise ValueError("image identity row is incomplete")
_require_sha(manifest.removeprefix("sha256:"), f"image {role} manifest digest")
_require_sha(config.removeprefix("sha256:"), f"image {role} config digest")
if role in roles or manifest in manifests:
raise ValueError("duplicate image role or manifest digest")
roles.add(role)
manifests.add(manifest)
normalized.append({"role": role, "repository": repository,
"manifest_digest": manifest, "config_digest": config})
if roles != set(REQUIRED_IMAGE_ROLES):
raise ValueError("image roles mismatch")
normalized.sort(key=lambda item: REQUIRED_IMAGE_ROLES.index(item["role"]))
return normalized, _hash_bytes(_canonical(normalized).encode("utf-8")), schema
def load_candidate_binding(runner, *, commit: str, tag: str,
source_manifest_path: Path, source_manifest_sha256: str,
source_manifest_payload_sha256: str, image_identities_path: Path,
image_identities_sha256: str, image_identity_set_sha256: str,
stage: str = "candidate preflight") -> tuple[dict[str, Any], dict[str, str]]:
"""Load and reverify a built immutable candidate without creating evidence."""
root = Path(runner.ROOT).resolve()
_verify_git_candidate(root, commit, tag, stage)
source_raw = _artifact_bytes(source_manifest_path, root, "source manifest")
image_raw = _artifact_bytes(image_identities_path, root, "image identities")
if _hash_bytes(source_raw) != _require_sha(source_manifest_sha256, "source manifest raw SHA-256"):
raise ValueError("source manifest raw SHA-256 mismatch")
if _hash_bytes(image_raw) != _require_sha(image_identities_sha256, "image identities raw SHA-256"):
raise ValueError("image identities raw SHA-256 mismatch")
source_index, embedded = _source_manifest(_strict_json(source_raw, "source manifest"), commit)
if embedded != _require_sha(source_manifest_payload_sha256, "source manifest payload SHA-256"):
raise ValueError("source manifest payload SHA-256 argument mismatch")
images, identity_set, image_schema = _image_identities(
_strict_json(image_raw, "image identities"), commit, tag)
if identity_set != _require_sha(image_identity_set_sha256, "image identity-set SHA-256"):
raise ValueError("image identity-set SHA-256 mismatch")
binding = {
"schema_version": CANDIDATE_SCHEMA_VERSION, "commit": commit, "tag": tag,
"source_manifest": {"schema_version": "yuqei.release-source-manifest.v1",
"raw_sha256": source_manifest_sha256,
"payload_sha256": source_manifest_payload_sha256},
"image_identities": {"schema_version": image_schema,
"raw_sha256": image_identities_sha256,
"identity_set_sha256": image_identity_set_sha256,
"images": images},
}
return binding, source_index
def _candidate_binding(value: Mapping[str, Any]) -> dict[str, Any]:
if not isinstance(value, Mapping) or value.get("schema_version") != CANDIDATE_SCHEMA_VERSION:
raise ValueError("candidate binding is missing or unsupported")
result = json.loads(_canonical(dict(value)))
if set(result) != {"schema_version", "commit", "tag", "source_manifest", "image_identities"}:
raise ValueError("candidate binding shape is invalid")
if not _COMMIT_RE.fullmatch(str(result.get("commit", ""))):
raise ValueError("candidate binding commit is invalid")
if (not isinstance(result.get("tag"), str) or not _TAG_RE.fullmatch(result["tag"])
or ".." in result["tag"] or "//" in result["tag"]):
raise ValueError("candidate binding tag is invalid")
source, images = result.get("source_manifest"), result.get("image_identities")
if not isinstance(source, dict) or not isinstance(images, dict):
raise ValueError("candidate binding artifacts are invalid")
if set(source) != {"schema_version", "raw_sha256", "payload_sha256"}:
raise ValueError("candidate source binding shape is invalid")
if source["schema_version"] != "yuqei.release-source-manifest.v1":
raise ValueError("candidate source binding schema is invalid")
if set(images) != {"schema_version", "raw_sha256", "identity_set_sha256", "images"}:
raise ValueError("candidate image binding shape is invalid")
if images["schema_version"] not in {
"yuqei.rc06-image-identities.v1", "yuqei.installable-release-manifest.v1",
}:
raise ValueError("candidate image binding schema is invalid")
_require_sha(source.get("raw_sha256"), "candidate source raw SHA-256")
_require_sha(source.get("payload_sha256"), "candidate source payload SHA-256")
_require_sha(images.get("raw_sha256"), "candidate image raw SHA-256")
_require_sha(images.get("identity_set_sha256"), "candidate image set SHA-256")
if not isinstance(images.get("images"), list) or len(images["images"]) != 4:
raise ValueError("candidate binding must include four image identities")
normalized, roles, manifests = [], set(), set()
for row in images["images"]:
if not isinstance(row, dict) or set(row) != {
"role", "repository", "manifest_digest", "config_digest",
}:
raise ValueError("candidate normalized image row is invalid")
role, manifest, config = row["role"], row["manifest_digest"], row["config_digest"]
if role not in REQUIRED_IMAGE_ROLES or role in roles or manifest in manifests:
raise ValueError("candidate image role or manifest digest is invalid")
if not isinstance(row["repository"], str) or not row["repository"]:
raise ValueError("candidate image repository is invalid")
if not isinstance(manifest, str) or not manifest.startswith("sha256:"):
raise ValueError("candidate image manifest digest is invalid")
if not isinstance(config, str) or not config.startswith("sha256:"):
raise ValueError("candidate image config digest is invalid")
_require_sha(manifest.removeprefix("sha256:"), "candidate image manifest digest")
_require_sha(config.removeprefix("sha256:"), "candidate image config digest")
roles.add(role); manifests.add(manifest); normalized.append(row)
if roles != set(REQUIRED_IMAGE_ROLES):
raise ValueError("candidate image roles mismatch")
normalized.sort(key=lambda item: REQUIRED_IMAGE_ROLES.index(item["role"]))
if images["images"] != normalized:
raise ValueError("candidate image identities are not canonically ordered")
if _hash_bytes(_canonical(normalized).encode("utf-8")) != images["identity_set_sha256"]:
raise ValueError("candidate image identity-set digest is invalid")
return result
def verify_source_pins_at_candidate(runner, source_pins: Mapping[str, str],
source_index: Mapping[str, str], commit: str) -> None:
root = Path(runner.ROOT).resolve()
for relative, digest in source_pins.items():
if source_index.get(relative) != digest:
raise ValueError(f"source pin is absent from or differs from manifest: {relative}")
committed = _git(root, "show", f"{commit}:{relative}", binary=True)
if _hash_bytes(committed) != digest:
raise ValueError(f"source pin differs from candidate commit: {relative}")
def draft_request_projection(request: ContractDraftRequest | dict[str, Any]) -> dict[str, Any]:
"""Return stable model facts and template identities; omit workspace/lifecycle data."""
request = ContractDraftRequest.model_validate(request)
data = request.model_dump(mode="json")
snapshot = data["template_snapshot"]
return {
"document_kind": data["document_kind"], "title": data["contract_title"],
"contract_type": data["contract_type"], "industry": data["industry"],
"party_position": data["party_position"], "jurisdiction": data["jurisdiction"],
"transaction_stage": data["transaction_stage"], "facts": data["facts"],
"missing_facts": data["missing_facts"], "base_document": data["base_document"],
"instructions": data["drafting_instructions"],
"template_content_sha256": snapshot.get("content_sha256"),
"template_schema_sha256": snapshot.get("schema_sha256"),
}
def _source_pins(runner) -> dict[str, str]:
paths: set[Path] = set()
for relative in PIN_ROOTS:
root = ROOT / relative
paths.update(path for path in root.rglob("*") if path.is_file()
and path.suffix.lower() in PIN_SUFFIXES
and "__pycache__" not in path.parts)
paths.update(ROOT / relative for relative in PIN_FILES)
runner_file = Path(getattr(runner, "__file__", ""))
if runner_file.is_file():
paths.add(runner_file.resolve())
cross = getattr(runner, "CROSS", None)
cross_file = Path(getattr(cross, "__file__", ""))
if cross_file.is_file():
paths.add(cross_file.resolve())
missing = [str(path.relative_to(ROOT)) for path in paths if not path.is_file()]
if missing:
raise ValueError(f"source pin file missing: {missing}")
return {path.relative_to(ROOT).as_posix(): _hash_bytes(path.read_bytes())
for path in sorted(paths)}
def _prepared_projection(case: dict[str, Any], repository: InMemoryContractRepository) -> tuple[dict[str, Any], Any]:
# Base cases need the real contract policy (including auto repair); scene
# release-gate cases must remain in the isolated evaluation entry point.
evaluation = case["mode"] == "scene_evaluation"
store = DraftingWorkspaceStore(repository, mode="scene_evaluation" if evaluation else "contract")
template, _ = (store.evaluation_source(case["template_id"]) if evaluation
else repository.get_template_source(case["template_id"]))
request_type = DraftingEvaluationCreateRequest if evaluation else DraftingCreateRequest
context = case.get("scene_request_context", {})
created = request_type(
template_id=case["template_id"], expected_template_content_sha256=template.content_sha256,
title=case["title"], values=case["values"], industry=context.get("industry", "未指定"),
party_position=context.get("party_position", "中立"),
jurisdiction=context.get("jurisdiction", "中国大陆"),
transaction_stage=context.get("transaction_stage", "drafting"),
instructions=case["instructions"], **({"synthetic_only": True} if evaluation else {}),
)
record = store.create(created, tenant="w4-isolated", owner="w4-builder",
key=f"create-{case['case_id']}")
policy = {"track_changes": True, "semantic_review": not evaluation,
"auto_repair": case["auto_repair"]}
_, candidate = store.prepare(record.id, DraftingCandidateRequest(
expected_revision=record.revision, idempotency_key=f"candidate-{draft_digest(case['case_id'])}",
**policy), tenant="w4-isolated", owner="w4-builder")
projection = draft_request_projection(candidate.model_request)
if projection["document_kind"] != case["document_kind"]:
raise ValueError(f"document kind drift: {case['case_id']}")
actual_missing = [item["field"] for item in projection["missing_facts"]]
expected_missing = case["expectation_profile"]["expected_missing_fields"]
actual_status = "incomplete" if actual_missing else "candidate_ready"
if actual_missing != expected_missing or actual_status != case["expectation_profile"]["expected_status"]:
raise ValueError(f"prepared missing/status drift: {case['case_id']}")
return projection, template
def _normalized_cases() -> list[dict[str, Any]]:
repository = InMemoryContractRepository(contracts=[])
repository.allow_internal_trial_templates = True
drafting = []
for case in build_cases():
projection, template = _prepared_projection(case, repository)
policy = {"track_changes": True, "semantic_review": case["mode"] == "contract",
"auto_repair": case["auto_repair"]}
failure_class = ("scene_drafting" if case["mode"] == "scene_evaluation"
else "contract_drafting")
drafting.append({**case, "failure_class": failure_class,
"candidate_policy": policy,
"template_content_sha256": projection["template_content_sha256"],
"template_input_schema_sha256": projection["template_schema_sha256"],
"values_sha256": draft_digest(case["values"]),
"draft_request_projection": projection})
supplemental = []
for case in build_supplemental_cases():
supplemental.append({
"case_id": case["case_id"], "source_case_id": case["case_id"],
"kind": case["operation"], "mode": case["operation"], "operation": case["operation"],
"operations": [case["operation"]], "provider_call_budget": 1,
"failure_class": case["operation"],
"request": case["request"].model_dump(mode="json"),
"request_sha256": case["request_sha256"], "expectations": case["expectations"],
**({"module": case["module"], "party_position": case["party_position"]}
if case["operation"] == "specialist_review" else {}),
})
return [*drafting, *supplemental]
def _operation_counts(cases: list[dict[str, Any]]) -> dict[str, int]:
result: dict[str, int] = {}
for case in cases:
for operation in case["operations"]:
result[operation] = result.get(operation, 0) + 1
return result
def build_plan(runner, candidate_binding: Mapping[str, Any]) -> dict[str, Any]:
cases = _normalized_cases()
counts = _operation_counts(cases)
if len(cases) != 144 or counts != EXPECTED_OPERATIONS:
raise ValueError(f"case or operation inventory mismatch: {len(cases)}, {counts}")
if sum(case["provider_call_budget"] for case in cases) != EXPECTED_BUDGET:
raise ValueError("global provider call budget mismatch")
inventory = {case["case_id"]: case for case in cases}
if any(case_id not in inventory for case_id in CANARY_CASE_IDS):
raise ValueError("predefined RC-05 canary inventory drift")
return {
"schema_version": SCHEMA_VERSION, "prepared_at": datetime.now(UTC).isoformat(),
"model": "gpt-5.6-sol", "reasoning_effort": "medium", "timeout_seconds": 300,
"case_count": 144, "drafting_case_count": 120, "supplemental_case_count": 24,
"operation_limits": counts, "total_provider_call_budget": EXPECTED_BUDGET,
"candidate": _candidate_binding(candidate_binding),
"release_controls": {
"schema_version": RELEASE_CONTROLS_SCHEMA_VERSION,
"canary_case_ids": list(CANARY_CASE_IDS),
"failure_classes": {case["case_id"]: case["failure_class"] for case in cases},
},
"source_pins": _source_pins(runner), "cases": cases,
"limitations": ["合成隔离评测,不是生产发布或法律批准。",
"deterministic projection equality does not establish semantic or legal correctness."],
}
def validate_plan(runner, path: Path, expected_sha: str | None = None,
candidate_binding: Mapping[str, Any] | None = None,
source_manifest_index: Mapping[str, str] | None = None) -> tuple[dict[str, Any], str]:
raw = Path(path).read_bytes()
actual = _hash_bytes(raw)
if expected_sha is not None and actual != expected_sha:
raise ValueError("plan raw SHA-256 mismatch")
plan = _strict_json(raw, "multiscene plan")
if plan.get("schema_version") != SCHEMA_VERSION:
raise ValueError("legacy/unsupported multiscene plan is read-only diagnostic material; execution refused")
frozen_candidate = _candidate_binding(plan.get("candidate"))
if candidate_binding is not None and _canonical(frozen_candidate) != _canonical(
_candidate_binding(candidate_binding)):
raise ValueError("multiscene plan candidate binding mismatch")
expected = build_plan(runner, frozen_candidate)
left = {key: value for key, value in plan.items() if key != "prepared_at"}
right = {key: value for key, value in expected.items() if key != "prepared_at"}
if _canonical(left) != _canonical(right):
raise ValueError("multiscene plan content or source pins drift")
if source_manifest_index is not None:
verify_source_pins_at_candidate(
runner, plan["source_pins"], source_manifest_index,
frozen_candidate["commit"])
return plan, actual