# #region ReportModels [C:3] [TYPE Module] [SEMANTICS pydantic, report, model, schema, task, task-type] # @defgroup Models Module group. # @BRIEF Canonical report schemas for unified task reporting across heterogeneous task types. # @LAYER Domain # @PRE Pydantic library and task manager models are available. # @POST Provides validated schemas for cross-plugin reporting and UI consumption. # @SIDE_EFFECT None (schema definition). # @DATA_CONTRACT Model[TaskReport, ReportCollection, ReportDetailView] # @RELATION DEPENDS_ON -> [TaskModels] # @INVARIANT Canonical report fields are always present for every report item. from datetime import datetime from enum import Enum from typing import Any from pydantic import BaseModel, Field, field_validator, model_validator # #region TaskType [C:3] [TYPE Class] [SEMANTICS enum, type, task] # @defgroup Models Module group. # @INVARIANT Must contain valid generic task type mappings. # @RELATION DEPENDS_ON -> ReportModels # @BRIEF Supported normalized task report types. class TaskType(str, Enum): LLM_VERIFICATION = "llm_verification" BACKUP = "backup" MIGRATION = "migration" DOCUMENTATION = "documentation" CLEAN_RELEASE = "clean_release" UNKNOWN = "unknown" # #endregion TaskType # #region ReportStatus [C:3] [TYPE Class] [SEMANTICS enum, status, task] # @defgroup Models Module group. # @INVARIANT TaskStatus enum mapping logic holds. # @BRIEF Supported normalized report status values. # @RELATION DEPENDS_ON -> ReportModels class ReportStatus(str, Enum): SUCCESS = "success" FAILED = "failed" IN_PROGRESS = "in_progress" PARTIAL = "partial" # #endregion ReportStatus # #region ErrorContext [C:3] [TYPE Class] [SEMANTICS error, context, payload] # @defgroup Models Module group. # @INVARIANT The properties accurately describe error state. # @BRIEF Error and recovery context for failed/partial reports. # # @TEST_CONTRACT ErrorContextModel -> # { # required_fields: { # message: str # }, # optional_fields: { # code: str, # next_actions: list[str] # } # } # @TEST_FIXTURE basic_error -> {"message": "Connection timeout", "code": "ERR_504", "next_actions": ["retry"]} # @TEST_EDGE missing_message -> {"code": "ERR_504"} # @RELATION DEPENDS_ON -> ReportModels class ErrorContext(BaseModel): code: str | None = None message: str next_actions: list[str] = Field(default_factory=list) # #endregion ErrorContext # #region TaskReport [C:3] [TYPE Class] [SEMANTICS report, model, summary] # @defgroup Models Module group. # @INVARIANT Must represent canonical task record attributes. # @BRIEF Canonical normalized report envelope for one task execution. # # @TEST_CONTRACT TaskReportModel -> # { # required_fields: { # report_id: str, # task_id: str, # task_type: TaskType, # status: ReportStatus, # updated_at: datetime, # summary: str # }, # invariants: [ # "report_id is a non-empty string", # "task_id is a non-empty string", # "summary is a non-empty string" # ] # } # @TEST_FIXTURE valid_task_report -> # { # report_id: "rep-123", # task_id: "task-456", # task_type: "migration", # status: "success", # updated_at: "2026-02-26T12:00:00Z", # summary: "Migration completed successfully" # } # @TEST_EDGE empty_report_id -> {"report_id": " ", "task_id": "task-456", "task_type": "migration", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"} # @TEST_EDGE empty_summary -> {"report_id": "rep-123", "task_id": "task-456", "task_type": "migration", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": ""} # @TEST_EDGE invalid_task_type -> {"report_id": "rep-123", "task_id": "task-456", "task_type": "invalid_type", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"} # @TEST_INVARIANT non_empty_validators -> verifies: [empty_report_id, empty_summary] # @RELATION DEPENDS_ON -> ReportModels class TaskReport(BaseModel): report_id: str task_id: str task_type: TaskType status: ReportStatus started_at: datetime | None = None updated_at: datetime summary: str details: dict[str, Any] | None = None validation_record: dict[str, Any] | None = None # Extended for US2 error_context: ErrorContext | None = None source_ref: dict[str, Any] | None = None @field_validator("report_id", "task_id", "summary") @classmethod def _non_empty_str(cls, value: str) -> str: if not isinstance(value, str) or not value.strip(): raise ValueError("Value must be a non-empty string") return value.strip() # #endregion TaskReport # #region ReportQuery [C:3] [TYPE Class] [SEMANTICS query, filter, search] # @defgroup Models Module group. # @INVARIANT Time and pagination queries are mutually consistent. # @BRIEF Query object for server-side report filtering, sorting, and pagination. # # @TEST_CONTRACT ReportQueryModel -> # { # optional_fields: { # page: int, page_size: int, task_types: list[TaskType], statuses: list[ReportStatus], # time_from: datetime, time_to: datetime, search: str, sort_by: str, sort_order: str # }, # invariants: [ # "page >= 1", "1 <= page_size <= 100", # "sort_by in {'updated_at', 'status', 'task_type'}", # "sort_order in {'asc', 'desc'}", # "time_from <= time_to if both exist" # ] # } # @TEST_FIXTURE valid_query -> {"page": 1, "page_size":20, "sort_by": "updated_at", "sort_order": "desc"} # @TEST_EDGE invalid_page_size_large -> {"page_size": 150} # @TEST_EDGE invalid_sort_by -> {"sort_by": "unknown_field"} # @TEST_EDGE invalid_time_range -> {"time_from": "2026-02-26T12:00:00Z", "time_to": "2026-02-25T12:00:00Z"} # @TEST_INVARIANT attribute_constraints_enforced -> verifies: [invalid_page_size_large, invalid_sort_by, invalid_time_range] # @RELATION DEPENDS_ON -> ReportModels class ReportQuery(BaseModel): page: int = Field(default=1, ge=1) page_size: int = Field(default=20, ge=1, le=100) task_types: list[TaskType] = Field(default_factory=list) statuses: list[ReportStatus] = Field(default_factory=list) time_from: datetime | None = None time_to: datetime | None = None search: str | None = Field(default=None, max_length=200) sort_by: str = Field(default="updated_at") sort_order: str = Field(default="desc") @field_validator("sort_by") @classmethod def _validate_sort_by(cls, value: str) -> str: allowed = {"updated_at", "status", "task_type"} if value not in allowed: raise ValueError(f"sort_by must be one of: {', '.join(sorted(allowed))}") return value @field_validator("sort_order") @classmethod def _validate_sort_order(cls, value: str) -> str: if value not in {"asc", "desc"}: raise ValueError("sort_order must be 'asc' or 'desc'") return value @model_validator(mode="after") def _validate_time_range(self): if self.time_from and self.time_to and self.time_from > self.time_to: raise ValueError("time_from must be less than or equal to time_to") return self # #endregion ReportQuery # #region ReportCollection [C:3] [TYPE Class] [SEMANTICS collection, pagination] # @defgroup Models Module group. # @INVARIANT Represents paginated data correctly. # @BRIEF Paginated collection of normalized task reports. # # @TEST_CONTRACT ReportCollectionModel -> # { # required_fields: { # items: list[TaskReport], total: int, page: int, page_size: int, has_next: bool, applied_filters: ReportQuery # }, # invariants: ["total >= 0", "page >= 1", "page_size >= 1"] # } # @TEST_FIXTURE empty_collection -> {"items": [], "total": 0, "page": 1, "page_size": 20, "has_next": False, "applied_filters": {}} # @TEST_EDGE negative_total -> {"items": [], "total": -5, "page": 1, "page_size": 20, "has_next": False, "applied_filters": {}} # @RELATION DEPENDS_ON -> ReportModels class ReportCollection(BaseModel): items: list[TaskReport] total: int = Field(ge=0) page: int = Field(ge=1) page_size: int = Field(ge=1) has_next: bool applied_filters: ReportQuery # #endregion ReportCollection # #region ReportDetailView [C:3] [TYPE Class] [SEMANTICS view, detail, logs] # @defgroup Models Module group. # @INVARIANT Incorporates a report and logs correctly. # @BRIEF Detailed report representation including diagnostics and recovery actions. # # @TEST_CONTRACT ReportDetailViewModel -> # { # required_fields: {report: TaskReport}, # optional_fields: {timeline: list[dict], diagnostics: dict, next_actions: list[str]} # } # @TEST_FIXTURE valid_detail -> {"report": {"report_id": "rep-1", "task_id": "task-1", "task_type": "backup", "status": "success", "updated_at": "2026-02-26T12:00:00Z", "summary": "Done"}} # @TEST_EDGE missing_report -> {} # @RELATION DEPENDS_ON -> ReportModels class ReportDetailView(BaseModel): report: TaskReport timeline: list[dict[str, Any]] = Field(default_factory=list) diagnostics: dict[str, Any] | None = None next_actions: list[str] = Field(default_factory=list) # #endregion ReportDetailView # #endregion ReportModels