zizi 061010ba1b 框架: 共享运行时装成可安装包,切断 Skill 之间的 sys.path 互指
被多个 Skill 或看板消费的连接、模型、嵌入、Claude 运行时与声音账入口
各只保留一份实现;Skill 只留 CLI/落库,看板只读 muse-db,门禁锁死跨域注入。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 00:45:20 +08:00

1065 lines
58 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""SemanticDetection v3 的无状态模型适配器与确定性绑定。"""
from __future__ import annotations
import copy
import hashlib
import json
import pathlib
import re
from typing import Any, Mapping, Protocol, Sequence, runtime_checkable
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
try:
from claude_runtime import ExecutionProfile, contains_path_traversal, run_claude, sha256_json # type: ignore[import-not-found] # noqa: E402
except ImportError:
ExecutionProfile = Any # type: ignore[misc,assignment]
run_claude = None # type: ignore[assignment]
sha256_json = None # type: ignore[assignment]
def contains_path_traversal(value: str) -> bool: # type: ignore[misc]
# WHY: claude_runtime 缺席时失败关闭——任何引用一律按路径穿越处理、触发泄露拦截,
# 绝不在精确判定缺失时放行引用。
return True
INPUT_VERSION = "semantic-detector-input-v3"
MODEL_OUTPUT_VERSION = "semantic-detection-draft-v3"
REPORT_VERSION = "semantic-detection-v3"
HASH_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
REAL_ARM_NAMES = frozenset({"A", "B", "C"})
RAW_PATH_PATTERN = re.compile(r"(?:^|[/\\])(?:private[/\\]tmp|tmp|raw)(?:[/\\]|$)", re.IGNORECASE)
VERDICTS = frozenset({"pass", "fail", "unknown"})
SEVERITIES = frozenset({"high", "medium", "low"})
PRIORITIES = frozenset({"high", "medium", "low"})
FINDING_CATEGORIES = frozenset({
"hard_constraint", "fact_conflict", "evidence_reference", "temporal_spatial",
"knowledge_scope", "ability_cost", "output_contract",
})
INPUT_FIELDS = frozenset({
"schemaVersion", "runId", "sampleId", "opaqueArmId", "candidateVersion",
"candidateSha256", "candidateBody", "contextSnapshotSha256", "fineOutline",
"hardConstraints", "factEvidence", "proseEvidence", "asOf",
"authorizationSnapshotId", "inputSha256",
})
REPORT_FIELDS = frozenset({
"schemaVersion", "runId", "sampleId", "opaqueArmId", "inputSha256",
"candidateVersion", "candidateSha256", "contextSnapshotSha256",
"modelReceiptSha256", "claims", "findings", "assertionVerdicts",
"hardConstraintVerdicts", "newSettingCandidates", "evidenceGaps", "status",
"reportSha256",
})
SAFE_DIAGNOSTIC_VERSION = "semantic-diagnostic-v1"
SAFE_DIAGNOSTIC_MAX_COUNT = 10_000
SAFE_DIAGNOSTIC_SECTIONS = frozenset({
"input", "model_output", "claims", "findings", "assertion_verdicts",
"hard_constraint_verdicts", "new_setting_candidates", "evidence_gaps",
"report", "runtime",
})
SAFE_DIAGNOSTIC_REASON_CODES = frozenset({
"ARRAY_REQUIRED", "CONTRACT_INVALID", "DUPLICATE_ID", "ENUM_INVALID",
"EVIDENCE_ID_INVALID", "EVIDENCE_REQUIRED", "FIELD_SET_INVALID",
"GAP_REASON_FORBIDDEN", "GAP_REASON_REQUIRED", "HASH_INVALID",
"ID_ORDER_MISMATCH", "INTEGER_RANGE_INVALID", "MODEL_VERSION_INVALID",
"NON_EMPTY_STRING_REQUIRED", "OBJECT_REQUIRED", "QUOTE_NOT_FOUND",
"REFERENCE_LEAKAGE", "RUNTIME_FAILED", "SEMANTIC_BLOCKED",
})
SAFE_PRIMARY_CODE_PATTERN = re.compile(r"^SEMANTIC_[A-Z0-9_]{1,95}$")
SAFE_BLOCKING_COUNT_FIELDS = (
"highFindings", "failedAssertions", "failedHardConstraints",
"conflictingClaims", "evidenceGaps", "unknownAssertions",
"unknownHardConstraints", "unknownClaims",
)
@runtime_checkable
class ModelRunner(Protocol):
def run(self, *, adapter_role: str, model_input: Mapping[str, Any], output_schema: Mapping[str, Any]) -> Mapping[str, Any]: ...
def _diagnostic_section(path: str) -> str:
"""把 adapter 自己生成的字段路径投影成固定区段,不保留索引或原始路径。"""
prefixes = (
("$.claims", "claims"),
("$.findings", "findings"),
("$.assertionVerdicts", "assertion_verdicts"),
("$.hardConstraintVerdicts", "hard_constraint_verdicts"),
("$.newSettingCandidates", "new_setting_candidates"),
("$.evidenceGaps", "evidence_gaps"),
)
for prefix, section in prefixes:
if path.startswith(prefix):
return section
return "model_output"
def _diagnostic_section_for_error(path: str, code: str) -> str:
_reason, default_section = _default_diagnostic(code)
if default_section in {"input", "report", "runtime"}:
return default_section
return _diagnostic_section(path)
def _default_diagnostic(code: str) -> tuple[str, str]:
if code in {"SEMANTIC_DETECTOR_RUNTIME_FAILED", "SEMANTIC_DETECTOR_RUNNER_INVALID"}:
return "RUNTIME_FAILED", "runtime"
if code == "SEMANTIC_DETECTOR_LEAKAGE_DETECTED":
return "REFERENCE_LEAKAGE", "input"
if code == "SEMANTIC_DETECTOR_QUOTE_NOT_FOUND":
return "QUOTE_NOT_FOUND", "model_output"
if "INPUT" in code or "CANDIDATE_HASH" in code:
return "CONTRACT_INVALID", "input"
if "REPORT" in code or "STATUS" in code or "OFFSET" in code:
return "CONTRACT_INVALID", "report"
return "CONTRACT_INVALID", "model_output"
def _safe_primary_code(value: Any) -> str:
if isinstance(value, str) and SAFE_PRIMARY_CODE_PATTERN.fullmatch(value):
return value
return "SEMANTIC_DETECTOR_INVALID"
class SemanticDetectorContractError(ValueError):
def __init__(
self,
code: str,
message: str,
*,
causes: Sequence[str] = (),
reason_code: str | None = None,
section: str | None = None,
) -> None:
super().__init__(message)
self.code = code
self.causes = tuple(cause for cause in causes if cause != code)
self.acceptance_eligible = False
default_reason, default_section = _default_diagnostic(code)
self.reason_code = reason_code if reason_code in SAFE_DIAGNOSTIC_REASON_CODES else default_reason
self.section = section if section in SAFE_DIAGNOSTIC_SECTIONS else default_section
def _canonical_json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def canonical_sha256(value: Any) -> str:
return "sha256:" + hashlib.sha256(_canonical_json(value).encode("utf-8")).hexdigest()
def _object(value: Any, path: str, required: frozenset[str], optional: frozenset[str] = frozenset(), *, code: str) -> Mapping[str, Any]:
if not isinstance(value, Mapping):
raise SemanticDetectorContractError(
code, f"{path} 必须是对象", reason_code="OBJECT_REQUIRED",
section=_diagnostic_section_for_error(path, code),
)
missing = sorted(required - set(value))
extra = sorted(set(value) - required - optional)
if missing or extra:
raise SemanticDetectorContractError(
code, f"{path} 字段非法,missing={missing}, extra={extra}",
reason_code="FIELD_SET_INVALID", section=_diagnostic_section_for_error(path, code),
)
return value
def _array(value: Any, path: str, *, code: str) -> list[Any]:
if not isinstance(value, list):
raise SemanticDetectorContractError(
code, f"{path} 必须是数组", reason_code="ARRAY_REQUIRED",
section=_diagnostic_section_for_error(path, code),
)
return value
def _string(value: Any, path: str, *, code: str) -> str:
if not isinstance(value, str) or not value.strip():
raise SemanticDetectorContractError(
code, f"{path} 必须是非空字符串", reason_code="NON_EMPTY_STRING_REQUIRED",
section=_diagnostic_section_for_error(path, code),
)
return value
def _integer(value: Any, path: str, *, minimum: int, code: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < minimum:
raise SemanticDetectorContractError(
code, f"{path} 必须是大于等于 {minimum} 的整数",
reason_code="INTEGER_RANGE_INVALID", section=_diagnostic_section_for_error(path, code),
)
return value
def _hash(value: Any, path: str, *, code: str) -> str:
text = _string(value, path, code=code)
if not HASH_PATTERN.fullmatch(text):
raise SemanticDetectorContractError(
code, f"{path} 必须是规范 SHA-256", reason_code="HASH_INVALID",
section=_diagnostic_section_for_error(path, code),
)
return text
def _safe_reference(value: Any, path: str, *, code: str) -> str:
text = _string(value, path, code=code)
# WHY: 路径穿越用 contains_path_traversal 做「路径分量级」精确判定,不用 `".." in text` 子串匹配——
# 子串匹配会把省略号 `...`(含子串 `..`)误判为路径穿越,冤杀含省略号的合法引用;
# 精确判定只拦 a/../b 这类真实穿越,臂名与 raw 路径仍由其余条件保留拦截。
if text in REAL_ARM_NAMES or text.startswith(("/", "file:")) or contains_path_traversal(text) or RAW_PATH_PATTERN.search(text):
raise SemanticDetectorContractError(
"SEMANTIC_DETECTOR_LEAKAGE_DETECTED",
f"{path} 含真实臂名或 raw 路径",
reason_code="REFERENCE_LEAKAGE",
section="input",
)
return text
def _source_ref(value: Any, path: str, *, code: str, as_of: int | None = None) -> Mapping[str, Any]:
ref = _object(
value,
path,
frozenset({"sourceId", "sourceVersion"}),
frozenset({"chapter", "blockId", "startCodePoint", "endCodePoint", "contentSha256"}),
code=code,
)
_safe_reference(ref["sourceId"], f"{path}.sourceId", code=code)
_safe_reference(ref["sourceVersion"], f"{path}.sourceVersion", code=code)
if "chapter" in ref:
chapter = _integer(ref["chapter"], f"{path}.chapter", minimum=1, code=code)
if as_of is not None and chapter > as_of:
raise SemanticDetectorContractError(code, f"{path}.chapter 越过冻结线")
if "blockId" in ref:
block_id = ref["blockId"]
if isinstance(block_id, int) and not isinstance(block_id, bool):
_integer(block_id, f"{path}.blockId", minimum=1, code=code)
else:
_safe_reference(block_id, f"{path}.blockId", code=code)
if "contentSha256" in ref:
_hash(ref["contentSha256"], f"{path}.contentSha256", code=code)
has_start = "startCodePoint" in ref
has_end = "endCodePoint" in ref
if has_start != has_end:
raise SemanticDetectorContractError(code, f"{path} 字符区间必须成对出现")
if has_start:
start = _integer(ref["startCodePoint"], f"{path}.startCodePoint", minimum=0, code=code)
end = _integer(ref["endCodePoint"], f"{path}.endCodePoint", minimum=1, code=code)
if end <= start:
raise SemanticDetectorContractError(code, f"{path} 字符区间倒置")
return ref
def _stable_ids(items: list[Any], path: str, id_field: str, *, code: str) -> list[str]:
result: list[str] = []
for index, raw in enumerate(items):
if not isinstance(raw, Mapping):
raise SemanticDetectorContractError(code, f"{path}[{index}] 必须是对象")
result.append(_string(raw.get(id_field), f"{path}[{index}].{id_field}", code=code))
if len(result) != len(set(result)):
raise SemanticDetectorContractError(code, f"{path}.{id_field} 不得重复")
return result
def validate_semantic_detector_input(value: Any) -> dict[str, Any]:
code = "SEMANTIC_DETECTOR_INPUT_SCHEMA_INVALID"
payload = _object(value, "$", INPUT_FIELDS, code=code)
if payload["schemaVersion"] != INPUT_VERSION:
raise SemanticDetectorContractError(code, "$.schemaVersion 版本不受支持")
for field in ("runId", "sampleId", "authorizationSnapshotId"):
_string(payload[field], f"$.{field}", code=code)
_safe_reference(payload["opaqueArmId"], "$.opaqueArmId", code=code)
_integer(payload["candidateVersion"], "$.candidateVersion", minimum=1, code=code)
body = _string(payload["candidateBody"], "$.candidateBody", code=code)
candidate_hash = _hash(payload["candidateSha256"], "$.candidateSha256", code=code)
if candidate_hash != "sha256:" + hashlib.sha256(body.encode("utf-8")).hexdigest():
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_CANDIDATE_HASH_MISMATCH", "候选哈希与正文不一致")
_hash(payload["contextSnapshotSha256"], "$.contextSnapshotSha256", code=code)
as_of = _integer(payload["asOf"], "$.asOf", minimum=1, code=code)
outline = _object(
payload["fineOutline"], "$.fineOutline",
frozenset({"sourceRef", "hardConstraints", "adjustableBeats", "declaredNewFacts"}), code=code,
)
_source_ref(outline["sourceRef"], "$.fineOutline.sourceRef", code=code, as_of=None)
constraints = _array(payload["hardConstraints"], "$.hardConstraints", code=code)
constraint_ids = _stable_ids(constraints, "$.hardConstraints", "constraintId", code=code)
outline_ids = _stable_ids(_array(outline["hardConstraints"], "$.fineOutline.hardConstraints", code=code), "$.fineOutline.hardConstraints", "constraintId", code=code)
if constraint_ids != outline_ids:
raise SemanticDetectorContractError(code, "细纲与顶层硬约束 ID/顺序不一致")
for index, item in enumerate(constraints):
_string(item.get("text"), f"$.hardConstraints[{index}].text", code=code)
for index, beat in enumerate(_array(outline["adjustableBeats"], "$.fineOutline.adjustableBeats", code=code)):
_string(beat, f"$.fineOutline.adjustableBeats[{index}]", code=code)
declared_ids: list[str] = []
for index, raw in enumerate(_array(outline["declaredNewFacts"], "$.fineOutline.declaredNewFacts", code=code)):
item = _object(raw, f"$.fineOutline.declaredNewFacts[{index}]", frozenset({"factId", "text", "sourceRef"}), code=code)
declared_ids.append(_string(item["factId"], f"$.fineOutline.declaredNewFacts[{index}].factId", code=code))
_string(item["text"], f"$.fineOutline.declaredNewFacts[{index}].text", code=code)
_source_ref(item["sourceRef"], f"$.fineOutline.declaredNewFacts[{index}].sourceRef", code=code)
if len(declared_ids) != len(set(declared_ids)):
raise SemanticDetectorContractError(code, "declaredNewFacts.factId 不得重复")
facts = _array(payload["factEvidence"], "$.factEvidence", code=code)
fact_ids = _stable_ids(facts, "$.factEvidence", "evidenceId", code=code)
for index, item in enumerate(facts):
_object(item, f"$.factEvidence[{index}]", frozenset({"evidenceId", "fact", "sourceType", "sourceRef", "contentSha256", "riskLevel"}), code=code)
_string(item.get("fact"), f"$.factEvidence[{index}].fact", code=code)
if item.get("sourceType") not in {"historical_prose", "formal_setting", "canonical_state", "fine_outline_declared_new"}:
raise SemanticDetectorContractError(code, f"$.factEvidence[{index}].sourceType 非法")
_source_ref(item.get("sourceRef"), f"$.factEvidence[{index}].sourceRef", code=code, as_of=as_of)
_hash(item.get("contentSha256"), f"$.factEvidence[{index}].contentSha256", code=code)
if item.get("riskLevel") not in {"low", "medium", "high"}:
raise SemanticDetectorContractError(code, f"$.factEvidence[{index}].riskLevel 非法")
prose = _array(payload["proseEvidence"], "$.proseEvidence", code=code)
prose_ids = _stable_ids(prose, "$.proseEvidence", "evidenceId", code=code)
for index, item in enumerate(prose):
_object(item, f"$.proseEvidence[{index}]", frozenset({"evidenceId", "chapter", "sourceRef", "contentSha256", "purpose", "text", "isRecentBaseline"}), code=code)
chapter = _integer(item.get("chapter"), f"$.proseEvidence[{index}].chapter", minimum=1, code=code)
if chapter > as_of:
raise SemanticDetectorContractError(code, f"$.proseEvidence[{index}] 越过冻结线")
_source_ref(item.get("sourceRef"), f"$.proseEvidence[{index}].sourceRef", code=code, as_of=as_of)
text = _string(item.get("text"), f"$.proseEvidence[{index}].text", code=code)
content_hash = _hash(item.get("contentSha256"), f"$.proseEvidence[{index}].contentSha256", code=code)
if content_hash != "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest():
raise SemanticDetectorContractError(code, f"$.proseEvidence[{index}] 正文哈希不一致")
_string(item.get("purpose"), f"$.proseEvidence[{index}].purpose", code=code)
if not isinstance(item.get("isRecentBaseline"), bool):
raise SemanticDetectorContractError(code, f"$.proseEvidence[{index}].isRecentBaseline 非法")
supplied = _hash(payload["inputSha256"], "$.inputSha256", code=code)
expected = canonical_sha256({key: payload[key] for key in payload if key != "inputSha256"})
if supplied != expected:
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_INPUT_HASH_MISMATCH", "inputSha256 与输入不一致")
normalized = json.loads(_canonical_json(payload))
normalized["_expectedAssertionIds"] = fact_ids
normalized["_expectedConstraintIds"] = constraint_ids
normalized["_evidenceIds"] = sorted(set(fact_ids + prose_ids + constraint_ids))
return normalized
def build_semantic_model_input(value: Any, correction: Mapping[str, Any] | None = None) -> dict[str, Any]:
# WHY: correction 只在「模型输入层」可选透传,绝不进入 detector_input——
# validate_semantic_detector_input 对 detector_input 执行固定 INPUT_FIELDS 闭集校验,
# 多塞 correction 会被当成非法 extra 字段拒掉。把 correction 放在这里拼进模型输入,
# 既不破坏正常输入的闭集校验,又能把「上一轮不合格产出 + 出错原因」喂回模型做自我纠错。
payload = validate_semantic_detector_input(value)
model_input: dict[str, Any] = {
"candidateBody": payload["candidateBody"],
"fineOutline": payload["fineOutline"],
"hardConstraints": payload["hardConstraints"],
"factEvidence": [
{
"assertionId": item["evidenceId"],
"evidenceId": item["evidenceId"],
"fact": item["fact"],
"sourceType": item.get("sourceType", "fact_evidence"),
"riskLevel": item.get("riskLevel", "low"),
}
for item in payload["factEvidence"]
],
"proseEvidence": [
{"evidenceId": item["evidenceId"], "chapter": item.get("chapter"), "purpose": item.get("purpose"), "text": item["text"]}
for item in payload["proseEvidence"]
],
"asOf": payload["asOf"],
}
if correction is not None:
model_input["correction"] = dict(correction)
return model_input
def _quote_location(body: str, quote: Any, path: str) -> tuple[str, int, int]:
text = _string(quote, path, code="SEMANTIC_DETECTOR_MODEL_OUTPUT_INVALID")
# 0 次 = 引文根本不在候选里 = 编造证据,必须失败关闭(防编造核心不动)。
if text not in body:
raise SemanticDetectorContractError(
"SEMANTIC_DETECTOR_QUOTE_NOT_FOUND",
f"{path} 引文未在候选正文中出现(不得编造证据)",
reason_code="QUOTE_NOT_FOUND",
section=_diagnostic_section(path),
)
# ≥1 次:由确定性代码绑定到首次出现,不再因多处出现而失败关闭——
# 大模型无法可靠数出一句话在长文里出现几次,精确唯一计数不是可信门槛。
start = body.index(text)
return text, start, start + len(text)
def _evidence_ids(value: Any, path: str, allowed: set[str]) -> list[str]:
ids = [_string(item, f"{path}[{index}]", code="SEMANTIC_DETECTOR_MODEL_OUTPUT_INVALID") for index, item in enumerate(_array(value, path, code="SEMANTIC_DETECTOR_MODEL_OUTPUT_INVALID"))]
if len(ids) != len(set(ids)) or any(item not in allowed for item in ids):
raise SemanticDetectorContractError(
"SEMANTIC_DETECTOR_MODEL_OUTPUT_INVALID",
f"{path} 含重复或越界证据 ID",
reason_code="EVIDENCE_ID_INVALID",
section=_diagnostic_section(path),
)
return ids
def _validate_model_output(value: Any, payload: Mapping[str, Any]) -> dict[str, Any]:
code = "SEMANTIC_DETECTOR_MODEL_OUTPUT_INVALID"
draft = _object(value, "$", frozenset({"schemaVersion", "claims", "findings", "assertionVerdicts", "hardConstraintVerdicts", "newSettingCandidates", "evidenceGaps"}), code=code)
if draft["schemaVersion"] != MODEL_OUTPUT_VERSION:
raise SemanticDetectorContractError(
code, "模型输出版本非法", reason_code="MODEL_VERSION_INVALID",
section="model_output",
)
body = payload["candidateBody"]
allowed_evidence = set(payload["_evidenceIds"])
claims: list[dict[str, Any]] = []
claim_ids: list[str] = []
coverage_states = {"supported", "declared_new", "unknown", "conflict"}
for index, raw in enumerate(_array(draft["claims"], "$.claims", code=code)):
item = _object(
raw,
f"$.claims[{index}]",
frozenset({"claimId", "factType", "text", "candidateQuote", "coverageState", "evidenceIds"}),
frozenset({"gapReason"}),
code=code,
)
claim_id = _string(item["claimId"], f"$.claims[{index}].claimId", code=code)
claim_ids.append(claim_id)
if item["coverageState"] not in coverage_states:
raise SemanticDetectorContractError(
code, f"$.claims[{index}].coverageState 非法",
reason_code="ENUM_INVALID", section="claims",
)
if item["coverageState"] == "unknown" and "gapReason" not in item:
raise SemanticDetectorContractError(
code, f"$.claims[{index}] unknown 必须携带 gapReason",
reason_code="GAP_REASON_REQUIRED", section="claims",
)
quote, start, end = _quote_location(body, item["candidateQuote"], f"$.claims[{index}].candidateQuote")
bound = {
"claimId": claim_id,
"factType": _string(item["factType"], f"$.claims[{index}].factType", code=code),
"text": _string(item["text"], f"$.claims[{index}].text", code=code),
"candidateSha256": payload["candidateSha256"],
"candidateQuote": quote,
"startCodePoint": start,
"endCodePoint": end,
"coverageState": item["coverageState"],
"evidenceIds": _evidence_ids(item["evidenceIds"], f"$.claims[{index}].evidenceIds", allowed_evidence),
}
# WHY: 模型可能在已判定的 claim 上残留解释性 gapReason;它不改变闭集状态,
# 绑定时确定性丢弃,避免把无害冗余升级为整份报告失败。unknown 仍须在上方校验非空原因。
if item["coverageState"] == "unknown":
bound["gapReason"] = _string(item["gapReason"], f"$.claims[{index}].gapReason", code=code)
claims.append(bound)
if len(claim_ids) != len(set(claim_ids)):
raise SemanticDetectorContractError(
code, "claimId 不得重复", reason_code="DUPLICATE_ID", section="claims"
)
findings: list[dict[str, Any]] = []
finding_ids: list[str] = []
for index, raw in enumerate(_array(draft["findings"], "$.findings", code=code)):
item = _object(raw, f"$.findings[{index}]", frozenset({"findingId", "severity", "category", "candidateQuote", "evidenceIds", "message"}), code=code)
finding_id = _string(item["findingId"], f"$.findings[{index}].findingId", code=code)
finding_ids.append(finding_id)
if item["severity"] not in SEVERITIES or item["category"] not in FINDING_CATEGORIES:
raise SemanticDetectorContractError(
code, f"$.findings[{index}] 枚举非法",
reason_code="ENUM_INVALID", section="findings",
)
quote, start, end = _quote_location(body, item["candidateQuote"], f"$.findings[{index}].candidateQuote")
findings.append({
"findingId": finding_id, "severity": item["severity"], "category": item["category"],
"candidateSha256": payload["candidateSha256"], "candidateQuote": quote,
"startCodePoint": start, "endCodePoint": end,
"evidenceIds": _evidence_ids(item["evidenceIds"], f"$.findings[{index}].evidenceIds", allowed_evidence),
"message": _string(item["message"], f"$.findings[{index}].message", code=code),
})
if len(finding_ids) != len(set(finding_ids)):
raise SemanticDetectorContractError(
code, "findingId 不得重复", reason_code="DUPLICATE_ID", section="findings"
)
def verdicts(field: str, id_field: str, expected_ids: list[str]) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
actual_ids: list[str] = []
for index, raw in enumerate(_array(draft[field], f"$.{field}", code=code)):
item = _object(raw, f"$.{field}[{index}]", frozenset({id_field, "verdict", "candidateQuote", "evidenceIds"}), frozenset({"gapReason"}), code=code)
stable_id = _string(item[id_field], f"$.{field}[{index}].{id_field}", code=code)
actual_ids.append(stable_id)
if item["verdict"] not in VERDICTS:
raise SemanticDetectorContractError(
code, f"$.{field}[{index}].verdict 非法",
reason_code="ENUM_INVALID", section=_diagnostic_section(f"$.{field}"),
)
if item["verdict"] == "unknown" and "gapReason" not in item:
raise SemanticDetectorContractError(
code, f"$.{field}[{index}] unknown 必须携带 gapReason",
reason_code="GAP_REASON_REQUIRED", section=_diagnostic_section(f"$.{field}"),
)
quote, start, end = _quote_location(body, item["candidateQuote"], f"$.{field}[{index}].candidateQuote")
bound = {
id_field: stable_id, "candidateSha256": payload["candidateSha256"], "verdict": item["verdict"],
"candidateQuote": quote, "startCodePoint": start, "endCodePoint": end,
"evidenceIds": _evidence_ids(item["evidenceIds"], f"$.{field}[{index}].evidenceIds", allowed_evidence),
}
# WHY: pass/fail 已是终态,额外 gapReason 不参与可信绑定;统一丢弃可消除模型格式噪声,
# 但 unknown 的非空原因仍由上方强制校验,其他字段和证据约束保持失败关闭。
if item["verdict"] == "unknown":
bound["gapReason"] = _string(item["gapReason"], f"$.{field}[{index}].gapReason", code=code)
result.append(bound)
# WHY: ID 集完整且无重复时,条目顺序不影响语义;由适配器按冻结输入顺序重排,
# 避免模型把同一组裁决按字典序返回而被误判为缺失。集合不完整、重复或越界仍失败关闭。
if (
len(actual_ids) != len(expected_ids)
or len(actual_ids) != len(set(actual_ids))
or set(actual_ids) != set(expected_ids)
):
raise SemanticDetectorContractError(
code, f"$.{field} ID 集或顺序不一致",
reason_code="ID_ORDER_MISMATCH", section=_diagnostic_section(f"$.{field}"),
)
by_id = {item[id_field]: item for item in result}
return [by_id[item] for item in expected_ids]
assertion_verdicts = verdicts("assertionVerdicts", "assertionId", list(payload["_expectedAssertionIds"]))
constraint_verdicts = verdicts("hardConstraintVerdicts", "constraintId", list(payload["_expectedConstraintIds"]))
settings: list[dict[str, Any]] = []
setting_ids: list[str] = []
for index, raw in enumerate(_array(draft["newSettingCandidates"], "$.newSettingCandidates", code=code)):
item = _object(raw, f"$.newSettingCandidates[{index}]", frozenset({"settingId", "factType", "text", "candidateQuote"}), code=code)
setting_id = _string(item["settingId"], f"$.newSettingCandidates[{index}].settingId", code=code)
setting_ids.append(setting_id)
quote, start, end = _quote_location(body, item["candidateQuote"], f"$.newSettingCandidates[{index}].candidateQuote")
settings.append({
"settingId": setting_id, "factType": _string(item["factType"], f"$.newSettingCandidates[{index}].factType", code=code),
"text": _string(item["text"], f"$.newSettingCandidates[{index}].text", code=code),
"candidateSha256": payload["candidateSha256"], "candidateQuote": quote,
"startCodePoint": start, "endCodePoint": end,
})
if len(setting_ids) != len(set(setting_ids)):
raise SemanticDetectorContractError(
code, "settingId 不得重复", reason_code="DUPLICATE_ID",
section="new_setting_candidates",
)
gaps: list[dict[str, Any]] = []
gap_ids: list[str] = []
for index, raw in enumerate(_array(draft["evidenceGaps"], "$.evidenceGaps", code=code)):
item = _object(raw, f"$.evidenceGaps[{index}]", frozenset({"gapId", "query", "reason", "priority", "candidateQuote"}), code=code)
gap_id = _string(item["gapId"], f"$.evidenceGaps[{index}].gapId", code=code)
gap_ids.append(gap_id)
if item["priority"] not in PRIORITIES:
raise SemanticDetectorContractError(
code, f"$.evidenceGaps[{index}].priority 非法",
reason_code="ENUM_INVALID", section="evidence_gaps",
)
quote, start, end = _quote_location(body, item["candidateQuote"], f"$.evidenceGaps[{index}].candidateQuote")
gaps.append({
"gapId": gap_id, "query": _string(item["query"], f"$.evidenceGaps[{index}].query", code=code),
"reason": _string(item["reason"], f"$.evidenceGaps[{index}].reason", code=code),
"priority": item["priority"], "candidateSha256": payload["candidateSha256"],
"candidateQuote": quote, "startCodePoint": start, "endCodePoint": end,
})
if len(gap_ids) != len(set(gap_ids)):
raise SemanticDetectorContractError(
code, "gapId 不得重复", reason_code="DUPLICATE_ID", section="evidence_gaps"
)
return {
"claims": claims, "findings": findings, "assertionVerdicts": assertion_verdicts,
"hardConstraintVerdicts": constraint_verdicts, "newSettingCandidates": settings,
"evidenceGaps": gaps,
}
def build_semantic_detection(value: Any, detector_input: Mapping[str, Any], *, model_receipt_sha256: str) -> dict[str, Any]:
payload = validate_semantic_detector_input(detector_input)
receipt_hash = _hash(model_receipt_sha256, "modelReceiptSha256", code="SEMANTIC_DETECTOR_RECEIPT_BINDING_MISMATCH")
content = _validate_model_output(value, payload)
has_unknown = any(item["verdict"] == "unknown" for field in ("assertionVerdicts", "hardConstraintVerdicts") for item in content[field]) or any(item["coverageState"] == "unknown" for item in content["claims"])
has_failure = any(item["severity"] == "high" for item in content["findings"]) or any(item["verdict"] == "fail" for field in ("assertionVerdicts", "hardConstraintVerdicts") for item in content[field]) or any(item["coverageState"] == "conflict" for item in content["claims"])
# 高危优先:有高危发现/失败裁决/冲突即 failed(不被证据缺口掩盖);无高危仅有证据缺口/未知才是 needs_evidence。
status = "failed" if has_failure else ("needs_evidence" if content["evidenceGaps"] or has_unknown else "passed")
report = {
"schemaVersion": REPORT_VERSION,
"runId": payload["runId"], "sampleId": payload["sampleId"], "opaqueArmId": payload["opaqueArmId"],
"inputSha256": payload["inputSha256"], "candidateVersion": payload["candidateVersion"],
"candidateSha256": payload["candidateSha256"], "contextSnapshotSha256": payload["contextSnapshotSha256"],
"modelReceiptSha256": receipt_hash, **content, "status": status,
}
report["reportSha256"] = canonical_sha256(report)
return report
def validate_semantic_detector_report(value: Any, detector_input: Mapping[str, Any], *, model_receipt_sha256: str | None = None) -> dict[str, Any]:
payload = validate_semantic_detector_input(detector_input)
code = "SEMANTIC_DETECTOR_REPORT_SCHEMA_INVALID"
report = _object(value, "$", REPORT_FIELDS, code=code)
if report["schemaVersion"] != REPORT_VERSION:
raise SemanticDetectorContractError(code, "报告版本非法")
expected = {
"runId": payload["runId"], "sampleId": payload["sampleId"], "opaqueArmId": payload["opaqueArmId"],
"inputSha256": payload["inputSha256"], "candidateVersion": payload["candidateVersion"],
"candidateSha256": payload["candidateSha256"], "contextSnapshotSha256": payload["contextSnapshotSha256"],
}
if any(report.get(field) != expected_value for field, expected_value in expected.items()):
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_INPUT_BINDING_MISMATCH", "报告未绑定当前输入")
_hash(report["modelReceiptSha256"], "$.modelReceiptSha256", code=code)
if model_receipt_sha256 is not None and report["modelReceiptSha256"] != model_receipt_sha256:
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_RECEIPT_BINDING_MISMATCH", "报告回执不一致")
for field in ("claims", "findings", "assertionVerdicts", "hardConstraintVerdicts", "newSettingCandidates", "evidenceGaps"):
_array(report[field], f"$.{field}", code=code)
for index, item in enumerate(report[field]):
if not isinstance(item, Mapping):
raise SemanticDetectorContractError(code, f"$.{field}[{index}] 必须是对象")
if item.get("candidateSha256") != payload["candidateSha256"]:
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_CANDIDATE_BINDING_MISMATCH", f"$.{field}[{index}] 未绑定候选")
quote, start, end = _quote_location(payload["candidateBody"], item.get("candidateQuote"), f"$.{field}[{index}].candidateQuote")
if item.get("startCodePoint") != start or item.get("endCodePoint") != end:
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_OFFSET_BINDING_MISMATCH", f"$.{field}[{index}] offset 非 adapter 计算值")
nested_fields = {
"claims": (frozenset({"claimId", "factType", "text", "candidateSha256", "candidateQuote", "startCodePoint", "endCodePoint", "coverageState", "evidenceIds"}), frozenset({"gapReason"})),
"findings": (frozenset({"findingId", "severity", "category", "candidateSha256", "candidateQuote", "startCodePoint", "endCodePoint", "evidenceIds", "message"}), frozenset()),
"assertionVerdicts": (frozenset({"assertionId", "candidateSha256", "verdict", "candidateQuote", "startCodePoint", "endCodePoint", "evidenceIds"}), frozenset({"gapReason"})),
"hardConstraintVerdicts": (frozenset({"constraintId", "candidateSha256", "verdict", "candidateQuote", "startCodePoint", "endCodePoint", "evidenceIds"}), frozenset({"gapReason"})),
"newSettingCandidates": (frozenset({"settingId", "factType", "text", "candidateSha256", "candidateQuote", "startCodePoint", "endCodePoint"}), frozenset()),
"evidenceGaps": (frozenset({"gapId", "query", "reason", "priority", "candidateSha256", "candidateQuote", "startCodePoint", "endCodePoint"}), frozenset()),
}
for field, (required, optional) in nested_fields.items():
for index, item in enumerate(report[field]):
_object(item, f"$.{field}[{index}]", required, optional, code=code)
expected_ids = {
"assertionVerdicts": ("assertionId", payload["_expectedAssertionIds"]),
"hardConstraintVerdicts": ("constraintId", payload["_expectedConstraintIds"]),
}
for field, (id_field, expected_values) in expected_ids.items():
if [item[id_field] for item in report[field]] != list(expected_values):
raise SemanticDetectorContractError(code, f"$.{field} ID 集或顺序非法")
for field, id_field in (("claims", "claimId"), ("findings", "findingId"), ("newSettingCandidates", "settingId"), ("evidenceGaps", "gapId")):
values = [item[id_field] for item in report[field]]
if len(values) != len(set(values)):
raise SemanticDetectorContractError(code, f"$.{field}.{id_field} 不得重复")
expected_status = "failed" if any(item.get("severity") == "high" for item in report["findings"]) or any(item.get("verdict") == "fail" for field in ("assertionVerdicts", "hardConstraintVerdicts") for item in report[field]) or any(item.get("coverageState") == "conflict" for item in report["claims"]) else ("needs_evidence" if report["evidenceGaps"] or any(item.get("verdict") == "unknown" for field in ("assertionVerdicts", "hardConstraintVerdicts") for item in report[field]) or any(item.get("coverageState") == "unknown" for item in report["claims"]) else "passed")
if report["status"] != expected_status:
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_STATUS_MISMATCH", "报告状态与语义内容不一致")
supplied_hash = _hash(report["reportSha256"], "$.reportSha256", code=code)
if supplied_hash != canonical_sha256({key: report[key] for key in report if key != "reportSha256"}):
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_REPORT_HASH_MISMATCH", "报告哈希不一致")
return json.loads(_canonical_json(report))
def calculate_semantic_metrics(report: Mapping[str, Any], detector_input: Mapping[str, Any]) -> dict[str, int | float]:
validated = validate_semantic_detector_report(report, detector_input)
constraints = validated["hardConstraintVerdicts"]
return {
"highSeverityCount": sum(item["severity"] == "high" for item in validated["findings"]),
"hardConstraintCoverage": (sum(item["verdict"] == "pass" for item in constraints) / len(constraints) if constraints else 1.0),
"evidenceGapCount": len(validated["evidenceGaps"]),
}
def _bounded_count(value: int) -> int:
return min(max(int(value), 0), SAFE_DIAGNOSTIC_MAX_COUNT)
def _empty_blocking_counts() -> dict[str, int]:
return {field: 0 for field in SAFE_BLOCKING_COUNT_FIELDS}
def _contract_safe_diagnostic(
error: SemanticDetectorContractError, *, attempt_count: int, correction_count: int | None = None
) -> dict[str, Any]:
attempts = _bounded_count(attempt_count)
corrections = _bounded_count(
max(attempts - 1, 0) if correction_count is None else correction_count
)
return {
"schemaVersion": SAFE_DIAGNOSTIC_VERSION,
"outcome": "invalid",
"primaryCode": _safe_primary_code(error.code),
"reasonCode": error.reason_code,
"section": error.section,
"attemptCount": attempts,
"correctionCount": min(corrections, max(attempts - 1, 0)),
"blockingCounts": _empty_blocking_counts(),
}
def build_safe_semantic_diagnostic(value: Mapping[str, Any]) -> dict[str, Any]:
"""从完整检测结果蒸馏固定闭集摘要;不复制任何模型文本、ID、引用或字段路径。"""
if value.get("ok") is not True or not isinstance(value.get("report"), Mapping):
candidate = value.get("safeDiagnostic")
if not isinstance(candidate, Mapping):
candidate = {}
reason = candidate.get("reasonCode")
section = candidate.get("section")
attempts = candidate.get("attemptCount")
corrections = candidate.get("correctionCount")
safe_attempts = _bounded_count(
attempts if isinstance(attempts, int) and not isinstance(attempts, bool) else 0
)
safe_corrections = _bounded_count(
corrections if isinstance(corrections, int) and not isinstance(corrections, bool) else 0
)
return {
"schemaVersion": SAFE_DIAGNOSTIC_VERSION,
"outcome": "invalid",
"primaryCode": _safe_primary_code(candidate.get("primaryCode") or value.get("primaryCode")),
"reasonCode": reason if reason in SAFE_DIAGNOSTIC_REASON_CODES else "CONTRACT_INVALID",
"section": section if section in SAFE_DIAGNOSTIC_SECTIONS else "model_output",
"attemptCount": safe_attempts,
"correctionCount": min(safe_corrections, max(safe_attempts - 1, 0)),
"blockingCounts": _empty_blocking_counts(),
}
report = value["report"]
status = str(report.get("status") or "")
if status not in {"failed", "needs_evidence"}:
raise SemanticDetectorContractError(
"SEMANTIC_DETECTOR_STATUS_MISMATCH",
"安全诊断只接受阻断态报告",
section="report",
)
findings = report.get("findings") if isinstance(report.get("findings"), list) else []
assertions = report.get("assertionVerdicts") if isinstance(report.get("assertionVerdicts"), list) else []
constraints = report.get("hardConstraintVerdicts") if isinstance(report.get("hardConstraintVerdicts"), list) else []
claims = report.get("claims") if isinstance(report.get("claims"), list) else []
gaps = report.get("evidenceGaps") if isinstance(report.get("evidenceGaps"), list) else []
counts = {
"highFindings": _bounded_count(sum(isinstance(item, Mapping) and item.get("severity") == "high" for item in findings)),
"failedAssertions": _bounded_count(sum(isinstance(item, Mapping) and item.get("verdict") == "fail" for item in assertions)),
"failedHardConstraints": _bounded_count(sum(isinstance(item, Mapping) and item.get("verdict") == "fail" for item in constraints)),
"conflictingClaims": _bounded_count(sum(isinstance(item, Mapping) and item.get("coverageState") == "conflict" for item in claims)),
"evidenceGaps": _bounded_count(len(gaps)),
"unknownAssertions": _bounded_count(sum(isinstance(item, Mapping) and item.get("verdict") == "unknown" for item in assertions)),
"unknownHardConstraints": _bounded_count(sum(isinstance(item, Mapping) and item.get("verdict") == "unknown" for item in constraints)),
"unknownClaims": _bounded_count(sum(isinstance(item, Mapping) and item.get("coverageState") == "unknown" for item in claims)),
}
attempts = value.get("attemptCount")
corrections = value.get("correctionCount")
safe_attempts = _bounded_count(
attempts if isinstance(attempts, int) and not isinstance(attempts, bool) else 1
)
safe_corrections = _bounded_count(
corrections
if isinstance(corrections, int) and not isinstance(corrections, bool)
else max(safe_attempts - 1, 0)
)
return {
"schemaVersion": SAFE_DIAGNOSTIC_VERSION,
"outcome": status,
"primaryCode": "SEMANTIC_EVIDENCE_REQUIRED" if status == "needs_evidence" else "SEMANTIC_CHECK_FAILED",
"reasonCode": "EVIDENCE_REQUIRED" if status == "needs_evidence" else "SEMANTIC_BLOCKED",
"section": "report",
"attemptCount": safe_attempts,
"correctionCount": min(safe_corrections, max(safe_attempts - 1, 0)),
"blockingCounts": counts,
}
class ClaudeRuntimeModelRunner:
def __init__(self, profile: ExecutionProfile, *, runtime_callable: Any = None) -> None:
self.profile = profile
self.runtime_callable = runtime_callable or run_claude
def run(self, *, adapter_role: str, model_input: Mapping[str, Any], output_schema: Mapping[str, Any]) -> Mapping[str, Any]:
if self.runtime_callable is None or sha256_json is None:
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_RUNNER_INVALID", "claude_runtime 不可用")
if getattr(self.profile, "adapter_role", None) != adapter_role or getattr(self.profile, "json_schema", None) != output_schema:
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_RUNNER_INVALID", "ExecutionProfile 与 adapter 不一致")
result = self.runtime_callable(self.profile, model_input)
output = getattr(result, "structured_output", None)
receipt = getattr(result, "receipt", None)
if not isinstance(output, Mapping) or receipt is None:
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_RUNTIME_FAILED", "runtime 成功结果形状非法")
receipt_value = receipt.as_dict() if hasattr(receipt, "as_dict") else receipt
return {"structuredOutput": dict(output), "modelReceiptSha256": sha256_json(receipt_value)}
def _invoke_model_runner(model_runner: ModelRunner, model_input: Mapping[str, Any]) -> tuple[Any, str]:
method = getattr(model_runner, "run", None)
if not callable(method):
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_RUNNER_INVALID", "model_runner 未实现 run")
result = method(adapter_role="semantic_detector", model_input=model_input, output_schema=SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA)
if not isinstance(result, Mapping) or not isinstance(result.get("modelReceiptSha256"), str) or "structuredOutput" not in result:
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_RECEIPT_BINDING_MISMATCH", "runner 必须返回 structuredOutput 与回执哈希")
return result["structuredOutput"], result["modelReceiptSha256"]
def _failure(
error: SemanticDetectorContractError, *, attempt_count: int, correction_count: int
) -> dict[str, Any]:
return {
"ok": False,
"acceptanceEligible": False,
"status": "failed",
"primaryCode": error.code,
"causes": list(error.causes),
"message": str(error),
"safeDiagnostic": _contract_safe_diagnostic(
error,
attempt_count=attempt_count,
correction_count=correction_count,
),
}
def run_writer_semantic_detector(
detector_input: Mapping[str, Any],
*,
model_runner: ModelRunner,
max_corrections: int = 2,
max_runtime_retries: int = 1,
) -> dict[str, Any]:
# WHY: 检测模型(opus high)最常见的不合格是引文校验挂——它引了一句正文里没有的话。
# 这类错误自我纠错最对症:把上一轮原始产出和出错原因回喂给模型,让它换一句正文里真实存在的原话。
# 盲重试(不带上一轮产出)不一定收敛,所以纠错反馈里携带 previousDraft + error。
# max_corrections 给出硬上界(默认 2 轮纠错 = 总共最多 3 次调用),防止无限循环;
# 每次调用都走同一个 model_runner(生产中是 _BudgetedModelRunner),各自过预算账本。
attempt_count = 0
correction_count = 0
runtime_retry_count = 0
try:
if (
isinstance(max_runtime_retries, bool)
or not isinstance(max_runtime_retries, int)
or not 0 <= max_runtime_retries <= 1
):
raise SemanticDetectorContractError(
"SEMANTIC_DETECTOR_RUNNER_INVALID",
"max_runtime_retries 必须是 0 或 1",
)
normalized = validate_semantic_detector_input(detector_input)
public_input = {key: value for key, value in normalized.items() if not key.startswith("_")}
base_model_input = build_semantic_model_input(public_input)
correction: dict[str, Any] | None = None
for attempt in range(max_corrections + 1):
attempt_count = attempt + 1
# 首轮不带 correction;纠错轮在原始模型输入基础上追加 correction 字段透传给模型。
model_input = base_model_input if correction is None else {**base_model_input, "correction": correction}
draft: Any = None
try:
try:
draft, receipt_hash = _invoke_model_runner(model_runner, model_input)
except SemanticDetectorContractError:
raise
except Exception as exc:
runtime_code = getattr(exc, "primary_code", None) or getattr(exc, "code", None)
if isinstance(runtime_code, str) and runtime_code.startswith("SEMANTIC_DETECTOR_"):
raise SemanticDetectorContractError(runtime_code, "模型运行底座失败", causes=getattr(exc, "causes", ())) from exc
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_RUNTIME_FAILED", f"模型运行失败: {type(exc).__name__}") from exc
report = build_semantic_detection(draft, public_input, model_receipt_sha256=receipt_hash)
report = validate_semantic_detector_report(report, public_input, model_receipt_sha256=receipt_hash)
return {
"ok": True,
"acceptanceEligible": False,
"status": report["status"],
"report": report,
"metrics": calculate_semantic_metrics(report, public_input),
"attemptCount": attempt_count,
"correctionCount": correction_count,
}
except SemanticDetectorContractError as error:
# 纠错只对有「模型原始产出」的不合格有意义(build/validate 抛错时 draft 已存在)。
# runner 底座失败没有 draft(draft 仍为 None),纠错帮不上忙,直接失败关闭;
# 已用尽纠错轮次时也直接抛出最后一轮错误,返回 ok=False。
if draft is None:
# WHY: 带可信失败回执的瞬时 API 错误可以重新发送同一输入一次;它占用
# 现有三次总调用额度,不携带伪造 correction,也不重试本地合同/认证错误。
if (
error.code == "SEMANTIC_DETECTOR_API_ERROR"
and runtime_retry_count < max_runtime_retries
and attempt < max_corrections
):
runtime_retry_count += 1
correction = None
continue
raise
if attempt >= max_corrections:
raise
correction = {"previousDraft": draft, "error": str(error)}
if error.reason_code == "ID_ORDER_MISMATCH":
# WHY: 纠错轮明确给出冻结输入要求的完整 ID 集;适配器仍严格校验
# 缺失、重复、越界和绑定,提示只帮助模型修正格式,不放宽覆盖门禁。
correction["expectedVerdictIds"] = {
"assertionVerdicts": list(normalized["_expectedAssertionIds"]),
"hardConstraintVerdicts": list(normalized["_expectedConstraintIds"]),
}
correction_count += 1
# 循环必然在 return 或 raise 处退出,此处不可达。
raise SemanticDetectorContractError("SEMANTIC_DETECTOR_RUNTIME_FAILED", "纠错环意外退出")
except SemanticDetectorContractError as error:
return _failure(
error,
attempt_count=attempt_count,
correction_count=correction_count,
)
def _closed(properties: Mapping[str, Any], required: Sequence[str], optional: Sequence[str] = ()) -> dict[str, Any]:
return {"type": "object", "properties": dict(properties), "required": list(required), "additionalProperties": False}
_QUOTE = {"type": "string", "minLength": 1}
_IDS = {"type": "array", "uniqueItems": True, "items": {"type": "string", "minLength": 1}}
_VERDICT_SCHEMA = _closed(
{
"verdict": {"type": "string", "enum": sorted(VERDICTS)}, "candidateQuote": _QUOTE,
"evidenceIds": _IDS, "gapReason": {"type": "string", "minLength": 1},
},
["verdict", "candidateQuote", "evidenceIds"],
)
SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA = _closed(
{
"schemaVersion": {"const": MODEL_OUTPUT_VERSION},
"claims": {"type": "array", "items": _closed({
"claimId": {"type": "string", "minLength": 1},
"factType": {"type": "string", "minLength": 1},
"text": {"type": "string", "minLength": 1},
"candidateQuote": _QUOTE,
"coverageState": {"type": "string", "enum": ["supported", "declared_new", "unknown", "conflict"]},
"evidenceIds": _IDS,
"gapReason": {"type": "string", "minLength": 1},
}, ["claimId", "factType", "text", "candidateQuote", "coverageState", "evidenceIds"])},
"findings": {"type": "array", "items": _closed({
"findingId": {"type": "string", "minLength": 1}, "severity": {"type": "string", "enum": sorted(SEVERITIES)},
"category": {"type": "string", "enum": sorted(FINDING_CATEGORIES)}, "candidateQuote": _QUOTE,
"evidenceIds": _IDS, "message": {"type": "string", "minLength": 1},
}, ["findingId", "severity", "category", "candidateQuote", "evidenceIds", "message"])},
"assertionVerdicts": {"type": "array", "items": _closed({"assertionId": {"type": "string", "minLength": 1}, **_VERDICT_SCHEMA["properties"]}, ["assertionId", "verdict", "candidateQuote", "evidenceIds"])},
"hardConstraintVerdicts": {"type": "array", "items": _closed({"constraintId": {"type": "string", "minLength": 1}, **_VERDICT_SCHEMA["properties"]}, ["constraintId", "verdict", "candidateQuote", "evidenceIds"])},
"newSettingCandidates": {"type": "array", "items": _closed({
"settingId": {"type": "string", "minLength": 1}, "factType": {"type": "string", "minLength": 1},
"text": {"type": "string", "minLength": 1}, "candidateQuote": _QUOTE,
}, ["settingId", "factType", "text", "candidateQuote"])},
"evidenceGaps": {"type": "array", "items": _closed({
"gapId": {"type": "string", "minLength": 1}, "query": {"type": "string", "minLength": 1},
"reason": {"type": "string", "minLength": 1}, "priority": {"type": "string", "enum": sorted(PRIORITIES)},
"candidateQuote": _QUOTE,
}, ["gapId", "query", "reason", "priority", "candidateQuote"])},
},
["schemaVersion", "claims", "findings", "assertionVerdicts", "hardConstraintVerdicts", "newSettingCandidates", "evidenceGaps"],
)
# 检测输入 sourceRef 闭集:与 _source_ref 合同逐字段对齐。writer 上下文的证据 sourceRef
# 允许携带 sourceType 等多余字段,投影给 detector 时必须清洗成闭集形状(上下文本身不动)。
_INPUT_SOURCE_REF_ALLOWED = frozenset(
{"sourceId", "sourceVersion", "chapter", "blockId", "startCodePoint", "endCodePoint", "contentSha256"}
)
def _clean_input_source_ref(ref: Any) -> Any:
"""把单个 sourceRef 深拷贝并清洗成检测输入闭集形状。"""
if not isinstance(ref, Mapping):
return copy.deepcopy(ref)
return {key: copy.deepcopy(ref[key]) for key in ref if key in _INPUT_SOURCE_REF_ALLOWED}
def _clean_input_evidence(evidence: Any) -> Any:
"""深拷贝证据列表,仅清洗每条证据的 sourceRef 子对象。"""
if not isinstance(evidence, list):
return copy.deepcopy(evidence)
cleaned: list[Any] = []
for item in evidence:
if not isinstance(item, Mapping):
cleaned.append(copy.deepcopy(item))
continue
new_item = {key: copy.deepcopy(value) for key, value in item.items() if key != "sourceRef"}
if "sourceRef" in item:
new_item["sourceRef"] = _clean_input_source_ref(item["sourceRef"])
cleaned.append(new_item)
return cleaned
def _project_outline_for_input(writer_context: Mapping[str, Any]) -> tuple[dict[str, Any], list[dict[str, str]]]:
"""把写手细纲投影成 detector 输入的稳定 ID 合同(constraint-N / declared-fact-N)。"""
outline = writer_context.get("fineOutline")
if not isinstance(outline, Mapping):
raise SemanticDetectorContractError(
"SEMANTIC_DETECTOR_INPUT_SCHEMA_INVALID", "writerContext.fineOutline 必须是对象"
)
raw_constraints = outline.get("hardConstraints") or []
if not isinstance(raw_constraints, list):
raise SemanticDetectorContractError(
"SEMANTIC_DETECTOR_INPUT_SCHEMA_INVALID", "fineOutline.hardConstraints 必须是数组"
)
constraints = [
{"constraintId": f"constraint-{index + 1}", "text": str(text)}
for index, text in enumerate(raw_constraints)
]
declared: list[dict[str, Any]] = []
raw_declared = outline.get("declaredNewFacts") or []
if not isinstance(raw_declared, list):
raise SemanticDetectorContractError(
"SEMANTIC_DETECTOR_INPUT_SCHEMA_INVALID", "fineOutline.declaredNewFacts 必须是数组"
)
for index, value in enumerate(raw_declared):
if isinstance(value, Mapping):
declared.append({
"factId": str(value.get("factId") or f"declared-fact-{index + 1}"),
"text": str(value.get("text") or ""),
"sourceRef": copy.deepcopy(value.get("sourceRef") or outline["sourceRef"]),
})
else:
declared.append({
"factId": f"declared-fact-{index + 1}",
"text": str(value),
"sourceRef": copy.deepcopy(outline["sourceRef"]),
})
projected = {
"sourceRef": copy.deepcopy(outline["sourceRef"]),
"hardConstraints": constraints,
"adjustableBeats": [str(item) for item in (outline.get("adjustableBeats") or [])],
"declaredNewFacts": declared,
}
return projected, constraints
def build_semantic_input_v3(
*,
run_id: str,
sample_id: str,
opaque_arm_id: str,
writer_context: Mapping[str, Any],
candidate: Mapping[str, Any],
) -> dict[str, Any]:
"""把 WriterContext + 候选投影成严格 semantic-detector-input-v3(生产/回放共用)。
只依赖 writer_context 与 candidate 两个输入,不读库不读文件;投影后立即由
validate_semantic_detector_input 做闭集校验,合同漂移在此失败关闭。
"""
fine_outline, constraints = _project_outline_for_input(writer_context)
payload = {
"schemaVersion": INPUT_VERSION,
"runId": run_id,
"sampleId": sample_id,
"opaqueArmId": opaque_arm_id,
"candidateVersion": candidate["candidateVersion"],
"candidateSha256": candidate["candidateSha256"],
"candidateBody": candidate["candidateBody"],
"contextSnapshotSha256": writer_context["contextSnapshot"]["contextSha256"],
"fineOutline": fine_outline,
"hardConstraints": constraints,
"factEvidence": _clean_input_evidence(writer_context.get("factEvidence", [])),
"proseEvidence": _clean_input_evidence(writer_context.get("proseEvidence", [])),
"asOf": writer_context["asOf"],
"authorizationSnapshotId": writer_context["authorizationSnapshot"]["snapshotId"],
}
payload["inputSha256"] = canonical_sha256(payload)
# 先做闭集校验失败关闭,再返回不含内部 "_" 派生键的干净输入(供 run/detector 复用)。
validate_semantic_detector_input(payload)
return json.loads(_canonical_json(payload))
__all__ = [
"ModelRunner", "ClaudeRuntimeModelRunner", "SemanticDetectorContractError",
"INPUT_VERSION", "MODEL_OUTPUT_VERSION", "REPORT_VERSION", "SEVERITIES",
"FINDING_CATEGORIES", "SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA", "canonical_sha256",
"validate_semantic_detector_input", "build_semantic_model_input",
"build_semantic_detection", "validate_semantic_detector_report",
"calculate_semantic_metrics", "build_safe_semantic_diagnostic",
"run_writer_semantic_detector", "build_semantic_input_v3",
]