Преглед изворни кода

主机:在 Phase2 边界强制闭合真实 Goal Coverage

让 Goal Coverage 只接受内容完整的 Paragraph 与 ElementSet 来源,并在 CandidatePortfolio 阶段边界复用同一领域规则,阻断 Structure-only 和悬空实现来源。
SamLee пре 11 часа
родитељ
комит
987143bf9c

+ 24 - 0
script_build_host/src/script_build_host/application/phase_two_boundary.py

@@ -7,6 +7,11 @@ from typing import Any
 from agent.orchestration import DecisionAction, OperationStatus, TaskStatus
 
 from script_build_host.domain.artifacts import ArtifactKind, ArtifactState, DirectionArtifact
+from script_build_host.domain.candidate_validation import (
+    artifact_realizes_goals,
+    require_complete_script,
+    require_complete_sources,
+)
 from script_build_host.domain.errors import PhaseTwoBoundaryNotReady, ScriptBuildError
 from script_build_host.domain.goal_coverage import validate_goal_coverage
 from script_build_host.domain.phase_two_artifacts import (
@@ -220,10 +225,29 @@ class ScriptPhaseTwoBoundaryVerifier:
             raise PhaseTwoBoundaryNotReady("adopted StructuredScript has the wrong type")
         if structured.direction_ref != _artifact_uri(direction_version_id):
             raise PhaseTwoBoundaryNotReady("StructuredScript uses a stale Direction")
+        require_complete_script(
+            structured.paragraphs,
+            structured.elements,
+            structured.paragraph_element_links,
+        )
+        realized_refs: list[str] = []
+        source_artifacts: list[Any] = []
+        for ref in structured.source_artifact_refs:
+            identifier = ref.removeprefix("script-build://artifact-versions/")
+            if not identifier.isdigit() or _artifact_uri(int(identifier)) != ref:
+                raise PhaseTwoBoundaryNotReady("Goal source ref is not immutable")
+            source = await self._artifacts.get_by_id(
+                int(identifier), script_build_id=script_build_id
+            )
+            source_artifacts.append(source.artifact)
+            if artifact_realizes_goals(source.artifact):
+                realized_refs.append(ref)
+        require_complete_sources(source_artifacts)
         validate_goal_coverage(
             direction_goal_ids=tuple(item.goal_id for item in direction_version.artifact.goals),
             coverage=structured.goal_coverage,
             adopted_source_refs=structured.source_artifact_refs,
+            realized_source_refs=realized_refs,
         )
         if any(item.status in _ACTIVE_OPERATIONS for item in ledger.operations.values()):
             raise PhaseTwoBoundaryNotReady("active or stopping Operations prevent checkpoint")

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

@@ -121,6 +121,7 @@ def validate_goal_coverage(
     direction_goal_ids: Sequence[str],
     coverage: Sequence[GoalCoverage],
     adopted_source_refs: Sequence[str],
+    realized_source_refs: Sequence[str] | None = None,
 ) -> None:
     """Prove exact all-node coverage and adopted-closure membership."""
 
@@ -145,3 +146,17 @@ def validate_goal_coverage(
             "GOAL_COVERAGE_INVALID",
             f"Goal coverage references non-adopted artifacts: {sorted(dangling)}",
         )
+    if realized_source_refs is not None:
+        realized = set(realized_source_refs)
+        unrealized = {
+            ref
+            for item in coverage
+            for ref in item.source_artifact_refs
+            if ref not in realized
+        }
+        if unrealized:
+            raise GoalPolicyError(
+                "GOAL_COVERAGE_INVALID",
+                "Goal coverage uses planning-only or incomplete artifacts: "
+                f"{sorted(unrealized)}",
+            )

+ 90 - 5
script_build_host/tests/test_phase_two_boundary.py

@@ -35,9 +35,15 @@ from script_build_host.domain.artifacts import (
 )
 from script_build_host.domain.errors import PhaseTwoBoundaryNotReady
 from script_build_host.domain.phase_two_artifacts import (
+    CandidateLineageV1,
     CandidatePortfolioArtifactV1,
+    ElementSetArtifactV1,
     GoalCoverage,
+    ParagraphArtifactV1,
+    ScriptElementV1,
+    ScriptParagraphElementLinkV1,
     ScriptParagraphV1,
+    StructureArtifactV1,
     StructuredScriptArtifactV1,
 )
 from script_build_host.domain.records import MissionBinding
@@ -58,6 +64,28 @@ SCOPE = "script-build://scopes/full"
 WRITE = "script-build://writes/full"
 
 
+def _complete_paragraph() -> ScriptParagraphV1:
+    atom = ({"原子点": "反转", "维度": "开场", "维度类型": "主维度"},)
+    return ScriptParagraphV1(
+        1,
+        1,
+        1,
+        None,
+        "opening",
+        {"scope": "opening"},
+        theme="具体反转",
+        form="对比",
+        function="吸引注意",
+        feeling="好奇",
+        theme_elements=atom,
+        form_elements=atom,
+        function_elements=atom,
+        feeling_elements=atom,
+        description="用可观察的细节推翻初始判断。",
+        full_description="大家以为问题来自天赋。但一个真实行动已经改变了结果。",
+    )
+
+
 def _task(task_id: str, parent: str | None, kind: str, status: TaskStatus) -> TaskRecord:
     return TaskRecord(
         task_id=task_id,
@@ -276,15 +304,36 @@ async def _fixture() -> tuple[ScriptPhaseTwoBoundaryVerifier, TaskLedger]:
             [],
         )
     }
