fix(projects): audit pass — 8 correctness and consistency fixes

- Project.to_dict() now includes user_id and auto_summary
- Status validation unified to (active/completed/archived) on both
  create and update project routes; update route previously had none
- Milestone routes: replace get_project (ownership-only) with
  get_project_for_user so shared viewers/editors can access milestones
- Add get_milestone_in_project() to milestones service for project-
  scoped lookup without user_id filter; all milestone routes use it
- Milestone PATCH now validates status as 'active'|'done'; fix tool
  enum which was wrongly ['active','completed','cancelled']
- Write mutation routes (POST/PATCH/DELETE milestones) now check
  can_write_project() and return 403 for read-only shared users
- update_project tool now exposes title and color fields so projects
  can be renamed or recolored via chat
- create_project tool now exposes color field
- GET /api/projects?include_summary=true embeds summaries in one
  backend pass; ProjectListView switches to this, eliminating N+1
  per-project fetches

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 13:14:42 -04:00
parent ed715dcc23
commit c5191837fb
7 changed files with 87 additions and 37 deletions
+9 -3
View File
@@ -392,6 +392,7 @@ _CORE_TOOLS = [
"title": {"type": "string", "description": "Project title"},
"description": {"type": "string", "description": "Brief description"},
"goal": {"type": "string", "description": "The goal or desired outcome"},
"color": {"type": "string", "description": "Optional hex color for the project (e.g. '#6366f1')"},
"confirmed": {"type": "boolean", "description": "Must be true — only set after the user has explicitly confirmed they want this project created."},
},
"required": ["title", "confirmed"],
@@ -433,14 +434,16 @@ _CORE_TOOLS = [
"type": "function",
"function": {
"name": "update_project",
"description": "Update a project's status, description, or goal.",
"description": "Update a project's title, status, description, goal, or color.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Project name to find"},
"title": {"type": "string", "description": "New project title"},
"status": {"type": "string", "enum": ["active", "completed", "archived"]},
"description": {"type": "string"},
"goal": {"type": "string"},
"color": {"type": "string", "description": "Hex color (e.g. '#6366f1'), or empty string to clear"},
},
"required": ["query"],
},
@@ -475,7 +478,7 @@ _CORE_TOOLS = [
"milestone": {"type": "string", "description": "Current milestone title to look up"},
"title": {"type": "string", "description": "New title (omit to keep current)"},
"description": {"type": "string", "description": "New description (omit to keep current)"},
"status": {"type": "string", "enum": ["active", "completed", "cancelled"], "description": "New status (omit to keep current)"},
"status": {"type": "string", "enum": ["active", "done"], "description": "New status: 'active' (in progress) or 'done' (completed)"},
},
"required": ["project", "milestone"],
},
@@ -1768,6 +1771,7 @@ async def execute_tool(
title=proj_title,
description=arguments.get("description", ""),
goal=arguments.get("goal", ""),
color=arguments.get("color") or None,
)
return {"success": True, "type": "project", "data": project.to_dict()}
@@ -1816,9 +1820,11 @@ async def execute_tool(
if project is None:
return {"success": False, "error": f"No project found matching '{query}'"}
fields = {}
for k in ("status", "description", "goal"):
for k in ("title", "status", "description", "goal"):
if k in arguments:
fields[k] = arguments[k]
if "color" in arguments:
fields["color"] = arguments["color"] or None # empty string → None (clear)
updated = await _up(user_id, project.id, **fields)
return {"success": True, "type": "project_updated", "data": updated.to_dict()}