Source code for ralph.mcp.artifacts.plan._acceptance_criteria
"""Acceptance-criteria sub-section for the plan design schema."""
from __future__ import annotations
from pydantic import ConfigDict, Field, field_validator
from ralph.pydantic_compat import RalphBaseModel
[docs]
class AcceptanceCriterion(RalphBaseModel):
model_config = ConfigDict(extra="forbid")
id: str = Field(..., min_length=1, pattern=r"^[A-Z]+-\d{2,}$")
description: str = Field(
...,
min_length=1,
max_length=8000,
description="Description (max 8000 chars; medium tier).",
)
verification_step: str | None = Field(
default=None,
max_length=2000,
description="Optional verification step (max 2000 chars; medium tier).",
)
evidence_path: str | None = Field(
default=None,
max_length=1000,
description="Optional evidence path (max 1000 chars; short tier).",
)
satisfied_by_steps: list[int] = Field(default_factory=list, max_length=50)
@field_validator("id")
@classmethod
def _strip_id(cls, value: str) -> str:
stripped = value.strip()
if not stripped:
msg = "acceptance_criterion.id must not be empty"
raise ValueError(msg)
return stripped
@field_validator("description")
@classmethod
def _strip_description(cls, value: str) -> str:
stripped = value.strip()
if not stripped:
msg = "acceptance_criterion.description must not be empty"
raise ValueError(msg)
return stripped
@field_validator("satisfied_by_steps", mode="before")
@classmethod
def _validate_satisfied_by_steps(cls, value: object) -> list[int]:
if not isinstance(value, list):
msg = "satisfied_by_steps must be a list of integers"
raise ValueError(msg)
cleaned: list[int] = []
seen: set[int] = set()
for raw_entry in value:
entry: object = raw_entry
if isinstance(entry, dict) and isinstance(entry.get("removed_step_number"), int):
msg = (
"satisfied_by_steps references removed step "
f"{entry['removed_step_number']}; choose a surviving file_change or action step"
)
raise ValueError(msg)
if isinstance(entry, bool) or not isinstance(entry, int):
msg = f"satisfied_by_steps entry must be an int, got {type(entry).__name__}"
raise ValueError(msg)
assert isinstance(entry, int)
if entry < 1:
msg = "satisfied_by_steps entries must be positive integers"
raise ValueError(msg)
if entry in seen:
continue
seen.add(entry)
cleaned.append(entry)
return cleaned
[docs]
class AcceptanceCriteria(RalphBaseModel):
model_config = ConfigDict(extra="forbid")
criteria: list[AcceptanceCriterion] = Field(..., min_length=1, max_length=500)
@field_validator("criteria")
@classmethod
def _reject_duplicate_ids(
cls, criteria: list[AcceptanceCriterion]
) -> list[AcceptanceCriterion]:
seen: set[str] = set()
for criterion in criteria:
lowered = criterion.id.lower()
if lowered in seen:
msg = f"duplicate acceptance_criterion id (case-insensitive): {criterion.id!r}"
raise ValueError(msg)
seen.add(lowered)
return criteria
__all__ = ["AcceptanceCriteria", "AcceptanceCriterion"]