§22 S5: in-app create-project + global-directory empty states (@S5 C3.1–C3.2)

Adds the global-Owner create-project action and the role-aware deployment
directory, completing slice S5 of the three-tier refactor (release v0.44.0,
minor/non-breaking — no migration). Per
docs/design/2026-06-05-three-tier-projects-collections.md Part E (S5),
Part C.3 (C3.1–C3.2).

Backend:
- auth.can_create_project — global-Owner gate (deployment owner/admin or an
  explicit scope_type='global' Owner grant).
- bot.create_project — provision the Gitea content repo (seed README so main
  exists), read+append+commit projects.yaml in the registry repo, audit-log
  (create_project). The bot stays the only git writer (§1).
- POST /api/projects (api_deployment) — global-Owner gated; validates the id
  (slug, not 'default'), name, type, visibility, content_repo; commits via the
  bot, then re-mirrors the registry so projects + default collection rows flow
  from git (§22.2). GET /api/deployment gains viewer.can_create_project and
  default_project_readable. make_router now takes gitea + bot.

Frontend:
- api.createProject; DeploymentProvider surfaces viewer + defaultProjectReadable
  + refresh; DeploymentLanding redirects into the default only when readable
  (gated/absent default falls through to the directory, no 404 bounce).
- Directory.jsx role-aware empty states (C3.1 "Create your first project" CTA;
  C3.2 "Nothing has been shared with you yet") + Owner-only "New project"
  control + CreateProjectModal.

Tests: backend test_create_project_vertical.py (vertical + gates + the
deployment empty-state signals); frontend Directory.test.jsx empty-state cases.

Also: ignore the session-local .superpowers/ tooling dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-06 01:02:16 -07:00
parent 2696e64ff5
commit 33212c71e4
16 changed files with 779 additions and 56 deletions
+19
View File
@@ -682,6 +682,25 @@ def can_create_collection(user: SessionUser | None, project_id: str) -> bool:
return row is not None
def can_create_project(user: SessionUser | None) -> bool:
"""§22 S5 (§A.2 / §B.1): may the user create a new project? "+ New project"
is a **global-Owner** action — a deployment owner/admin (a global Owner per
§B.1) or a holder of an explicit `scope_type='global'` Owner grant. Creating
a project is deployment-level, so it is not reachable by a project- or
collection-scope grant nor by a global RFC Contributor (that role creates
collections, not projects). Subject to the §6 admission floor."""
if user is None or user.permission_state != "granted":
return False
if user.role in _DEPLOYMENT_SUPERUSER_ROLES:
return True
row = db.conn().execute(
"SELECT 1 FROM memberships "
"WHERE user_id = ? AND scope_type = 'global' AND role = 'owner' LIMIT 1",
(user.user_id,),
).fetchone()
return row is not None
def can_invite_at_project(user: SessionUser | None, project_id: str) -> bool:
"""§22 S4 (C.2): may the user grant scope roles at this project (or at any
collection within it)? Managing membership is an *Owner* capability whose