Просмотр исходного кода

任务合同:冻结每个 Phase 2 Task 的 goal_ids

ScriptTaskContractV1 将 goal_ids 设为必填字段,限制最多 30 个且必须非空白、无重复。

旧合同载荷缺少该字段时明确返回 GOAL_SCOPE_INVALID,不通过默认值掩盖历史数据。
SamLee 21 часов назад
Родитель
Сommit
5ca946228e
1 измененных файлов с 15 добавлено и 0 удалено
  1. 15 0
      script_build_host/src/script_build_host/domain/task_contracts.py

+ 15 - 0
script_build_host/src/script_build_host/domain/task_contracts.py

@@ -13,6 +13,7 @@ from agent.orchestration import ArtifactRef
 
 from script_build_host.domain.digests import Sha256Digest
 from script_build_host.domain.errors import ScriptBuildError
+from script_build_host.domain.goal_coverage import MAX_DIRECTION_GOALS
 
 _CONTROL_URI_PREFIX = "script-build://"
 _ARTIFACT_URI_PREFIX = "script-build://artifact-versions/"
@@ -250,6 +251,7 @@ class ScriptTaskContractV1:
     output_schema: str
     criteria: tuple[ScriptCriterion, ...]
     budget: ScriptTaskBudget
+    goal_ids: tuple[str, ...]
     supersedes_decision_ids: tuple[str, ...] = ()
     candidate_closure_decision_refs: tuple[AcceptedDecisionRef, ...] = ()
     adopted_decision_ids: tuple[str, ...] = ()
@@ -270,6 +272,15 @@ class ScriptTaskContractV1:
                 "TASK_CONTRACT_INVALID", f"criteria must contain between 1 and {MAX_CRITERIA} items"
             )
         _require_unique((item.criterion_id for item in self.criteria), "criterion IDs")
+        if len(self.goal_ids) > MAX_DIRECTION_GOALS:
+            raise TaskContractError(
+                "GOAL_SCOPE_INVALID",
+                f"goal_ids may contain at most {MAX_DIRECTION_GOALS} values",
+            )
+        if any(not item.strip() for item in self.goal_ids) or len(set(self.goal_ids)) != len(
+            self.goal_ids
+        ):
+            raise TaskContractError("GOAL_SCOPE_INVALID", "goal_ids must be nonblank and unique")
         for item in self.write_scope:
             _require_control_uri(item, "write_scope")
         _require_unique(self.write_scope, "write_scope")
@@ -417,6 +428,7 @@ class ScriptTaskContractV1:
             "output_schema": self.output_schema,
             "criteria": [item.to_payload() for item in self.criteria],
             "budget": self.budget.to_payload(),
+            "goal_ids": list(self.goal_ids),
             "supersedes_decision_ids": list(self.supersedes_decision_ids),
             "candidate_closure_decision_refs": [
                 item.to_payload() for item in self.candidate_closure_decision_refs
@@ -443,6 +455,8 @@ class ScriptTaskContractV1:
         budget = value.get("budget")
         if not isinstance(budget, Mapping):
             raise TaskContractError("TASK_CONTRACT_INVALID", "task budget must be an object")
+        if "goal_ids" not in value:
+            raise TaskContractError("GOAL_SCOPE_INVALID", "goal_ids is required")
         return cls(
             task_kind=_enum_value(ScriptTaskKind, value.get("task_kind"), "task_kind"),
             scope_ref=str(value.get("scope_ref", "")),
@@ -455,6 +469,7 @@ class ScriptTaskContractV1:
             output_schema=str(value.get("output_schema", "")),
             criteria=_criteria(value.get("criteria")),
             budget=ScriptTaskBudget.from_payload(budget),
+            goal_ids=_strings(value.get("goal_ids"), "goal_ids"),
             supersedes_decision_ids=_strings(
                 value.get("supersedes_decision_ids"), "supersedes_decision_ids"
             ),