zizi 091b66a9bb 重构: 收敛 Agent/Skill 运行时与创作质量闭环
将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。

同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
2026-08-22 02:12:32 +08:00

374 lines
15 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
"""正文候选 Shadow 准入与用户三决策的纯函数前置检查。"""
from __future__ import annotations
import pathlib
import sys
from datetime import datetime
from typing import Any, Mapping
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
READ_CONTEXT_DIR = SCRIPT_DIR.parents[1] / "assemble-context" / "scripts"
if str(READ_CONTEXT_DIR) not in sys.path:
sys.path.insert(0, str(READ_CONTEXT_DIR))
from writer_contract import ( # noqa: E402
ContractError,
han_count,
validate_writer_context,
validate_writer_output,
)
PRODUCTION_POLICY = "writer-production-v1"
ACTIVE_SOURCE_STATUS = "active"
DECISIONS = frozenset({"accept", "merge", "discard"})
_LIVE_STATE_FIELDS = frozenset(
{
"qualityPolicyVersion",
"contextSnapshotId",
"contextSnapshotSha256",
"authorizationSnapshotId",
"authorizationValid",
"sourceStatus",
"candidateExpiresAt",
"checkedAt",
"canonicalRevision",
}
)
class AcceptanceError(RuntimeError):
"""携带稳定失败码的接受前置检查错误,任何错误都不可接受。"""
def __init__(
self, code: str, message: str, *, details: Mapping[str, Any] | None = None
) -> None:
super().__init__(message)
self.code = code
self.details = dict(details or {})
self.acceptance_eligible = False
def _require_mapping(value: Any, field: str) -> Mapping[str, Any]:
"""拒绝非对象输入,避免宽松取值绕过实时检查。"""
if not isinstance(value, Mapping):
raise AcceptanceError("ACCEPTANCE_STATE_INVALID", f"{field} 必须是对象")
return value
def _parse_timestamp(value: Any, field: str) -> datetime:
"""解析带时区的 ISO-8601 时间;无时区时间失败关闭。"""
if not isinstance(value, str) or not value:
raise AcceptanceError("ACCEPTANCE_STATE_INVALID", f"{field} 必须是时间字符串")
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise AcceptanceError("ACCEPTANCE_STATE_INVALID", f"{field} 格式非法") from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise AcceptanceError("ACCEPTANCE_STATE_INVALID", f"{field} 必须带时区")
return parsed
def _validate_live_state(value: Any) -> dict[str, Any]:
"""严格校验接受时重新读取的可信实时状态。"""
live = dict(_require_mapping(value, "live_state"))
if set(live) != _LIVE_STATE_FIELDS:
missing = sorted(_LIVE_STATE_FIELDS - set(live))
extra = sorted(set(live) - _LIVE_STATE_FIELDS)
raise AcceptanceError(
"ACCEPTANCE_STATE_INVALID",
"live_state 字段不完整或含未知字段",
details={"missing": missing, "extra": extra},
)
if not isinstance(live["authorizationValid"], bool):
raise AcceptanceError(
"ACCEPTANCE_STATE_INVALID", "authorizationValid 必须是布尔值"
)
revision = live["canonicalRevision"]
if isinstance(revision, bool) or not isinstance(revision, int) or revision < 0:
raise AcceptanceError(
"ACCEPTANCE_STATE_INVALID", "canonicalRevision 必须是非负整数"
)
checked_at = _parse_timestamp(live["checkedAt"], "checkedAt")
expires_at = _parse_timestamp(live["candidateExpiresAt"], "candidateExpiresAt")
live["_checkedAt"] = checked_at
live["_expiresAt"] = expires_at
return live
def _validate_detector_result(
detector_result: Any, candidate: Mapping[str, Any]
) -> None:
"""要求最终报告同时证明机械门和语义接口通过并绑定当前候选。"""
report = _require_mapping(detector_result, "detector_result")
if report.get("schemaVersion") != "writer-pipeline-result-v1":
raise AcceptanceError(
"DETECTOR_REPORT_INVALID", "detector 终态报告版本不受支持"
)
if report.get("status") != "PASSED" or report.get("failureCode") is not None:
raise AcceptanceError("DETECTOR_NOT_PASSED", "detector 尚未得到无阻塞通过终态")
expected = {
"runId": candidate["runId"],
"attempt": candidate["attempt"],
"candidateVersion": candidate["candidateVersion"],
"candidateSha256": candidate["candidateSha256"],
}
mismatches = [field for field, value in expected.items() if report.get(field) != value]
if mismatches:
raise AcceptanceError(
"DETECTOR_BINDING_MISMATCH",
"detector 终态未绑定当前候选",
details={"fields": mismatches},
)
trace = report.get("trace")
if not isinstance(trace, list) or not trace or not isinstance(trace[-1], Mapping):
raise AcceptanceError("DETECTOR_REPORT_INVALID", "detector 终态缺少审查轨迹")
final_check = trace[-1]
final_expected = {
"attempt": candidate["attempt"],
"candidateVersion": candidate["candidateVersion"],
"candidateSha256": candidate["candidateSha256"],
}
if any(final_check.get(field) != value for field, value in final_expected.items()):
raise AcceptanceError(
"DETECTOR_BINDING_MISMATCH", "detector 最终审查轨迹绑定了旧候选"
)
if (
final_check.get("mechanicalPassed") is not True
or final_check.get("semanticStatus") != "passed"
or final_check.get("failureCodes") != []
):
raise AcceptanceError(
"DETECTOR_NOT_PASSED", "候选没有同时通过机械硬门和语义审查接口"
)
def _validate_context_binding(
context: Mapping[str, Any], candidate: Mapping[str, Any]
) -> None:
"""保证候选没有逃离本次冻结上下文、运行身份和生产策略。"""
expected = {
"runId": context["runId"],
"attempt": context["attempt"],
"mode": context["mode"],
"qualityPolicyVersion": context["qualityPolicyVersion"],
"contextSnapshotId": context["contextSnapshot"]["manifestId"],
"contextSnapshotSha256": context["contextSnapshot"]["contextSha256"],
"acceptanceEligible": context["acceptanceEligible"],
}
mismatches = [field for field, value in expected.items() if candidate.get(field) != value]
if mismatches:
raise AcceptanceError(
"CONTEXT_BINDING_MISMATCH",
"候选未绑定当前 WriterContext",
details={"fields": mismatches},
)
def check_shadow_ready(
*,
context: Mapping[str, Any],
candidate: Mapping[str, Any],
detector_result: Mapping[str, Any],
live_state: Mapping[str, Any],
) -> dict[str, Any]:
"""纯函数校验候选是否可进入 Shadow 待接受态,不执行任何写入。"""
raw_candidate = _require_mapping(candidate, "candidate")
if raw_candidate.get("acceptanceEligible") is not True:
raise AcceptanceError(
"ACCEPTANCE_NOT_ELIGIBLE", "评测、诊断或显式不可接受候选不得进入接受链"
)
try:
normalized_context = validate_writer_context(context)
except ContractError as exc:
raise AcceptanceError("CONTEXT_CONTRACT_INVALID", str(exc)) from exc
try:
normalized_candidate = validate_writer_output(candidate)
except ContractError as exc:
raise AcceptanceError("CANDIDATE_CONTRACT_INVALID", str(exc)) from exc
if (
normalized_context["mode"] != "production"
or normalized_candidate["mode"] != "production"
):
raise AcceptanceError("ACCEPTANCE_NOT_ELIGIBLE", "只有生产候选可进入接受链")
_validate_context_binding(normalized_context, normalized_candidate)
contract = normalized_context["outputContract"]
actual_han_chars = han_count(normalized_candidate["candidateBody"])
if not contract["minChars"] <= actual_han_chars <= contract["maxChars"]:
raise AcceptanceError(
"CANDIDATE_LENGTH_OUT_OF_RANGE",
"候选正文汉字数超出动态篇幅合同",
details={
"actualHanChars": actual_han_chars,
"minChars": contract["minChars"],
"maxChars": contract["maxChars"],
"targetChars": contract["targetChars"],
},
)
live = _validate_live_state(live_state)
if (
normalized_context["qualityPolicyVersion"] != PRODUCTION_POLICY
or normalized_candidate["qualityPolicyVersion"] != PRODUCTION_POLICY
or live["qualityPolicyVersion"] != PRODUCTION_POLICY
):
raise AcceptanceError("QUALITY_POLICY_STALE", "生产质量策略已变化或绑定错误")
if (
live["contextSnapshotId"] != normalized_candidate["contextSnapshotId"]
or live["contextSnapshotSha256"]
!= normalized_candidate["contextSnapshotSha256"]
):
raise AcceptanceError("CONTEXT_STALE", "冻结上下文已失效或哈希变化")
if (
live["authorizationSnapshotId"]
!= normalized_context["authorizationSnapshot"]["snapshotId"]
or live["authorizationValid"] is not True
):
raise AcceptanceError("AUTHORIZATION_STALE", "授权快照已失效或变化")
if (
live["sourceStatus"] != ACTIVE_SOURCE_STATUS
or normalized_context["sourceStatus"] != ACTIVE_SOURCE_STATUS
):
raise AcceptanceError("SOURCE_STALE", "来源状态已不允许接受")
if live["_checkedAt"] >= live["_expiresAt"]:
raise AcceptanceError("CANDIDATE_EXPIRED", "候选接受窗口已过期")
_validate_detector_result(detector_result, normalized_candidate)
return {
"schemaVersion": "writer-acceptance-result-v1",
"status": "SHADOW_READY",
"runId": normalized_candidate["runId"],
"candidateVersion": normalized_candidate["candidateVersion"],
"candidateSha256": normalized_candidate["candidateSha256"],
"canonicalRevision": live["canonicalRevision"],
"canonicalMutationPerformed": False,
}
def _validate_edited_candidate(
previous_candidate: Any, candidate: Mapping[str, Any]
) -> None:
"""要求用户编辑形成严格下一版本,并保持同一冻结运行身份。"""
if previous_candidate is None:
raise AcceptanceError("EDIT_BASE_REQUIRED", "修改后合并必须提供编辑前候选")
try:
previous = validate_writer_output(previous_candidate)
except ContractError as exc:
raise AcceptanceError("EDIT_BASE_INVALID", str(exc)) from exc
if candidate["candidateVersion"] != previous["candidateVersion"] + 1:
raise AcceptanceError(
"EDIT_VERSION_INVALID", "用户编辑必须生成 candidateVersion 的严格下一版本"
)
identity_fields = (
"runId",
"attempt",
"mode",
"qualityPolicyVersion",
"contextSnapshotId",
"contextSnapshotSha256",
"acceptanceEligible",
)
if any(candidate[field] != previous[field] for field in identity_fields):
raise AcceptanceError("EDIT_BASE_MISMATCH", "编辑候选改变了冻结运行身份")
if candidate["candidateSha256"] == previous["candidateSha256"]:
raise AcceptanceError("EDIT_BODY_UNCHANGED", "修改后合并必须包含实际正文变更")
def check_writer_acceptance(
*,
decision: str,
confirmed: bool,
context: Mapping[str, Any],
candidate: Mapping[str, Any],
detector_result: Mapping[str, Any],
live_state: Mapping[str, Any],
expected_revision: int,
previous_candidate: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""校验用户三决策并返回命令意图;实验台不写 Canonical。"""
if decision not in DECISIONS:
raise AcceptanceError("DECISION_INVALID", "decision 只允许 accept、merge 或 discard")
if confirmed is not True:
raise AcceptanceError("CONFIRMATION_REQUIRED", f"{decision} 必须由用户明确确认")
if decision == "discard":
try:
normalized_context = validate_writer_context(context)
except ContractError as exc:
raise AcceptanceError("CONTEXT_CONTRACT_INVALID", str(exc)) from exc
try:
normalized_candidate = validate_writer_output(candidate)
except ContractError as exc:
raise AcceptanceError("CANDIDATE_CONTRACT_INVALID", str(exc)) from exc
_validate_context_binding(normalized_context, normalized_candidate)
return {
"schemaVersion": "writer-acceptance-result-v1",
"status": "DISCARD_INTENT_READY",
"decision": decision,
"runId": normalized_candidate["runId"],
"candidateVersion": normalized_candidate["candidateVersion"],
"candidateSha256": normalized_candidate["candidateSha256"],
"commandIntent": {
"command": "close_shadow_candidate",
"expectedCandidateVersion": normalized_candidate["candidateVersion"],
"expectedCandidateSha256": normalized_candidate["candidateSha256"],
},
"canonicalMutationPerformed": False,
}
shadow = check_shadow_ready(
context=context,
candidate=candidate,
detector_result=detector_result,
live_state=live_state,
)
if isinstance(expected_revision, bool) or not isinstance(expected_revision, int):
raise AcceptanceError("EXPECTED_REVISION_INVALID", "expectedRevision 必须是整数")
if expected_revision != shadow["canonicalRevision"]:
raise AcceptanceError(
"REVISION_CONFLICT",
"expectedRevision 与当前 Canonical revision 不一致",
details={
"expectedRevision": expected_revision,
"canonicalRevision": shadow["canonicalRevision"],
},
)
normalized_candidate = validate_writer_output(candidate)
if decision == "merge":
_validate_edited_candidate(previous_candidate, normalized_candidate)
# detector 绑定已由 check_shadow_ready 针对编辑后的新版本重新校验。
return {
"schemaVersion": "writer-acceptance-result-v1",
"status": "ACCEPTANCE_INTENT_READY",
"decision": decision,
"runId": normalized_candidate["runId"],
"candidateVersion": normalized_candidate["candidateVersion"],
"candidateSha256": normalized_candidate["candidateSha256"],
"expectedRevision": expected_revision,
"canonicalMutationPerformed": False,
"commandIntent": {
"command": "queue_chapter_extraction",
"dispatch": "async",
"executeAfter": "canonical_commit",
# 本函数只是 preflight,尚无 Canonical 提交凭证;正式提交层验证凭证后才能放行。
"allowed": False,
"requiresCanonicalCommit": True,
"workId": context["workId"],
"chapter": context["targetChapter"],
"candidateSha256": normalized_candidate["candidateSha256"],
},
}
__all__ = [
"AcceptanceError",
"check_shadow_ready",
"check_writer_acceptance",
]