Explorar o código

方向持久化:严格读写新的结构化 Direction

让任务恢复、发布闭包和 SQLAlchemy Artifact 水合统一识别 DirectionArtifact。

旧 payload 缺少 goals、constraints、preferences 或 Host 投影不一致时直接拒绝,不保留历史字段回退和隐式兼容。
SamLee hai 23 horas
pai
achega
2d37d9b6e6

+ 2 - 2
script_build_host/src/script_build_host/application/mission_service.py

@@ -16,7 +16,7 @@ from agent.orchestration import DecisionAction, OperationStatus, TaskStatus, Val
 from script_build_host.domain.artifacts import (
     ArtifactKind,
     ArtifactState,
-    ScriptDirectionArtifactV1,
+    DirectionArtifact,
 )
 from script_build_host.domain.canonical_json import canonical_sha256
 from script_build_host.domain.errors import (
@@ -195,7 +195,7 @@ class DirectionReconciler:
             task_id=task.task_id,
             attempt_id=attempt.attempt_id,
         )
-        if not isinstance(version.artifact, ScriptDirectionArtifactV1):
+        if not isinstance(version.artifact, DirectionArtifact):
             raise ProtocolViolation("accepted direction reference has the wrong artifact type")
         if version.state not in {ArtifactState.FROZEN, ArtifactState.PUBLISHED}:
             raise ProtocolViolation("accepted direction artifact is not frozen")

+ 3 - 3
script_build_host/src/script_build_host/application/publisher.py

@@ -16,7 +16,7 @@ from script_build_host.domain.artifacts import (
     ArtifactKind,
     ArtifactState,
     ArtifactVersion,
-    ScriptDirectionArtifactV1,
+    DirectionArtifact,
 )
 from script_build_host.domain.errors import (
     ProtocolViolation,
@@ -176,7 +176,7 @@ class ScriptPublisher:
         RootDeliveryManifestV1,
         ArtifactVersion,
         StructuredScriptArtifactV1,
-        ScriptDirectionArtifactV1,
+        DirectionArtifact,
     ]:
         if (
             owner_token.root_trace_id != root_trace_id
@@ -240,7 +240,7 @@ class ScriptPublisher:
             manifest.structured_script_ref, binding.script_build_id
         )
         if (
-            not isinstance(direction_version.artifact, ScriptDirectionArtifactV1)
+            not isinstance(direction_version.artifact, DirectionArtifact)
             or not isinstance(portfolio_version.artifact, CandidatePortfolioArtifactV1)
             or not isinstance(structured_version.artifact, StructuredScriptArtifactV1)
         ):

+ 32 - 8
script_build_host/src/script_build_host/repositories/sqlalchemy.py

@@ -14,10 +14,11 @@ from script_build_host.domain.artifacts import (
     ArtifactState,
     ArtifactVersion,
     BusinessArtifact,
-    Criterion,
+    DirectionArtifact,
+    DirectionConstraint,
     DirectionGoal,
+    DirectionPreference,
     EvidenceRecordV1,
-    ScriptDirectionArtifactV1,
 )
 from script_build_host.domain.digests import Sha256Digest
 from script_build_host.domain.errors import (
@@ -439,7 +440,7 @@ class SqlAlchemyMissionBindingRepository:
 def _artifact_kind(value: BusinessArtifact) -> ArtifactKind:
     if isinstance(value, EvidenceRecordV1):
         return ArtifactKind.EVIDENCE
-    if isinstance(value, ScriptDirectionArtifactV1):
+    if isinstance(value, DirectionArtifact):
         return ArtifactKind.DIRECTION
     mapping = (
         (StructureArtifactV1, ArtifactKind.STRUCTURE),
@@ -477,17 +478,40 @@ def _hydrate_artifact(kind: ArtifactKind, payload: dict[str, Any], digest: str)
             created_at=datetime.fromisoformat(str(payload["created_at"]).replace("Z", "+00:00")),
         )
     if kind is ArtifactKind.DIRECTION:
-        return ScriptDirectionArtifactV1(
-            goals=tuple(DirectionGoal(**item) for item in payload["goals"]),
-            criteria=tuple(Criterion(**item) for item in payload.get("criteria", [])),
-            domain_criteria=tuple(Criterion(**item) for item in payload.get("domain_criteria", [])),
+        required_fields = {"goals", "constraints", "preferences", "legacy_markdown"}
+        required_goal_fields = {
+            "goal_id",
+            "parent_goal_id",
+            "statement",
+            "rationale",
+            "success_criteria",
+        }
+        if not required_fields.issubset(payload) or any(
+            not required_goal_fields.issubset(item) for item in payload.get("goals", [])
+        ):
+            raise ProtocolViolation("stored direction does not match current structured contract")
+        artifact = DirectionArtifact(
+            goals=tuple(
+                DirectionGoal(
+                    goal_id=str(item["goal_id"]),
+                    statement=str(item["statement"]),
+                    rationale=str(item["rationale"]),
+                    parent_goal_id=item["parent_goal_id"],
+                    success_criteria=tuple(item["success_criteria"]),
+                )
+                for item in payload["goals"]
+            ),
+            constraints=tuple(DirectionConstraint(**item) for item in payload["constraints"]),
+            preferences=tuple(DirectionPreference(**item) for item in payload["preferences"]),
             topic_refs=tuple(payload.get("topic_refs", [])),
             persona_refs=tuple(payload.get("persona_refs", [])),
             strategy_refs=tuple(payload.get("strategy_refs", [])),
             evidence_refs=tuple(payload.get("evidence_refs", [])),
-            legacy_markdown=str(payload.get("legacy_markdown", "")),
             canonical_sha256=digest,
         )
+        if payload["legacy_markdown"] != artifact.legacy_markdown:
+            raise ProtocolViolation("stored direction projection differs from structured content")
+        return artifact
     if kind is ArtifactKind.ROOT_DELIVERY_MANIFEST:
         return hydrate_root_delivery_manifest(payload, digest)
     return replace(hydrate_phase_two_artifact(payload), canonical_sha256=digest)