from enum import Enum from typing import Literal from typing import Optional from pydantic import BaseModel, Field, model_validator class ChangeOp(str, Enum): CREATE = "create" UPDATE = "update" DELETE = "delete" class PatchHunk(BaseModel): type: Literal["append_end", "replace_between", "replace_line_equals"] new_text: str = Field(default="") start_anchor: Optional[str] = None end_anchor: Optional[str] = None old_line: Optional[str] = None @model_validator(mode="after") def validate_hunk(self) -> "PatchHunk": if self.type == "append_end": if not self.new_text.strip(): raise ValueError("append_end requires non-empty new_text") return self if self.type == "replace_between": if not (self.start_anchor and self.end_anchor): raise ValueError("replace_between requires start_anchor and end_anchor") return self if self.type == "replace_line_equals": if not self.old_line: raise ValueError("replace_line_equals requires old_line") if not self.new_text: raise ValueError("replace_line_equals requires new_text") return self return self class ChangeItem(BaseModel): op: ChangeOp path: str = Field(min_length=1) base_hash: Optional[str] = None proposed_content: Optional[str] = None reason: str = Field(min_length=1, max_length=500) hunks: list[PatchHunk] = Field(default_factory=list) @model_validator(mode="after") def validate_op_fields(self) -> "ChangeItem": if self.op in (ChangeOp.UPDATE, ChangeOp.DELETE) and not self.base_hash: raise ValueError("base_hash is required for update/delete") if self.op in (ChangeOp.CREATE, ChangeOp.UPDATE) and self.proposed_content is None: raise ValueError("proposed_content is required for create/update") if self.op == ChangeOp.DELETE and self.proposed_content is not None: raise ValueError("proposed_content is forbidden for delete") return self class ChangeSetPayload(BaseModel): schema_version: str task_id: str changeset: list[ChangeItem]