|
|
@@ -0,0 +1,187 @@
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+from dataclasses import dataclass
|
|
|
+from datetime import datetime
|
|
|
+from enum import StrEnum
|
|
|
+from typing import Any
|
|
|
+
|
|
|
+
|
|
|
+class ArtifactKind(StrEnum):
|
|
|
+ EVIDENCE = "evidence"
|
|
|
+ DIRECTION = "direction"
|
|
|
+
|
|
|
+
|
|
|
+class ArtifactState(StrEnum):
|
|
|
+ DRAFT = "draft"
|
|
|
+ FROZEN = "frozen"
|
|
|
+ PUBLISHED = "published"
|
|
|
+ DISCARDED = "discarded"
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(frozen=True, slots=True)
|
|
|
+class DirectionGoal:
|
|
|
+ goal_id: str
|
|
|
+ statement: str
|
|
|
+ rationale: str = ""
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(frozen=True, slots=True)
|
|
|
+class Criterion:
|
|
|
+ criterion_id: str
|
|
|
+ description: str
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(frozen=True, slots=True)
|
|
|
+class ScriptDirectionArtifactV1:
|
|
|
+ goals: tuple[DirectionGoal, ...]
|
|
|
+ criteria: tuple[Criterion, ...] = ()
|
|
|
+ domain_criteria: tuple[Criterion, ...] = ()
|
|
|
+ topic_refs: tuple[str, ...] = ()
|
|
|
+ persona_refs: tuple[str, ...] = ()
|
|
|
+ strategy_refs: tuple[str, ...] = ()
|
|
|
+ evidence_refs: tuple[str, ...] = ()
|
|
|
+ legacy_markdown: str = ""
|
|
|
+ canonical_sha256: str = ""
|
|
|
+
|
|
|
+ def __post_init__(self) -> None:
|
|
|
+ if not 1 <= len(self.goals) <= 3:
|
|
|
+ raise ValueError("a direction artifact must contain between one and three goals")
|
|
|
+ _require_unique_text(
|
|
|
+ ((item.goal_id, item.statement) for item in self.goals),
|
|
|
+ label="direction goal",
|
|
|
+ )
|
|
|
+ _require_unique_text(
|
|
|
+ ((item.criterion_id, item.description) for item in self.criteria),
|
|
|
+ label="direction criterion",
|
|
|
+ )
|
|
|
+ _require_unique_text(
|
|
|
+ ((item.criterion_id, item.description) for item in self.domain_criteria),
|
|
|
+ label="domain criterion",
|
|
|
+ )
|
|
|
+ for label, refs in (
|
|
|
+ ("topic", self.topic_refs),
|
|
|
+ ("persona", self.persona_refs),
|
|
|
+ ("strategy", self.strategy_refs),
|
|
|
+ ("evidence", self.evidence_refs),
|
|
|
+ ):
|
|
|
+ if any(not isinstance(ref, str) or not ref.strip() for ref in refs):
|
|
|
+ raise ValueError(f"{label} references must be non-empty strings")
|
|
|
+ if len(set(refs)) != len(refs):
|
|
|
+ raise ValueError(f"{label} references must be unique")
|
|
|
+ if any(
|
|
|
+ not ref.startswith("script-build://artifact-versions/") for ref in self.evidence_refs
|
|
|
+ ):
|
|
|
+ raise ValueError("evidence references must use immutable business Artifact URIs")
|
|
|
+ if not self.evidence_refs:
|
|
|
+ raise ValueError("a direction artifact must cite accepted retrieval evidence")
|
|
|
+ if not self.legacy_markdown.strip():
|
|
|
+ raise ValueError("legacy direction markdown must not be blank")
|
|
|
+
|
|
|
+ def content_payload(self) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "schema_version": "script-direction/v1",
|
|
|
+ "goals": [
|
|
|
+ {"goal_id": item.goal_id, "statement": item.statement, "rationale": item.rationale}
|
|
|
+ for item in self.goals
|
|
|
+ ],
|
|
|
+ "criteria": [
|
|
|
+ {"criterion_id": item.criterion_id, "description": item.description}
|
|
|
+ for item in self.criteria
|
|
|
+ ],
|
|
|
+ "domain_criteria": [
|
|
|
+ {"criterion_id": item.criterion_id, "description": item.description}
|
|
|
+ for item in self.domain_criteria
|
|
|
+ ],
|
|
|
+ "topic_refs": list(self.topic_refs),
|
|
|
+ "persona_refs": list(self.persona_refs),
|
|
|
+ "strategy_refs": list(self.strategy_refs),
|
|
|
+ "evidence_refs": list(self.evidence_refs),
|
|
|
+ "legacy_markdown": self.legacy_markdown,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(frozen=True, slots=True)
|
|
|
+class EvidenceRecordV1:
|
|
|
+ evidence_id: str
|
|
|
+ source_type: str
|
|
|
+ tool_name: str
|
|
|
+ query: dict[str, Any]
|
|
|
+ source_refs: tuple[str, ...]
|
|
|
+ raw_artifact_ref: str | None
|
|
|
+ summary: str
|
|
|
+ supports: tuple[str, ...]
|
|
|
+ confidence: str
|
|
|
+ limitations: tuple[str, ...]
|
|
|
+ content_sha256: str
|
|
|
+ created_at: datetime
|
|
|
+
|
|
|
+ def __post_init__(self) -> None:
|
|
|
+ for label, value in (
|
|
|
+ ("evidence_id", self.evidence_id),
|
|
|
+ ("source_type", self.source_type),
|
|
|
+ ("tool_name", self.tool_name),
|
|
|
+ ):
|
|
|
+ if not value.strip():
|
|
|
+ raise ValueError(f"{label} must not be blank")
|
|
|
+ if any(not ref.strip() for ref in self.source_refs):
|
|
|
+ raise ValueError("evidence source references must not be blank")
|
|
|
+ if len(set(self.source_refs)) != len(self.source_refs):
|
|
|
+ raise ValueError("evidence source references must be unique")
|
|
|
+ if not self.summary.strip() or not self.confidence.strip():
|
|
|
+ raise ValueError("evidence summary and confidence must not be blank")
|
|
|
+ if any(not item.strip() for item in (*self.supports, *self.limitations)):
|
|
|
+ raise ValueError("evidence supports and limitations must not contain blank values")
|
|
|
+ if len(set(self.supports)) != len(self.supports):
|
|
|
+ raise ValueError("evidence supports must be unique")
|
|
|
+ if self.raw_artifact_ref is not None and not self.raw_artifact_ref.startswith(
|
|
|
+ "script-build://raw-artifacts/sha256/"
|
|
|
+ ):
|
|
|
+ raise ValueError("raw artifact references must use the immutable raw namespace")
|
|
|
+
|
|
|
+ def content_payload(self) -> dict[str, Any]:
|
|
|
+ return {
|
|
|
+ "schema_version": "evidence-record/v1",
|
|
|
+ "evidence_id": self.evidence_id,
|
|
|
+ "source_type": self.source_type,
|
|
|
+ "tool_name": self.tool_name,
|
|
|
+ "query": self.query,
|
|
|
+ "source_refs": list(self.source_refs),
|
|
|
+ "raw_artifact_ref": self.raw_artifact_ref,
|
|
|
+ "summary": self.summary,
|
|
|
+ "supports": list(self.supports),
|
|
|
+ "confidence": self.confidence,
|
|
|
+ "limitations": list(self.limitations),
|
|
|
+ "created_at": self.created_at,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+BusinessArtifact = EvidenceRecordV1 | ScriptDirectionArtifactV1
|
|
|
+
|
|
|
+
|
|
|
+@dataclass(frozen=True, slots=True)
|
|
|
+class ArtifactVersion:
|
|
|
+ artifact_version_id: int
|
|
|
+ script_build_id: int
|
|
|
+ task_id: str
|
|
|
+ attempt_id: str
|
|
|
+ spec_version: int
|
|
|
+ artifact_type: ArtifactKind
|
|
|
+ canonical_sha256: str
|
|
|
+ state: ArtifactState
|
|
|
+ artifact: BusinessArtifact
|
|
|
+ created_at: datetime
|
|
|
+ frozen_at: datetime
|
|
|
+
|
|
|
+
|
|
|
+def _require_unique_text(
|
|
|
+ values: Any,
|
|
|
+ *,
|
|
|
+ label: str,
|
|
|
+) -> None:
|
|
|
+ identifiers: list[str] = []
|
|
|
+ for identifier, text in values:
|
|
|
+ if not identifier.strip() or not text.strip():
|
|
|
+ raise ValueError(f"{label} identity and content must not be blank")
|
|
|
+ identifiers.append(identifier)
|
|
|
+ if len(set(identifiers)) != len(identifiers):
|
|
|
+ raise ValueError(f"{label} IDs must be unique")
|