# 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](02-data-model.md); routes in [04-api.md](04-api.md). ## Project lifecycle - [ ] Create: requires title; creator becomes `creator_id`, `owner_id`, and the sole `owner`-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), but `completed_at` automation is: - [ ] status → `completed` sets `completed_at = now()` - [ ] leaving `completed` clears it - [ ] Priority: optional, `low…frantic`; UI color mapping in [14-design-system.md](14-design-system.md) - [ ] `git_repository_url`: normalized on save (trim, strip trailing `.git` and `/`); used to route incoming webhooks ([11-integrations.md](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](08-collaboration.md)) ## Ownership & membership - [ ] Exactly one owner per project — invariant maintained in the service (`projects.owner_id` ↔ the single `role='owner'` membership row) - [ ] Transfer: `PUT /projects/{id}/owner` (owner only, target must be an existing member); atomically swap roles; emit `project.ownership_transferred` - [ ] Add member: owner picks an existing instance user (`GET /users`) + role; emits `project.member_added` → triggers `added_to_project` notification; 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 `member` role assignable via PATCH (ownership moves only through transfer) ## Favorites - [ ] `PUT/DELETE /projects/{id}/favorite` toggles a `project_favorites` row - [ ] `GET /projects` sorts 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_id` **depends on** `depends_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_id` following existing `depends_on` edges; reaching `project_id` → 409 `cycle_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 DESC` paged; render display text app-side from `action_type` + `previous_values`/`new_values` + actor - [ ] `GET /dashboard/activity`: same over `project_id IN (my projects)`, capped at 50 per page - [ ] Feed entries link to their resource (`#N` issues resolve via `project_issue_id` kept in event payloads) - [ ] Hidden from feeds: pure rank moves (`issue.moved` with only rank changes) — noise; still audited