Files
solopm-server/plans/06-projects.md
T

52 lines
3.7 KiB
Markdown

# 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