|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
|
|
|
import base64
|
|
|
from collections.abc import Mapping, Sequence
|
|
|
-from dataclasses import asdict, dataclass
|
|
|
+from dataclasses import asdict, dataclass, replace
|
|
|
from hashlib import sha256
|
|
|
from typing import Any, Protocol, cast
|
|
|
|
|
|
@@ -28,8 +28,12 @@ from script_build_host.domain.artifacts import (
|
|
|
EvidenceRecordV1,
|
|
|
)
|
|
|
from script_build_host.domain.candidate_validation import (
|
|
|
+ artifact_realizes_goals,
|
|
|
+ candidate_content_issues,
|
|
|
comparison_fairness_errors,
|
|
|
placeholder_paths,
|
|
|
+ require_complete_script,
|
|
|
+ require_complete_sources,
|
|
|
)
|
|
|
from script_build_host.domain.defects import ScriptDefectV1
|
|
|
from script_build_host.domain.errors import ProtocolViolation, ScriptBuildError
|
|
|
@@ -44,6 +48,7 @@ from script_build_host.domain.phase_two_artifacts import (
|
|
|
ElementSetArtifactV1,
|
|
|
ParagraphArtifactV1,
|
|
|
ScriptElementV1,
|
|
|
+ ScriptParagraphElementLinkV1,
|
|
|
ScriptParagraphV1,
|
|
|
StructureArtifactV1,
|
|
|
StructuredScriptArtifactV1,
|
|
|
@@ -344,15 +349,19 @@ class PhaseTwoCandidateService:
|
|
|
),
|
|
|
}
|
|
|
]
|
|
|
+ content_issues = candidate_content_issues(version.artifact)
|
|
|
placeholders = placeholder_paths(version.artifact)
|
|
|
+ content_errors = tuple(
|
|
|
+ [*(f"{item.path}: {item.message}" for item in content_issues), *placeholders]
|
|
|
+ )
|
|
|
rules.append(
|
|
|
{
|
|
|
"rule_id": "realized-content",
|
|
|
- "verdict": "failed" if placeholders else "passed",
|
|
|
+ "verdict": "failed" if content_errors else "passed",
|
|
|
"reason": (
|
|
|
- "Unrealized content at " + ", ".join(placeholders[:8])
|
|
|
- if placeholders
|
|
|
- else "Artifact contains no placeholder or deferred-production prose"
|
|
|
+ "Unrealized content at " + ", ".join(content_errors[:8])
|
|
|
+ if content_errors
|
|
|
+ else "Artifact satisfies its deterministic content contract"
|
|
|
),
|
|
|
}
|
|
|
)
|
|
|
@@ -455,12 +464,17 @@ class PhaseTwoCandidateService:
|
|
|
raise PhaseTwoCandidateError(
|
|
|
"GOAL_COVERAGE_INVALID", "StructuredScript uses a stale Direction"
|
|
|
)
|
|
|
+ realized_refs = await self._realized_source_refs(
|
|
|
+ script_build_id=scope.script_build_id,
|
|
|
+ source_refs=structured.source_artifact_refs,
|
|
|
+ )
|
|
|
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,
|
|
|
)
|
|
|
except ScriptBuildError as exc:
|
|
|
coverage_error = f"{exc.code}: {exc.summary}"
|
|
|
@@ -473,6 +487,39 @@ class PhaseTwoCandidateService:
|
|
|
)
|
|
|
return tuple(rules)
|
|
|
|
|
|
+ async def validation_evidence_refs(
|
|
|
+ self, *, context: Mapping[str, Any]
|
|
|
+ ) -> tuple[ArtifactRef, ...]:
|
|
|
+ scope, bundle = await self._resolve_bundle(context)
|
|
|
+ snapshot = await self._framework_artifact_store.get(
|
|
|
+ scope.root_trace_id, _required_context(context, "snapshot_id")
|
|
|
+ )
|
|
|
+ values = [
|
|
|
+ *snapshot.artifact_refs,
|
|
|
+ *snapshot.evidence_refs,
|
|
|
+ *(item.artifact_ref for item in bundle.inputs),
|
|
|
+ ]
|
|
|
+ output: list[ArtifactRef] = []
|
|
|
+ seen: set[tuple[str, str | None, str | None, str]] = set()
|
|
|
+ for ref in values:
|
|
|
+ key = (ref.uri, ref.version, ref.digest, ref.kind)
|
|
|
+ if key not in seen:
|
|
|
+ seen.add(key)
|
|
|
+ output.append(ref)
|
|
|
+ return tuple(output)
|
|
|
+
|
|
|
+ async def _realized_source_refs(
|
|
|
+ self, *, script_build_id: int, source_refs: Sequence[str]
|
|
|
+ ) -> tuple[str, ...]:
|
|
|
+ realized: list[str] = []
|
|
|
+ for ref in source_refs:
|
|
|
+ version = await self._artifacts.get_by_id(
|
|
|
+ _artifact_version_id(ref), script_build_id=script_build_id
|
|
|
+ )
|
|
|
+ if artifact_realizes_goals(version.artifact):
|
|
|
+ realized.append(ref)
|
|
|
+ return tuple(realized)
|
|
|
+
|
|
|
async def read_attempt_workspace(self, *, context: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
|
scope = await self._scope(context)
|
|
|
if scope.contract.task_kind in {ScriptTaskKind.STRUCTURE, ScriptTaskKind.PARAGRAPH}:
|
|
|
@@ -670,25 +717,30 @@ class PhaseTwoCandidateService:
|
|
|
direction_ref, evidence_refs = _compose_support_refs(support_versions)
|
|
|
direction = _accepted_direction(support_versions)
|
|
|
source_refs = tuple(item.artifact_ref.uri for item in frontier)
|
|
|
+ _require_complete_frontier(versions)
|
|
|
+ creative_sources: list[tuple[str, tuple[str, ...]]] = []
|
|
|
+ for version in versions:
|
|
|
+ artifact = version.artifact
|
|
|
+ if isinstance(artifact, (ParagraphArtifactV1, ElementSetArtifactV1)) and (
|
|
|
+ artifact_realizes_goals(artifact)
|
|
|
+ ):
|
|
|
+ creative_sources.append(
|
|
|
+ (
|
|
|
+ f"script-build://artifact-versions/{version.artifact_version_id}",
|
|
|
+ artifact.lineage.goal_ids,
|
|
|
+ )
|
|
|
+ )
|
|
|
+ realized_refs = tuple(ref for ref, _ in creative_sources)
|
|
|
goal_coverage = build_goal_coverage(
|
|
|
direction_goal_ids=tuple(item.goal_id for item in direction.goals),
|
|
|
- creative_sources=(
|
|
|
- (
|
|
|
- f"script-build://artifact-versions/{version.artifact_version_id}",
|
|
|
- version.artifact.lineage.goal_ids,
|
|
|
- )
|
|
|
- for version in versions
|
|
|
- if isinstance(
|
|
|
- version.artifact,
|
|
|
- (StructureArtifactV1, ParagraphArtifactV1, ElementSetArtifactV1),
|
|
|
- )
|
|
|
- ),
|
|
|
+ creative_sources=creative_sources,
|
|
|
)
|
|
|
_require_paragraph_structures(versions)
|
|
|
validate_goal_coverage(
|
|
|
direction_goal_ids=tuple(item.goal_id for item in direction.goals),
|
|
|
coverage=goal_coverage,
|
|
|
adopted_source_refs=source_refs,
|
|
|
+ realized_source_refs=realized_refs,
|
|
|
)
|
|
|
workspace = await self._workspace_repo().get_or_create(
|
|
|
scope.write_context, artifact_kind=ArtifactKind.STRUCTURED_SCRIPT
|
|
|
@@ -791,10 +843,19 @@ class PhaseTwoCandidateService:
|
|
|
raise PhaseTwoCandidateError(
|
|
|
"GOAL_COVERAGE_INVALID", "StructuredScript uses a stale Direction"
|
|
|
)
|
|
|
+ require_complete_script(
|
|
|
+ adopted_script.paragraphs,
|
|
|
+ adopted_script.elements,
|
|
|
+ adopted_script.paragraph_element_links,
|
|
|
+ )
|
|
|
validate_goal_coverage(
|
|
|
direction_goal_ids=tuple(item.goal_id for item in direction_version.artifact.goals),
|
|
|
coverage=adopted_script.goal_coverage,
|
|
|
adopted_source_refs=adopted_script.source_artifact_refs,
|
|
|
+ realized_source_refs=await self._realized_source_refs(
|
|
|
+ script_build_id=scope.script_build_id,
|
|
|
+ source_refs=adopted_script.source_artifact_refs,
|
|
|
+ ),
|
|
|
)
|
|
|
superseded = _text_sequence(payload.get("superseded_decision_ids"))
|
|
|
if not set(superseded) <= set(scope.contract.held_or_rejected_decision_ids):
|
|
|
@@ -1375,6 +1436,11 @@ class PhaseTwoCandidateService:
|
|
|
"LEGACY_REFERENCE_INVALID",
|
|
|
"every adopted Element must be linked to an adopted Paragraph",
|
|
|
)
|
|
|
+ _require_complete_projection(
|
|
|
+ paragraph_projection=paragraph_projection,
|
|
|
+ element_projection=element_projection,
|
|
|
+ links=active_links,
|
|
|
+ )
|
|
|
|
|
|
paragraph_ids: dict[tuple[str, str, int], int] = {}
|
|
|
next_index = 1
|
|
|
@@ -1616,6 +1682,52 @@ def _require_paragraph_structures(versions: Sequence[ArtifactVersion]) -> None:
|
|
|
)
|
|
|
|
|
|
|
|
|
+def _require_complete_frontier(versions: Sequence[ArtifactVersion]) -> None:
|
|
|
+ artifacts = tuple(item.artifact for item in versions)
|
|
|
+ require_complete_sources(artifacts)
|
|
|
+
|
|
|
+
|
|
|
+def _require_complete_projection(
|
|
|
+ *,
|
|
|
+ paragraph_projection: Mapping[
|
|
|
+ tuple[str, str, int],
|
|
|
+ tuple[ScriptParagraphV1, tuple[str, str, int] | None],
|
|
|
+ ],
|
|
|
+ element_projection: Mapping[tuple[str, str, int], ScriptElementV1],
|
|
|
+ links: set[tuple[tuple[str, str, int], tuple[str, str, int]]],
|
|
|
+) -> None:
|
|
|
+ paragraph_keys = {
|
|
|
+ key: index
|
|
|
+ for index, key in enumerate(sorted(paragraph_projection), start=1)
|
|
|
+ }
|
|
|
+ element_keys = {
|
|
|
+ key: index for index, key in enumerate(sorted(element_projection), start=1)
|
|
|
+ }
|
|
|
+ paragraphs = tuple(
|
|
|
+ replace(
|
|
|
+ paragraph,
|
|
|
+ paragraph_id=paragraph_keys[key],
|
|
|
+ paragraph_index=index,
|
|
|
+ parent_id=paragraph_keys[parent_key] if parent_key is not None else None,
|
|
|
+ )
|
|
|
+ for index, (key, (paragraph, parent_key)) in enumerate(
|
|
|
+ sorted(paragraph_projection.items()), start=1
|
|
|
+ )
|
|
|
+ )
|
|
|
+ elements = tuple(
|
|
|
+ replace(element, element_id=element_keys[key])
|
|
|
+ for key, element in sorted(element_projection.items())
|
|
|
+ )
|
|
|
+ projected_links = tuple(
|
|
|
+ ScriptParagraphElementLinkV1(
|
|
|
+ paragraph_id=paragraph_keys[paragraph_key],
|
|
|
+ element_id=element_keys[element_key],
|
|
|
+ )
|
|
|
+ for paragraph_key, element_key in sorted(links)
|
|
|
+ )
|
|
|
+ require_complete_script(paragraphs, elements, projected_links)
|
|
|
+
|
|
|
+
|
|
|
def _image_media_type(content: bytes) -> str:
|
|
|
if content.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
|
return "image/png"
|