"""Job / Experiment Repository Protocol(Phase 4)。""" from __future__ import annotations from typing import Protocol from app.domain.entities.research import ExperimentRecord, ExperimentSummary, JobRecord class JobRepository(Protocol): def create(self, job: JobRecord) -> JobRecord: ... def get(self, job_id: str) -> JobRecord | None: ... def update(self, job: JobRecord) -> None: ... def list_recent(self, kind: str | None = None, limit: int = 20) -> list[JobRecord]: ... def list_by_status(self, status: str, limit: int = 100) -> list[JobRecord]: """按状态查询(服务启动清理残留 queued/running 用)。""" class ExperimentRepository(Protocol): def save(self, experiment: ExperimentRecord) -> ExperimentRecord: ... def upsert(self, experiment: ExperimentRecord) -> ExperimentRecord: """按 id 插入或**覆盖**(重建归档用:同一 id 已存在时替换整行)。 与 `save` 的区别:`save` 只插入(同 id 会主键冲突),用于新归档; `upsert` 用于灾备/重建场景 —— 例如从 Job 副本重建一条被删除的历史归档, 此时归档 id 必须保持不变(外部链接、对比记录仍指向它)。 """ ... def get(self, experiment_id: str) -> ExperimentRecord | None: ... def list_recent(self, limit: int = 50) -> list[ExperimentRecord]: ... def list_filtered( self, *, kind: str | None = None, q: str | None = None, limit: int = 200, offset: int = 0, ) -> list[ExperimentSummary]: """按 kind 精确过滤 + q 模糊过滤(id / 因子名 / summary_text,大小写不敏感)。 只返回元数据(ExperimentSummary,**不含 result_json**),过滤与分页在 SQL 层完成;排序为 created_at 倒序 + id 倒序(同秒创建时保证分页稳定)。 """ def count_filtered(self, *, kind: str | None = None, q: str | None = None) -> int: """与 `list_filtered` 同口径的过滤总数(列表接口 X-Total-Count 用)。""" def delete(self, experiment_id: str) -> bool: """删除归档本身,返回是否存在。**不触碰**关联的 Job 记录。"""