#5 规则 DELETE + id:暴露 RuleView.id(PK);新增 DELETE /projects/{id}/rules/{rule_id} (项目/规则不存在→404,成功→204,按 (id,project_id) 限定);rule_repo 加 list_for_project/delete;RulesPage 每条加删除(乐观删+回滚+toast)。assemble 侧 RuleView(缓存前缀)不动,列表另立 RuleListItemView。 #7 codex 角色 relations:写侧本已持久化、读端点 _existing_characters 硬编码 []。 加 _relations_from_jsonb 解析 {name,kind,note},CodexPage 渲染关系 chip。 #8 角色入库幂等:SqlCharacterWriteRepo.create 改 (project_id,name) app 层 upsert—— 重复入库改更新而非插入;不加 UNIQUE/迁移(线上已有重复行会让约束迁移失败)。 #1 大纲卷过滤:GET /outline 支持可选 ?volume(无参=全部,向后兼容);OutlineEditor 加「查看:全部/卷N」筛选,与生成目标卷解耦。 H3/#9 文风回炉锚点:StyleDriftSegment 加 text(逐字命中段),style.md 指示审稿输出; 前端按内容锚点定位回炉目标(idx 仅排序),命中失败 → 提示「无法定位该段」而非 静默 no-op。style golden fixture 已重生成。 契约变更已 pnpm gen:api(RuleView.id / DELETE rules / outline ?volume)。无迁移 (alembic 无漂移)。门禁绿:ruff/mypy(210)/alembic/pytest 760 · 前端 tsc/lint/vitest 329。
94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
"""规则端点(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)
|