소스 검색

下沉验收评估并解除Stage装配反向依赖

SamLee 4 일 전
부모
커밋
e1840642fc

+ 36 - 134
production_build_agents/agents/validator/stage_agent.py

@@ -27,8 +27,15 @@ from ...tools.brief_reader import create_brief_reader_tool
 from ...run.langgraph_checkpointer import create_sqlite_checkpointer
 from ...preprocess.brief_access import (
     build_allowed_source_paths,
+    build_global_data_audit_paths,
     load_production_brief,
 )
+from ...contracts.evaluation import (
+    evaluate_global_data_stage_report,
+    evaluate_stage_validator_candidate,
+    stage_validation_verdict,
+)
+from ...contracts.identifiers import stage_validator_run_id_for
 from ...tools.registry import create_default_tool_registry
 from .task_agent import (
     ValidatorOutputError,
@@ -39,134 +46,20 @@ from .task_context import build_validator_system_prompt
 from .skills.registry import load_global_data_stage_validator_skill
 from .stage_context import (
     build_stage_validator_user_message,
-    create_stage_validator_run_id,
-    required_stage_audit_paths,
     validate_stage_candidate_identity,
 )
 
 
-def _validate_candidate(
-    output: GlobalDataStageValidatorCandidate,
-    *,
-    plan: GlobalDataPlan,
-    candidate: GlobalDataStageCandidate,
-    actually_inspected_paths: set[str] | None = None,
-) -> None:
-    mismatches: list[str] = []
-    for field in ("run_id", "plan_id", "plan_version"):
-        if getattr(output, field) != getattr(candidate, field):
-            mismatches.append(field)
-    expected_ids = [
-        item.requirement_id for item in plan.stage_requirements
-    ]
-    if [
-        item.requirement_id for item in output.requirement_results
-    ] != expected_ids:
-        mismatches.append("requirement_results")
-    if any(not item.evidence for item in output.requirement_results):
-        mismatches.append("requirement_results.evidence")
-
-    artifact_ids = {
-        artifact.artifact_id for artifact in candidate.artifacts
-    }
-    required_paths = set(
-        required_stage_audit_paths(candidate.production_brief_uri)
-    )
-    declared_paths = set(output.inspected_source_paths)
-    if not required_paths.issubset(declared_paths):
-        mismatches.append("inspected_source_paths")
-    if (
-        actually_inspected_paths is not None
-        and not declared_paths.issubset(actually_inspected_paths)
-    ):
-        mismatches.append("inspected_source_paths.tool_evidence")
-    evidence_ids = {
-        artifact_id
-        for result in output.requirement_results
-        for artifact_id in result.evidence_artifact_ids
-    }
-    if evidence_ids.difference(artifact_ids):
-        mismatches.append("requirement_results.evidence_artifact_ids")
-    if {
-        item.artifact_id for item in output.artifact_rejections
-    }.difference(artifact_ids):
-        mismatches.append("artifact_rejections")
-
-    artifact_ids_by_requirement = {
-        item.requirement_id: set(item.actual_artifact_ids)
-        for item in candidate.requirement_evaluations
-    }
-    if any(
-        not set(result.evidence_artifact_ids).issubset(
-            artifact_ids_by_requirement.get(result.requirement_id, set())
-        )
-        for result in output.requirement_results
-    ):
-        mismatches.append("requirement_results.cross_requirement_evidence")
-    gap_requirement_ids = {
-        gap.requirement_id for gap in candidate.expectation_gaps
-    }
-    if any(
-        result.verdict == "PASS"
-        and result.requirement_id in gap_requirement_ids
-        for result in output.requirement_results
-    ):
-        mismatches.append("requirement_results.expectation_gaps")
-
-    _, brief = load_production_brief(candidate.production_brief_uri)
-    allowed_paths = set(build_allowed_source_paths(brief))
-    if any(
-        set(item.source_paths).difference(allowed_paths)
-        for item in output.missing_requirements
-    ):
-        mismatches.append("missing_requirements.source_paths")
-    if mismatches:
-        raise ValidatorOutputError(
-            "Stage ValidatorCandidate 身份、Requirement 或 Artifact 不匹配:"
-            + ", ".join(mismatches)
-        )
-
-
-def _has_blocking_failure(
-    plan: GlobalDataPlan,
-    candidate: GlobalDataStageCandidate,
-    output: GlobalDataStageValidatorCandidate,
-) -> bool:
-    importance = {
-        item.requirement_id: item.importance
-        for item in plan.stage_requirements
-    }
-    if any(
-        importance[result.requirement_id] == "critical"
-        and result.verdict == "FAIL"
-        for result in output.requirement_results
-    ):
-        return True
-    if any(
-        importance[gap.requirement_id] == "critical"
-        for gap in candidate.expectation_gaps
-    ):
-        return True
-    return (
-        any(
-            item.importance == "critical"
-            for item in output.missing_requirements
-        )
-        or bool(output.artifact_rejections)
-    )
-
-
 def _build_report(
     plan: GlobalDataPlan,
     candidate: GlobalDataStageCandidate,
     output: GlobalDataStageValidatorCandidate,
     validator_run_id: str,
 ) -> GlobalDataStageValidationReport:
-    failed = _has_blocking_failure(plan, candidate, output)
     return GlobalDataStageValidationReport(
         **output.model_dump(),
         validator_run_id=validator_run_id,
-        verdict="FAIL" if failed else "PASS",
+        verdict=stage_validation_verdict(plan, candidate, output),
     )
 
 
@@ -175,23 +68,18 @@ def validate_global_data_stage_report(
     candidate: GlobalDataStageCandidate,
     report: GlobalDataStageValidationReport,
 ) -> None:
-    validate_stage_candidate_identity(plan, candidate)
-    output = GlobalDataStageValidatorCandidate.model_validate(
-        report.model_dump(exclude={"validator_run_id", "verdict"})
-    )
-    _validate_candidate(output, plan=plan, candidate=candidate)
-    if report.validator_run_id != create_stage_validator_run_id(candidate):
-        raise ValidatorOutputError(
-            "Stage Report.validator_run_id 与当前 Candidate 不一致"
-        )
-    verdict = (
-        "FAIL"
-        if _has_blocking_failure(plan, candidate, output)
-        else "PASS"
+    _, brief = load_production_brief(candidate.production_brief_uri)
+    issues = evaluate_global_data_stage_report(
+        plan,
+        candidate,
+        report,
+        required_source_paths=set(build_global_data_audit_paths(brief)),
+        allowed_source_paths=set(build_allowed_source_paths(brief)),
     )
-    if report.verdict != verdict:
+    if issues:
         raise ValidatorOutputError(
-            "Stage Report 的逐项结果与 verdict 不一致"
+            "Stage ValidationReport 合同无效:"
+            + ";".join(f"{item.code}: {item.message}" for item in issues)
         )
 
 
@@ -243,7 +131,7 @@ def run_global_data_stage_validator(
     validate_stage_candidate_identity(plan, candidate)
     _, brief = load_production_brief(candidate.production_brief_uri)
     skill = load_global_data_stage_validator_skill()
-    validator_run_id = create_stage_validator_run_id(candidate)
+    validator_run_id = stage_validator_run_id_for(candidate)
     config = {
         "configurable": {"thread_id": validator_run_id},
         "recursion_limit": 16,
@@ -287,14 +175,28 @@ def run_global_data_stage_validator(
                         error_type=ValidatorOutputError,
                     )
                 )
-                _validate_candidate(
+                issues = evaluate_stage_validator_candidate(
+                    plan,
+                    candidate,
                     output,
-                    plan=plan,
-                    candidate=candidate,
+                    required_source_paths=set(
+                        build_global_data_audit_paths(brief)
+                    ),
+                    allowed_source_paths=set(
+                        build_allowed_source_paths(brief)
+                    ),
                     actually_inspected_paths=_inspected_paths(
                         result["messages"]
                     ),
                 )
+                if issues:
+                    raise ValidatorOutputError(
+                        "Stage ValidatorCandidate 合同无效:"
+                        + ";".join(
+                            f"{item.code}: {item.message}"
+                            for item in issues
+                        )
+                    )
                 break
             except (ValidationError, ValidatorOutputError, ValueError) as exc:
                 if attempt >= max_attempts:

+ 0 - 9
production_build_agents/agents/validator/stage_context.py

@@ -29,15 +29,6 @@ from .media_evidence import (
 )
 
 
-def create_stage_validator_run_id(
-    candidate: GlobalDataStageCandidate,
-) -> str:
-    return (
-        f"{candidate.run_id}-validator-global-data-stage-"
-        f"v{candidate.plan_version}"
-    )
-
-
 def validate_stage_candidate_identity(
     plan: GlobalDataPlan,
     candidate: GlobalDataStageCandidate,

+ 3 - 3
production_build_agents/agents/validator/task_agent.py

@@ -33,6 +33,7 @@ from ...contracts.models import (
     ValidatorCandidate,
 )
 from ...contracts.evaluation import task_expectation_pairs
+from ...contracts.identifiers import validator_run_id_for
 from ...tools.registry import (
     SIDE_EFFECT_TOOL_IDS,
     ToolRegistry,
@@ -41,7 +42,6 @@ from ...tools.registry import (
 from .task_context import (
     build_validator_system_prompt,
     build_validator_user_message,
-    create_validator_run_id,
     validate_delivery_identity,
     validate_validation_report,
 )
@@ -238,7 +238,7 @@ def run_validator_agent(
     if max_attempts < 1:
         raise ValueError("Validator max_attempts 必须至少为 1")
     brief, plan, planned_task = validate_delivery_identity(task, delivery)
-    validator_run_id = create_validator_run_id(task, delivery)
+    validator_run_id = validator_run_id_for(delivery)
     skill: ValidatorSkill = load_validator_skill(
         delivery.skill_id,
         delivery.deliverable_type,
@@ -312,5 +312,5 @@ def run_validator_agent(
         planned_task=planned_task,
         validator_run_id=validator_run_id,
     )
-    validate_validation_report(plan, task, delivery, report)
+    validate_validation_report(plan, delivery, report)
     return report

+ 0 - 18
production_build_agents/agents/validator/task_context.py

@@ -3,7 +3,6 @@
 from __future__ import annotations
 
 import json
-from hashlib import sha256
 from pathlib import Path
 from typing import Any
 
@@ -46,18 +45,6 @@ from .skills.registry import ValidatorSkill
 _BASE_PROMPT = Path(__file__).with_name("prompt.md").read_text(encoding="utf-8")
 
 
-def create_validator_run_id(
-    task: TaskPackage,
-    delivery: ExecutorDelivery,
-) -> str:
-    identity = (
-        f"{task.run_id}|{task.plan_id}|{task.task_id}|"
-        f"{task.plan_version}|{delivery.executor_run_id}"
-    )
-    suffix = sha256(identity.encode("utf-8")).hexdigest()[:16]
-    return f"validator-{task.task_id}-v{task.plan_version}-{suffix}"
-
-
 def validate_delivery_identity(
     task: TaskPackage,
     delivery: ExecutorDelivery,
@@ -85,7 +72,6 @@ def validate_delivery_identity(
 
 def validate_validation_report(
     plan: GlobalDataPlan,
-    task: TaskPackage,
     delivery: ExecutorDelivery,
     report: ValidationReport,
 ) -> None:
@@ -97,10 +83,6 @@ def validate_validation_report(
             "ValidationReport 合同无效:"
             + ";".join(f"{item.code}: {item.message}" for item in issues)
         )
-    if report.validator_run_id != create_validator_run_id(task, delivery):
-        raise ValidatorContextError(
-            "ValidationReport.validator_run_id 与当前交付不一致"
-        )
 
 
 def build_validator_system_prompt(skill: ValidatorSkill) -> str:

+ 205 - 1
production_build_agents/contracts/evaluation.py

@@ -9,7 +9,11 @@ from ..preprocess.brief_access import (
     build_allowed_source_paths,
     build_global_data_audit_paths,
 )
-from .identifiers import source_asset_id_for
+from .identifiers import (
+    source_asset_id_for,
+    stage_validator_run_id_for,
+    validator_run_id_for,
+)
 from .models import (
     Artifact,
     ArtifactExpectation,
@@ -21,6 +25,9 @@ from .models import (
     ExpectationGap,
     GlobalDataPlan,
     GlobalDataRequirement,
+    GlobalDataStageCandidate,
+    GlobalDataStageValidationReport,
+    GlobalDataStageValidatorCandidate,
     PlannedTask,
     ProductionBrief,
     RequirementEvaluation,
@@ -591,6 +598,11 @@ def evaluate_validation_report(
             "validation_report_executor_mismatch",
             "ValidationReport.executor_run_id 与 Delivery 不一致",
         )
+    if report.validator_run_id != validator_run_id_for(delivery):
+        add_issue(
+            "validation_report_validator_mismatch",
+            "ValidationReport.validator_run_id 与 Delivery 不一致",
+        )
     matches = [task for task in plan.tasks if task.task_id == delivery.task_id]
     if len(matches) != 1:
         add_issue(
@@ -632,6 +644,198 @@ def evaluate_validation_report(
     return issues
 
 
+def evaluate_stage_validator_candidate(
+    plan: GlobalDataPlan,
+    candidate: GlobalDataStageCandidate,
+    output: GlobalDataStageValidatorCandidate,
+    *,
+    required_source_paths: set[str],
+    allowed_source_paths: set[str],
+    actually_inspected_paths: set[str] | None = None,
+) -> list[ContractIssue]:
+    """校验 Stage Validator 输出身份、证据范围和跨 Requirement 归属。"""
+
+    issues: list[ContractIssue] = []
+
+    def add_issue(code: str, message: str) -> None:
+        _add_issue(issues, code, message)
+
+    if (
+        candidate.plan_id != plan.plan_id
+        or candidate.plan_version != plan.plan_version
+    ):
+        add_issue(
+            "stage_candidate_plan_mismatch",
+            "Stage Candidate 与当前 Plan 身份或版本不一致",
+        )
+    requirement_ids = [
+        requirement.requirement_id for requirement in plan.stage_requirements
+    ]
+    evaluation_ids = [
+        item.requirement_id for item in candidate.requirement_evaluations
+    ]
+    if evaluation_ids != requirement_ids:
+        add_issue(
+            "stage_candidate_requirements_mismatch",
+            "Stage Candidate 没有逐项评价当前 Plan 的 Requirement",
+        )
+    for field in ("run_id", "plan_id", "plan_version"):
+        if getattr(output, field) != getattr(candidate, field):
+            add_issue(
+                "stage_report_identity_mismatch",
+                f"Stage ValidatorCandidate.{field} 与 Candidate 不一致",
+            )
+    if [
+        item.requirement_id for item in output.requirement_results
+    ] != requirement_ids:
+        add_issue(
+            "stage_report_requirements_mismatch",
+            "Stage Validator 必须按 Plan 顺序逐项返回 Requirement",
+        )
+    if any(not item.evidence for item in output.requirement_results):
+        add_issue(
+            "stage_report_evidence_missing",
+            "Stage Requirement 结果必须包含证据",
+        )
+
+    artifact_ids = {
+        artifact.artifact_id for artifact in candidate.artifacts
+    }
+    declared_paths = set(output.inspected_source_paths)
+    if not required_source_paths.issubset(declared_paths):
+        add_issue(
+            "stage_report_audit_paths_missing",
+            "Stage Validator 没有声明全部必查来源路径",
+        )
+    if (
+        actually_inspected_paths is not None
+        and not declared_paths.issubset(actually_inspected_paths)
+    ):
+        add_issue(
+            "stage_report_tool_evidence_missing",
+            "Stage Validator 声明了未实际读取的来源路径",
+        )
+    if declared_paths.difference(allowed_source_paths):
+        add_issue(
+            "stage_report_source_paths_invalid",
+            "Stage Validator 使用了 Brief 目录外的来源路径",
+        )
+    evidence_ids = {
+        artifact_id
+        for result in output.requirement_results
+        for artifact_id in result.evidence_artifact_ids
+    }
+    if evidence_ids.difference(artifact_ids):
+        add_issue(
+            "stage_report_artifact_unknown",
+            "Stage Validator 引用了不存在的 Artifact",
+        )
+    if {
+        item.artifact_id for item in output.artifact_rejections
+    }.difference(artifact_ids):
+        add_issue(
+            "stage_report_rejection_unknown",
+            "Stage Validator 拒绝了不存在的 Artifact",
+        )
+
+    artifact_ids_by_requirement = {
+        item.requirement_id: set(item.actual_artifact_ids)
+        for item in candidate.requirement_evaluations
+    }
+    if any(
+        not set(result.evidence_artifact_ids).issubset(
+            artifact_ids_by_requirement.get(result.requirement_id, set())
+        )
+        for result in output.requirement_results
+    ):
+        add_issue(
+            "stage_report_cross_requirement_evidence",
+            "Stage Validator 跨 Requirement 借用了 Artifact 证据",
+        )
+    gap_requirement_ids = {
+        gap.requirement_id for gap in candidate.expectation_gaps
+    }
+    if any(
+        result.verdict == "PASS"
+        and result.requirement_id in gap_requirement_ids
+        for result in output.requirement_results
+    ):
+        add_issue(
+            "stage_report_gap_verdict_mismatch",
+            "存在 Expectation gap 的 Requirement 不能判为 PASS",
+        )
+    if any(
+        set(item.source_paths).difference(allowed_source_paths)
+        for item in output.missing_requirements
+    ):
+        add_issue(
+            "stage_report_missing_paths_invalid",
+            "Stage MissingRequirement 使用了 Brief 目录外路径",
+        )
+    return issues
+
+
+def stage_validation_verdict(
+    plan: GlobalDataPlan,
+    candidate: GlobalDataStageCandidate,
+    output: GlobalDataStageValidatorCandidate,
+) -> str:
+    importance = {
+        item.requirement_id: item.importance
+        for item in plan.stage_requirements
+    }
+    blocking = (
+        any(
+            importance.get(result.requirement_id) == "critical"
+            and result.verdict == "FAIL"
+            for result in output.requirement_results
+        )
+        or any(
+            importance.get(gap.requirement_id) == "critical"
+            for gap in candidate.expectation_gaps
+        )
+        or any(
+            item.importance == "critical"
+            for item in output.missing_requirements
+        )
+        or bool(output.artifact_rejections)
+    )
+    return "FAIL" if blocking else "PASS"
+
+
+def evaluate_global_data_stage_report(
+    plan: GlobalDataPlan,
+    candidate: GlobalDataStageCandidate,
+    report: GlobalDataStageValidationReport,
+    *,
+    required_source_paths: set[str],
+    allowed_source_paths: set[str],
+) -> list[ContractIssue]:
+    output = GlobalDataStageValidatorCandidate.model_validate(
+        report.model_dump(exclude={"validator_run_id", "verdict"})
+    )
+    issues = evaluate_stage_validator_candidate(
+        plan,
+        candidate,
+        output,
+        required_source_paths=required_source_paths,
+        allowed_source_paths=allowed_source_paths,
+    )
+    if report.validator_run_id != stage_validator_run_id_for(candidate):
+        _add_issue(
+            issues,
+            "stage_report_validator_mismatch",
+            "Stage Report.validator_run_id 与 Candidate 不一致",
+        )
+    if report.verdict != stage_validation_verdict(plan, candidate, output):
+        _add_issue(
+            issues,
+            "stage_report_verdict_mismatch",
+            "Stage Report 的逐项结果与 verdict 不一致",
+        )
+    return issues
+
+
 def evaluate_stage(
     plan: GlobalDataPlan,
     passed_deliveries: Sequence[ExecutorDelivery],

+ 22 - 0
production_build_agents/contracts/identifiers.py

@@ -4,9 +4,31 @@ from __future__ import annotations
 
 from hashlib import sha256
 
+from .models import ExecutorDelivery, GlobalDataStageCandidate
+
 
 def source_asset_id_for(artifact_type: str, source_uri: str) -> str:
     """由媒体类型与精确 URI 生成唯一 SourceAsset ID。"""
 
     digest = sha256(f"{artifact_type}\0{source_uri}".encode("utf-8")).hexdigest()
     return f"SourceAsset-{digest}"
+
+
+def validator_run_id_for(delivery: ExecutorDelivery) -> str:
+    identity = (
+        f"{delivery.run_id}|{delivery.plan_id}|{delivery.task_id}|"
+        f"{delivery.plan_version}|{delivery.executor_run_id}"
+    )
+    suffix = sha256(identity.encode("utf-8")).hexdigest()[:16]
+    return (
+        f"validator-{delivery.task_id}-v{delivery.plan_version}-{suffix}"
+    )
+
+
+def stage_validator_run_id_for(
+    candidate: GlobalDataStageCandidate,
+) -> str:
+    return (
+        f"{candidate.run_id}-validator-global-data-stage-"
+        f"v{candidate.plan_version}"
+    )

+ 30 - 5
production_build_agents/global_data_stage_assembly.py

@@ -19,15 +19,19 @@ from .contracts.models import (
     TaskRecord,
     ValidationReport,
 )
-from .preprocess.brief_access import load_production_brief
+from .preprocess.brief_access import (
+    build_allowed_source_paths,
+    build_global_data_audit_paths,
+    load_production_brief,
+)
 from .contracts.evaluation import (
+    evaluate_global_data_stage_report,
     evaluate_stage,
     evaluate_task_delivery,
+    evaluate_validation_report,
     planned_task_for,
 )
 from .agents.capabilities import CAPABILITIES
-from .agents.validator.stage_agent import validate_global_data_stage_report
-from .agents.validator.task_context import validate_validation_report
 from .run.artifacts import (
     ArtifactIntegrityError,
     verify_delivery_artifacts,
@@ -137,7 +141,16 @@ def _read_task_evidence(
             )
         except ArtifactIntegrityError as exc:
             raise GlobalDataStageError(str(exc)) from exc
-        validate_validation_report(package_plan, task, delivery, report)
+        report_issues = evaluate_validation_report(
+            package_plan,
+            delivery,
+            report,
+        )
+        if report_issues:
+            raise GlobalDataStageError(
+                f"{planned.task_id} 的 ValidationReport 合同无效:"
+                + ";".join(item.message for item in report_issues)
+            )
         if report.verdict != "PASS":
             raise GlobalDataStageError(
                 f"{planned.task_id} 的 Task ValidationReport 不是 PASS"
@@ -283,7 +296,19 @@ def build_global_data_stage_delivery(
     )
     if rebuilt != candidate:
         raise GlobalDataStageError("Stage Candidate 与当前证据不一致")
-    validate_global_data_stage_report(plan, candidate, report)
+    _, brief = load_production_brief(candidate.production_brief_uri)
+    report_issues = evaluate_global_data_stage_report(
+        plan,
+        candidate,
+        report,
+        required_source_paths=set(build_global_data_audit_paths(brief)),
+        allowed_source_paths=set(build_allowed_source_paths(brief)),
+    )
+    if report_issues:
+        raise GlobalDataStageError(
+            "Stage ValidationReport 合同无效:"
+            + ";".join(item.message for item in report_issues)
+        )
     if report.verdict != "PASS":
         raise GlobalDataStageError("Stage Report 尚未通过")
     importance = {

+ 1 - 1
production_build_agents/graph_nodes/validate_task.py

@@ -56,7 +56,7 @@ def validate_task_node(
             report = ValidationReport.model_validate_json(
                 report_path.read_text(encoding="utf-8")
             )
-            validate_validation_report(plan, task, delivery, report)
+            validate_validation_report(plan, delivery, report)
         else:
             report = run_validator_agent(
                 task,

+ 3 - 3
tests/agents/test_task_validator.py

@@ -20,11 +20,11 @@ from production_build_agents.agents.validator.task_agent import (
 from production_build_agents.agents.validator.task_context import (
     ValidatorContextError,
     build_validator_user_message,
-    create_validator_run_id,
 )
 from production_build_agents.contracts.evaluation import (
     task_expectation_pairs,
 )
+from production_build_agents.contracts.identifiers import validator_run_id_for
 from production_build_agents.contracts.models import (
     Artifact,
     ArtifactExpectation,
@@ -299,7 +299,7 @@ class GeneralValidatorTest(unittest.TestCase):
             brief, plan, planned_task = load_task_contract(task)
             Path(delivery.manifest_uri).unlink()
             message = build_validator_user_message(
-                validator_run_id=create_validator_run_id(task, delivery),
+                validator_run_id=validator_run_id_for(delivery),
                 task=task,
                 delivery=delivery,
                 brief=brief,
@@ -432,7 +432,7 @@ class GeneralValidatorTest(unittest.TestCase):
         self.assertEqual(report.verdict, "PASS")
         self.assertEqual(
             report.validator_run_id,
-            create_validator_run_id(task, delivery),
+            validator_run_id_for(delivery),
         )
 
 

+ 5 - 2
tests/contracts/test_evaluation.py

@@ -14,7 +14,10 @@ from production_build_agents.contracts.evaluation import (
     task_expectation_pairs,
     validate_replan,
 )
-from production_build_agents.contracts.identifiers import source_asset_id_for
+from production_build_agents.contracts.identifiers import (
+    source_asset_id_for,
+    validator_run_id_for,
+)
 from production_build_agents.contracts.models import (
     Artifact,
     ArtifactBindingClaim,
@@ -302,7 +305,7 @@ def _report(
         plan_version=package.plan_version,
         task_id=package.task_id,
         executor_run_id=delivery.executor_run_id,
-        validator_run_id=f"validator-{package.task_id}",
+        validator_run_id=validator_run_id_for(delivery),
         criterion_results=results,
         verdict="FAIL" if has_critical_failure else "PASS",
         summary="失败" if has_critical_failure else "通过",

+ 1 - 1
tests/fixtures/v18_replay/validation_reports/report.Task1.v1.json

@@ -5,7 +5,7 @@
   "plan_version": 1,
   "task_id": "Task1",
   "executor_run_id": "v18-replay-executor-Task1-v1",
-  "validator_run_id": "v18-replay-validator-Task1-v1",
+  "validator_run_id": "validator-Task1-v1-107a76936b69a655",
   "criterion_results": [
     {
       "expectation_id": "Requirement1-Expectation1",

+ 1 - 1
tests/fixtures/v18_replay/validation_reports/report.Task2.v2.json

@@ -5,7 +5,7 @@
   "plan_version": 2,
   "task_id": "Task2",
   "executor_run_id": "v18-replay-executor-Task2-v2",
-  "validator_run_id": "v18-replay-validator-Task2-v2",
+  "validator_run_id": "validator-Task2-v2-be52fce87ebe05d6",
   "criterion_results": [
     {
       "expectation_id": "Requirement2-Expectation1",

+ 2 - 2
tests/graph/test_recovery.py

@@ -21,7 +21,7 @@ from production_build_agents.run.records import VersionConflictError
 from production_build_agents.tools.registry import (
     create_default_tool_registry as real_create_default_tool_registry,
 )
-from production_build_agents.agents.validator.task_context import create_validator_run_id
+from production_build_agents.contracts.identifiers import validator_run_id_for
 from tests.support.executor_fixtures import (
     build_executor_candidate,
     build_executor_model,
@@ -685,7 +685,7 @@ class DurableRunTest(unittest.TestCase):
                 plan_version=1,
                 task_id="Task1",
                 executor_run_id=delivery.executor_run_id,
-                validator_run_id=create_validator_run_id(task, delivery),
+                validator_run_id=validator_run_id_for(delivery),
                 criterion_results=failed.criterion_results,
                 verdict="PASS",
                 summary="故意构造的自相矛盾报告",