"""规则端点(C3 扩 / PRODUCT_SPEC §7 `POST /projects/:id/rules`;不变量 #3)。 POST /projects/:id/rules:作者显式加规则——审稿发现的问题/亮点随手沉淀为项目规则。 写一行 `rules`(level + content),喂给 assemble 的四级合并(`merge_rules`)。 加规则是**作者显式动作**(不变量 #3:规则入库不经 AI 静默写库)。`level` 合法性由 `RuleCreateRequest` 的 Literal 校验(非法 → FastAPI 422)。 提交边界:`RuleWriteRepo.create` 只 `flush()`,端点写后 `await session.commit()` (仿 foreshadow/outline 写侧,见 memory/gotchas)。 """ from __future__ import annotations import uuid from typing import Annotated from fastapi import APIRouter, Depends, Request, Response from sqlalchemy.ext.asyncio import AsyncSession from ww_core.domain import RuleWriteRepo from ww_core.domain.project_repo import ProjectRepo from ww_db import get_session from ww_shared import AppError, ErrorCode from ww_api.logging_config import get_logger from ww_api.schemas.rules import RuleCreateRequest, RuleView from ww_api.services.credentials import STUB_OWNER_ID from ww_api.services.project_deps import get_project_repo, get_rule_write_repo log = get_logger("ww.api.rules") router = APIRouter(prefix="/projects", tags=["rules"]) RuleWriteRepoDep = Annotated[RuleWriteRepo, Depends(get_rule_write_repo)] ProjectRepoDep = Annotated[ProjectRepo, Depends(get_project_repo)] SessionDep = Annotated[AsyncSession, Depends(get_session)] @router.post("/{project_id}/rules", status_code=201) async def create_rule( project_id: uuid.UUID, body: RuleCreateRequest, request: Request, repo: RuleWriteRepoDep, project_repo: ProjectRepoDep, session: SessionDep, ) -> RuleView: """新增一条规则(201)。非法 level / 空 content → FastAPI 422;项目不存在 → 404。 项目存在性须在 insert 前校验:否则 FK 违例会逃逸成 500(QA H2),而非干净的 404。 """ request_id = getattr(request.state, "request_id", None) if await project_repo.get(STUB_OWNER_ID, project_id) is None: raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}") view = await repo.create(project_id, level=body.level, content=body.content) await session.commit() log.info( "rule_created", project_id=str(project_id), request_id=request_id, level=body.level, ) return RuleView(id=view.id, level=view.level, content=view.content) @router.delete("/{project_id}/rules/{rule_id}", status_code=204) async def delete_rule( project_id: uuid.UUID, rule_id: uuid.UUID, request: Request, repo: RuleWriteRepoDep, project_repo: ProjectRepoDep, session: SessionDep, ) -> Response: """删除一条本作品规则(204)。项目不存在 → 404;规则不存在 / 不属于该项目 → 404。 删规则是**作者显式动作**(不变量 #3:规则增删不经 AI 静默写库)。repo.delete 只 flush, 端点提交;删不到行(未知 id / 跨项目)→ 不提交、抛 404。 """ request_id = getattr(request.state, "request_id", None) if await project_repo.get(STUB_OWNER_ID, project_id) is None: raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}") deleted = await repo.delete(project_id, rule_id) if not deleted: raise AppError(ErrorCode.NOT_FOUND, f"rule not found: {rule_id}") await session.commit() log.info( "rule_deleted", project_id=str(project_id), rule_id=str(rule_id), request_id=request_id, ) return Response(status_code=204)