3.7 KiB
3.7 KiB
06 — Projects, Members, Labels, Dependencies
The project is the permission boundary and the container for everything else. Fields and enums in 02-data-model.md; routes in 04-api.md.
Project lifecycle
- Create: requires title; creator becomes
creator_id,owner_id, and the soleowner-role member; seed the 6 default labels (enhancement, bug, chore, task, feature, documentation) with palette colors - Status field:
backlog → planned → in_progress → completed | canceled— transitions are not enforced as a state machine (any status settable), butcompleted_atautomation is:- status →
completedsetscompleted_at = now() - leaving
completedclears it
- status →
- Priority: optional,
low…frantic; UI color mapping in 14-design-system.md git_repository_url: normalized on save (trim, strip trailing.gitand/); used to route incoming webhooks (11-integrations.md)- Update/delete emit
project.updated/project.status_changed/project.deleted; delete is soft and owner-only - Project description is markdown with mention/#ref support (08-collaboration.md)
Ownership & membership
- Exactly one owner per project — invariant maintained in the service (
projects.owner_id↔ the singlerole='owner'membership row) - Transfer:
PUT /projects/{id}/owner(owner only, target must be an existing member); atomically swap roles; emitproject.ownership_transferred - Add member: owner picks an existing instance user (
GET /users) + role; emitsproject.member_added→ triggersadded_to_projectnotification; no email-invitation flow in v1 (small instance, users already exist) - Remove member: owner only; removing a member clears their assignee slots in the project? No — assignments persist (history matters); UI shows them as non-members. Their watches are deleted.
- Owner cannot leave/be removed without transferring first (409)
- Role change: owner only; only
memberrole assignable via PATCH (ownership moves only through transfer)
Favorites
PUT/DELETE /projects/{id}/favoritetoggles aproject_favoritesrowGET /projectssorts favorites first (by favorited_at desc), then the rest by latest activity- Favorites are personal — no events, no audit entries
Labels
- Project-scoped; name unique per project (case-sensitive match on citext-free
text, uniqueness enforced by partial unique index) - CRUD is member-accessible except delete (owner only); color must be a valid
#rrggbb - Soft delete; a deleted label disappears from issues immediately (join filters
deleted_at IS NULL) but rows persist until cull - Emit
label.created/updated/deleted
Project dependencies
- Edge
project_iddepends ondepends_on_project_id(i.e. blocked by it) - Both endpoints must be projects the caller is a member of
- Cycle detection before insert: DFS from
depends_on_project_idfollowing existingdepends_onedges; reachingproject_id→ 409cycle_detected - Project detail shows both directions: "depends on" and "blocks" (reverse lookup)
- Emit
project.dependency_added/removed
Activity feed
GET /projects/{id}/activity:audit_log WHERE project_id = $1 ORDER BY created_at DESCpaged; render display text app-side fromaction_type+previous_values/new_values+ actorGET /dashboard/activity: same overproject_id IN (my projects), capped at 50 per page- Feed entries link to their resource (
#Nissues resolve viaproject_issue_idkept in event payloads) - Hidden from feeds: pure rank moves (
issue.movedwith only rank changes) — noise; still audited