-    paragraph = ScriptParagraphV1(1, 1, 1, None, "opening", {}, description="done")
+    paragraph = _complete_paragraph()
+    element = ScriptElementV1(1, "可观察反转", "实质", "叙事证据")
+    link = ScriptParagraphElementLinkV1(1, 1)
+    lineage = CandidateLineageV1(
+        scope_ref=SCOPE,
+        input_snapshot_ref=f"script-build://inputs/{SNAPSHOT_ID}",
+        input_closure_digest=INPUT_DIGEST,
+        write_scope=("script-build://writes/paragraphs",),
+        goal_ids=("goal-1",),
+    )
     structured = StructuredScriptArtifactV1(
         "script-build://artifact-versions/9",
         INPUT_DIGEST,
         (paragraph,),
-        (),
-        (),
-        ("script-build://artifact-versions/8",),
-        (GoalCoverage("goal-1", ("script-build://artifact-versions/8",)),),
+        (element,),
+        (link,),
+        (
+            "script-build://artifact-versions/6",
+            "script-build://artifact-versions/7",
+            "script-build://artifact-versions/8",
+        ),
+        (
+            GoalCoverage(
+                "goal-1",
+                (
+                    "script-build://artifact-versions/7",
+                    "script-build://artifact-versions/8",
+                ),
+            ),
+        ),
         (),
         (),
         SCRIPT_DIGEST,
@@ -317,6 +366,42 @@ async def _fixture() -> tuple[ScriptPhaseTwoBoundaryVerifier, TaskLedger]:
                     evidence_refs=("script-build://artifact-versions/99",),
                 ),
             ),
+            6: _version(
+                6,
+                "structure",
+                "attempt-structure",
+                ArtifactKind.STRUCTURE,
+                INPUT_DIGEST,
+                StructureArtifactV1(lineage, (paragraph,)),
+            ),
+            7: _version(
+                7,
+                "paragraph",
+                "attempt-paragraph",
+                ArtifactKind.PARAGRAPH,
+                INPUT_DIGEST,
+                ParagraphArtifactV1(
+                    lineage,
+                    (paragraph,),
+                    (element,),
+                    (link,),
+                    {"created": {"paragraph_ids": [1]}},
+                ),
+            ),
+            8: _version(
+                8,
+                "elements",
+                "attempt-elements",
+                ArtifactKind.ELEMENT_SET,
+                INPUT_DIGEST,
+                ElementSetArtifactV1(
+                    replace(lineage, write_scope=("script-build://writes/elements",)),
+                    (element,),
+                    (link,),
+                    (paragraph,),
+                    {"created": {"element_ids": [1]}},
+                ),
+            ),
         }
     )
     contracts = _Contracts(