فهرست منبع

最终交付:增加 Root Goal 与 Direction 合同预检

Root deterministic preflight 新增 root-goal-coverage 和 root-direction-contract,复用统一覆盖规则检查全部 Goal、来源闭包与 Direction/Portfolio/StructuredScript 一致性。

Phase 3 冻结 Root Delivery 合同时带入全部 Goal ID,旧 Direction 类型不再进入最终交付。
SamLee 1 روز پیش
والد
کامیت
651299042a

+ 9 - 2
script_build_host/src/script_build_host/application/phase_three.py

@@ -11,7 +11,7 @@ from agent.orchestration import DecisionAction, OperationStatus, TaskStatus, Val
 
 from script_build_host.application.input_snapshot_service import ScriptInputSnapshotService
 from script_build_host.application.mission_factory import ScriptMissionFactory
-from script_build_host.domain.artifacts import ArtifactState, ScriptDirectionArtifactV1
+from script_build_host.domain.artifacts import ArtifactState, DirectionArtifact
 from script_build_host.domain.canonical_json import canonical_sha256
 from script_build_host.domain.errors import (
     MissionRecoveryRequired,
@@ -482,7 +482,7 @@ class PhaseThreeContinuationService:
             if version.state not in {ArtifactState.FROZEN, ArtifactState.PUBLISHED}:
                 raise PhaseThreeBoundaryNotReady("Root child Artifact is not frozen")
             if kind == ScriptTaskKind.DIRECTION.value and not isinstance(
-                version.artifact, ScriptDirectionArtifactV1
+                version.artifact, DirectionArtifact
             ):
                 raise PhaseThreeBoundaryNotReady("accepted Direction has the wrong Artifact type")
             contract_refs = [
@@ -523,6 +523,12 @@ class PhaseThreeContinuationService:
         closure: _AcceptedRootClosure,
         snapshot_id: str,
     ) -> Any:
+        direction_version = await self.artifacts.read_by_ref(
+            closure.direction.artifact_ref,
+            script_build_id=(await self.bindings.get_by_root(root_trace_id)).script_build_id,
+        )
+        if not isinstance(direction_version.artifact, DirectionArtifact):
+            raise PhaseThreeBoundaryNotReady("Root Direction has the wrong Artifact type")
         contract = ScriptTaskContractV1(
             task_kind=ScriptTaskKind.ROOT_DELIVERY,
             scope_ref=closure.direction.scope_ref,
@@ -538,6 +544,7 @@ class PhaseThreeContinuationService:
                 for item in root.current_spec.acceptance_criteria
             ),
             budget=ScriptTaskBudget(),
+            goal_ids=tuple(item.goal_id for item in direction_version.artifact.goals),
         )
         frozen = await self.contracts.freeze(root_trace_id, contract)
         # Snapshot identity is present in TaskSpec; this guards accidental use of

+ 49 - 5
script_build_host/src/script_build_host/application/root_delivery.py

@@ -12,9 +12,10 @@ from script_build_host.domain.artifacts import (
     ArtifactKind,
     ArtifactState,
     ArtifactVersion,
-    ScriptDirectionArtifactV1,
+    DirectionArtifact,
 )
 from script_build_host.domain.errors import ProtocolViolation
+from script_build_host.domain.goal_coverage import GoalPolicyError, validate_goal_coverage
 from script_build_host.domain.phase_three_artifacts import (
     RootDeliveryManifestV1,
     canonicalize_legacy_projection,
@@ -166,10 +167,27 @@ class RootDeliveryService:
             and root_manifest.input_closure_digest == _closure_digest(closure)
         )
         projection_passed = False
+        direction = _direction(closure.direction)
+        portfolio = _portfolio(closure.portfolio)
+        structured = _structured(closure.structured)
+        goal_coverage_error: str | None = None
+        try:
+            validate_goal_coverage(
+                direction_goal_ids=tuple(item.goal_id for item in direction.goals),
+                coverage=structured.goal_coverage,
+                adopted_source_refs=structured.source_artifact_refs,
+            )
+        except GoalPolicyError as exc:
+            goal_coverage_error = f"{exc.code}: {exc.summary}"
+        goal_coverage_passed = goal_coverage_error is None
+        direction_contract_passed = (
+            structured.direction_ref == _uri(closure.direction)
+            and portfolio.adopted_structured_script_ref == _uri(closure.structured)
+        )
         if root_manifest is not None:
             canonical = canonicalize_legacy_projection(
-                _structured(closure.structured),
-                direction=_direction(closure.direction).legacy_markdown,
+                structured,
+                direction=direction.legacy_markdown,
                 summary=root_manifest.build_summary,
             )
             projection_passed = canonical.canonical_sha256 == root_manifest.legacy_projection_digest
@@ -207,6 +225,32 @@ class RootDeliveryService:
                 "error": None,
                 "evidence_refs": [],
             },
+            {
+                "rule_id": "root-goal-coverage",
+                "verdict": "passed" if goal_coverage_passed else "failed",
+                "reason": (
+                    "Every parent and child Goal has adopted creative sources"
+                    if goal_coverage_passed
+                    else goal_coverage_error
+                ),
+                "error": None,
+                "evidence_refs": list(structured.source_artifact_refs),
+            },
+            {
+                "rule_id": "root-direction-contract",
+                "verdict": "passed" if direction_contract_passed else "failed",
+                "reason": (
+                    "StructuredScript, Direction rules and Portfolio share one frozen closure"
+                    if direction_contract_passed
+                    else "Root accepted inputs disagree on Direction or StructuredScript"
+                ),
+                "error": None,
+                "evidence_refs": [
+                    _uri(closure.direction),
+                    _uri(closure.portfolio),
+                    _uri(closure.structured),
+                ],
+            },
             {
                 "rule_id": "root-legacy-projection",
                 "verdict": "passed" if projection_passed else "failed",
@@ -295,8 +339,8 @@ class _RootClosure:
         self.structured = structured
 
 
-def _direction(version: ArtifactVersion) -> ScriptDirectionArtifactV1:
-    if not isinstance(version.artifact, ScriptDirectionArtifactV1):
+def _direction(version: ArtifactVersion) -> DirectionArtifact:
+    if not isinstance(version.artifact, DirectionArtifact):
         raise ProtocolViolation("Root Direction artifact has the wrong type")
     return version.artifact