fix(status-enum): add paused to ProjectStatus, validate, fix progress
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 46s
CI & Build / Python tests (push) Successful in 50s
CI & Build / Build & push image (push) Successful in 1m14s

Drift-audit Group 6 + Group 5 #6 (enum-extension / status drift):

- ProjectStatus gains 'paused' — routes and frontend already treated it as
  first-class, but the enum (the source of truth) omitted it and the error
  strings lied. A future CHECK derived from the enum would have rejected
  existing paused rows.
- create_project/update_project now validate status via ProjectStatus at the
  service layer (canonical gate; notes.status has no DB CHECK), so the MCP
  create/update_project path can't persist a typo'd status. MCP docstrings
  realigned to the 4-value domain; route error strings corrected.
- get_milestone_progress: cancelled tasks are excluded from the percent
  denominator (and now reported in status_counts), so a milestone whose only
  open task was cancelled reaches 100% instead of stalling below it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 19:16:45 -04:00
parent 4a220db513
commit c016bd664e
5 changed files with 31 additions and 7 deletions
+18 -1
View File
@@ -6,11 +6,25 @@ from sqlalchemy import func, select
from fabledassistant.models import async_session
from fabledassistant.models.note import Note
from fabledassistant.models.project import Project
from fabledassistant.models.project import Project, ProjectStatus
logger = logging.getLogger(__name__)
def _validate_status(status: str) -> str:
"""Coerce/validate a project status against ProjectStatus.
Canonical gate so the MCP create/update_project path (which passes status
straight through) can't persist an out-of-enum value — there's no DB CHECK.
"""
try:
return ProjectStatus(status).value
except ValueError:
raise ValueError(
f"Invalid status: {status!r}. Must be one of: {[s.value for s in ProjectStatus]}"
)
async def create_project(
user_id: int,
title: str,
@@ -19,6 +33,7 @@ async def create_project(
color: str | None = None,
status: str = "active",
) -> Project:
status = _validate_status(status)
async with async_session() as session:
project = Project(
user_id=user_id,
@@ -87,6 +102,8 @@ async def update_project(user_id: int, project_id: int, **fields: object) -> Pro
project = result.scalars().first()
if project is None:
return None
if "status" in fields and fields["status"] is not None:
fields["status"] = _validate_status(fields["status"])
for key, value in fields.items():
if hasattr(project, key):
setattr(project, key, value)