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

fix(Phase3推进): 从运行态边界安全补齐 partial 检查点

真实 E2E 到达 Phase2 Root 边界但 build 尚未切为 partial 时,先校验 Portfolio 闭包、活动 Operation 和 OwnerLease,再以 fenced checkpoint 收敛状态。Root Delivery 继续沿 accepted Direction 的真实业务 scope 构建。
SamLee 1 день назад
Родитель
Сommit
cdf0b2d2bb
1 измененных файлов с 62 добавлено и 4 удалено
  1. 62 4
      script_build_host/src/script_build_host/application/phase_three.py

+ 62 - 4
script_build_host/src/script_build_host/application/phase_three.py

@@ -132,6 +132,39 @@ class PhaseThreeContinuationService:
                 "status"
                 "status"
             ) in {TaskStatus.NEEDS_REPLAN.value, TaskStatus.PENDING.value}:
             ) in {TaskStatus.NEEDS_REPLAN.value, TaskStatus.PENDING.value}:
                 reentry = await self._phase_three_reentry_ready(binding.root_trace_id)
                 reentry = await self._phase_three_reentry_ready(binding.root_trace_id)
+            at_phase_two_boundary = (
+                completion.get("status") == TaskStatus.BLOCKED.value
+                and completion.get("blocked_reason")
+                == PHASE_TWO_CANDIDATE_PORTFOLIO_READY
+            )
+            if status is BuildStatus.RUNNING and at_phase_two_boundary:
+                if self.phase_two_boundary_verifier is None:
+                    raise PhaseThreeBoundaryNotReady(
+                        "phase-two closure verifier is unavailable"
+                    )
+                await self.phase_two_boundary_verifier.verify_checkpoint(
+                    script_build_id=script_build_id,
+                    root_trace_id=binding.root_trace_id,
+                    input_snapshot_id=str(binding.input_snapshot_id),
+                )
+                ledger = await self.coordinator.task_store.load(binding.root_trace_id)
+                if any(
+                    item.status
+                    in {
+                        OperationStatus.PENDING,
+                        OperationStatus.RUNNING,
+                        OperationStatus.STOP_REQUESTED,
+                    }
+                    for item in ledger.operations.values()
+                ):
+                    raise PhaseThreeBoundaryNotReady("phase two still has active operations")
+                lease = self.owner_lease_factory(script_build_id)
+                token = await lease.acquire(script_build_id)
+                try:
+                    await self._set_phase_two_checkpoint(token)
+                finally:
+                    await lease.release()
+                status = BuildStatus.PARTIAL
             if status is not BuildStatus.PARTIAL and not reentry:
             if status is not BuildStatus.PARTIAL and not reentry:
                 raise PhaseThreeBoundaryNotReady("phase-three advance requires a partial build")
                 raise PhaseThreeBoundaryNotReady("phase-three advance requires a partial build")
             if not reentry and (
             if not reentry and (
@@ -228,7 +261,7 @@ class PhaseThreeContinuationService:
         ]
         ]
         if active_operations:
         if active_operations:
             raise PhaseThreeBoundaryNotReady("phase two still has active operations")
             raise PhaseThreeBoundaryNotReady("phase two still has active operations")
-        closure = await self._accepted_root_closure(script_build_id, ledger)
+        closure = await self._accepted_root_closure(script_build_id, binding.root_trace_id, ledger)
         await self._verify_owner(owner_token)
         await self._verify_owner(owner_token)
         snapshot, binding = await self._ensure_snapshot(snapshot, binding, owner_token)
         snapshot, binding = await self._ensure_snapshot(snapshot, binding, owner_token)
         root_contract = await self._execute_fenced(
         root_contract = await self._execute_fenced(
@@ -378,8 +411,25 @@ class PhaseThreeContinuationService:
             root_trace_id=owner_token.root_trace_id,
             root_trace_id=owner_token.root_trace_id,
         )
         )
 
 
+    async def _set_phase_two_checkpoint(self, owner_token: MissionOwnerToken) -> None:
+        summary = "Phase two accepted one closed candidate portfolio."
+        if self.fenced_command_gate is not None:
+            await self.fenced_command_gate.set_checkpoint(
+                owner_token,
+                checkpoint_code=PHASE_TWO_CANDIDATE_PORTFOLIO_READY,
+                summary=summary,
+            )
+            return
+        await self._verify_owner(owner_token)
+        await self.legacy_state.set_checkpoint(
+            owner_token.script_build_id,
+            checkpoint_code=PHASE_TWO_CANDIDATE_PORTFOLIO_READY,
+            summary=summary,
+            root_trace_id=owner_token.root_trace_id,
+        )
+
     async def _accepted_root_closure(
     async def _accepted_root_closure(
-        self, script_build_id: int, ledger: Any
+        self, script_build_id: int, root_trace_id: str, ledger: Any
     ) -> _AcceptedRootClosure:
     ) -> _AcceptedRootClosure:
         root = ledger.tasks[ledger.root_task_id]
         root = ledger.tasks[ledger.root_task_id]
         if len(root.child_task_ids) != 2:
         if len(root.child_task_ids) != 2:
@@ -435,10 +485,18 @@ class PhaseThreeContinuationService:
                 version.artifact, ScriptDirectionArtifactV1
                 version.artifact, ScriptDirectionArtifactV1
             ):
             ):
                 raise PhaseThreeBoundaryNotReady("accepted Direction has the wrong Artifact type")
                 raise PhaseThreeBoundaryNotReady("accepted Direction has the wrong Artifact type")
+            contract_refs = [
+                item
+                for item in child.current_spec.context_refs
+                if item.startswith("script-build://task-contracts/sha256/")
+            ]
+            if len(contract_refs) != 1:
+                raise PhaseThreeBoundaryNotReady("Root child does not pin one Task contract")
+            child_contract = await self.contracts.read(root_trace_id, contract_refs[0])
             accepted[kind] = AcceptedDecisionRef(
             accepted[kind] = AcceptedDecisionRef(
                 decision_id=decisions[0].decision_id,
                 decision_id=decisions[0].decision_id,
                 artifact_ref=artifact_ref,
                 artifact_ref=artifact_ref,
-                scope_ref="script-build://scope/root",
+                scope_ref=child_contract.contract.scope_ref,
                 expected_task_kind=ScriptTaskKind(kind),
                 expected_task_kind=ScriptTaskKind(kind),
             )
             )
         if set(accepted) != {
         if set(accepted) != {
@@ -467,7 +525,7 @@ class PhaseThreeContinuationService:
     ) -> Any:
     ) -> Any:
         contract = ScriptTaskContractV1(
         contract = ScriptTaskContractV1(
             task_kind=ScriptTaskKind.ROOT_DELIVERY,
             task_kind=ScriptTaskKind.ROOT_DELIVERY,
-            scope_ref="script-build://scope/root",
+            scope_ref=closure.direction.scope_ref,
             intent_class=ScriptIntentClass.DELIVER,
             intent_class=ScriptIntentClass.DELIVER,
             objective=root.current_spec.objective,
             objective=root.current_spec.objective,
             input_decision_refs=(closure.direction, closure.portfolio),
             input_decision_refs=(closure.direction, closure.portfolio),