fabrica

hanna/fabrica

docs: expand scope to a complete forge (issues, PRs, sign-up, labels)

c91497d · hanna committed on 2026-07-25

Rewrite the spec and CLAUDE.md non-goals: issues and pull requests with
scoped labels, assignees, comments, and server-side merge machinery
(per-repo toggle); optional self-registration with captcha; and expanded
account settings. Add spec.md §§18-19 and a decisions entry. Orgs, forks,
stars, wikis, LFS, webhooks, and federation stay out; CI stays deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
3 files changed · +128 −15UnifiedSplit
CLAUDE.md +15 −7
@@ -17,13 +17,21 @@ the deferred **CI** (HCL + Docker via bollard) has not been started.
1717 and SSH (read + write) with a fast, themeable, htmx-driven web UI. Private by default, no sign-up.
1818 Build order is fixed — see **Implementation phases** below.
1919
20-## What this project deliberately is NOT
21-
22-Do not build: issues, pull/merge requests, merge machinery, GitHub-style orgs, web sign-up/self-
23-registration, forks, stars, activity feeds, wikis, releases, git-LFS, webhooks, federation. When
24-a feature is ambiguous, choose the simpler behaviour. CI is **deferred** — design the seams for it
25-(reserve the `runs` nav slot, `/-/runs` routes, `runs`/`jobs` tables, `post-receive` hook entry
26-point) but ship them inert behind `ui.show_runs = false`. Do not implement CI until phase 18 is done.
20+## Scope: a complete forge, minus CI
21+
22+The project now aims to be a **complete forge minus CI** (feature-comparable to Forgejo/Gitea for
23+the areas below). In scope: code browsing/diffs/signatures, nested groups, three-level visibility
24+(public/internal/private) + collaborators, **issues and pull requests** with GitHub-style **scoped
25+labels** (`kind: value`), assignees, comments, and **server-side merge machinery** (merge/squash/
26+rebase) — issues and PRs are **toggleable per repository**; optional web **self-registration**
27+(toggleable, optional hCaptcha/reCAPTCHA) alongside admin invites; and a full **account settings**
28+area (username/email/password, SSH/GPG keys, API tokens, profile). See `spec.md` §§18–19.
29+
30+Still **out of scope** (choose the simpler behaviour when ambiguous): GitHub-style orgs, forks,
31+stars, followers, notifications, wikis, releases-as-a-feature, git-LFS, webhooks, mirroring,
32+federation. CI remains **deferred** — design the seams (reserve the `runs` nav slot, `/-/runs`
33+routes, `runs`/`jobs` tables, `post-receive` hook entry point), ship them inert behind
34+`ui.show_runs = false`, and build CI last.
2735
2836 ## Commands
2937
docs/decisions.md +23 −0
@@ -616,3 +616,26 @@ is what it always meant. The fixed `/repo-settings/{id}` prefix sidesteps the
616616 catch-all cleanly and keys on the id (rename-safe). Push-to-create matches the
617617 muscle memory of every other forge and stays safe: only the authenticated owner
618618 (or an admin) can trigger it, and only on receive-pack.
619+
620+## 2026-07-25 — Scope expansion: a complete forge minus CI
621+
622+**Decision:** Reverse the original "issues / pull requests / self-registration are non-goals"
623+posture. fabrica now targets a **complete forge minus CI**: issues and pull requests with
624+GitHub-style **scoped labels** (`kind: value`, mutually exclusive within a scope), assignees,
625+comments, and open/closed state; PRs with branch compare, diff, review, and server-side
626+**merge machinery** (merge commit / squash / rebase) performed via the spawned `git`
627+subprocess (never libgit2, per §3.1); issues and PRs **toggleable per repository**; optional
628+web **self-registration** (`instance.allow_registration`, optional hCaptcha/reCAPTCHA)
629+alongside admin invites; and expanded **account settings** (username/email/password, SSH/GPG
630+keys, API tokens, profile). Still out of scope: orgs, forks, stars, wikis, releases, LFS,
631+webhooks, federation. CI stays deferred and is built last.
632+
633+**Numbering:** issues and PRs share a per-repo counter (`repos.next_iid`) so `#N` is unique
634+across both; a PR is an `issues` row with `is_pull = 1` plus a `pull_requests` side table, so
635+labels/comments/numbering are shared.
636+
637+**Rationale:** the maintainer wants the instance to be a full-featured forge for real use, not
638+just a private browser. Sharing the issue/PR numbering and tables keeps labels and comments
639+uniform and avoids a parallel implementation. Merge via subprocess preserves the one code path
640+for write operations. spec.md §§1, 18–19 and CLAUDE.md are updated to match; this is a large,
641+multi-phase build delivered incrementally with the gate green at each step.
spec.md +90 −8
@@ -14,23 +14,32 @@ This document is the implementation contract. Follow it section by section. Wher
1414
1515 ### Goals
1616
17-- Host git repositories for a small, fixed set of users (personal / small-team private instance).
17+- Host git repositories for users on a self-hosted instance, personal or small-team.
1818 - Browse code, history, diffs, branches, and tags in a UI that is fast on desktop and mobile.
19-- Syntax highlighting everywhere code appears, including inside diffs.
19+- Syntax highlighting everywhere code appears, including inside diffs and rendered markdown.
2020 - Show whether each commit is cryptographically signed, via SSH or GPG.
2121 - Organize repositories under nested, user-owned groups.
22+- **Collaboration.** Issues and pull requests with GitHub-style **scoped labels**
23+ (`kind: value`, e.g. `type: backend`), assignees, comments, and open/closed state; PRs
24+ include branch comparison, diff, review, and server-side **merge machinery** (merge commit,
25+ squash, rebase). Issues and PRs are **toggleable per repository**.
26+- **Three-level visibility** (public / internal / private), GitLab-style, with a per-instance
27+ default; explicit collaborators with read/write/admin.
28+- **Accounts.** Optional web **self-registration** (toggleable, with optional hCaptcha /
29+ reCAPTCHA) alongside admin invites; a full account settings area (change username, email,
30+ password; manage SSH/GPG keys, API tokens, and profile).
2231 - Re-theme and re-brand without recompiling.
2332 - One binary, one config file, one data directory.
2433
34+The aim is a **complete forge minus CI** — feature-comparable to Forgejo/Gitea for the areas
35+above. When a feature is ambiguous, choose the simpler behaviour.
36+
2537 ### Non-goals (do not build these)
2638
27-- **Issues.** Not planned.
28-- **Pull requests / merge requests.** Not planned. No merge machinery, no review flow.
2939 - **Organizations** in the GitHub sense. Repos belong to a user; groups provide the hierarchy.
30-- Web sign-up, public registration, or self-service account creation of any kind.
31-- Forks, stars, followers, activity feeds, notifications, wikis, releases-as-a-feature,
32- git-LFS, webhooks, mirroring, federation.
33-- Being Forgejo/Gitea/GitLab/GitHub. When a feature is ambiguous, choose the simpler behaviour.
40+- Forks, stars, followers, activity feeds beyond the dashboard, notifications, wikis,
41+ releases-as-a-feature, git-LFS, webhooks, mirroring, federation. (Not planned for now; may be
42+ revisited, but out of the current "complete minus CI" scope.)
3443
3544 ### Deferred (design for, do not implement)
3645
@@ -1164,3 +1173,76 @@ stages and jobs, log streaming into the reserved `runs` view.
11641173 - The UI is usable at 380px wide.
11651174 - Docker Compose brings up a working instance from the example config.
11661175 - Every crate has tests; all four cargo checks pass; every commit is conventional.
1176+
1177+---
1178+
1179+## 18. Collaboration: issues, pull requests, labels
1180+
1181+Issues and pull requests are enabled **per repository** (`repos.issues_enabled`,
1182+`repos.pulls_enabled`, both default true). When disabled, the nav tab and routes for that
1183+feature 404 for the repo. Access follows the existing `auth::access` verdict: any viewer with
1184+`Read` may open issues and PRs and comment; `Write` (or the author) may edit/close; `Admin`
1185+may manage labels and force-merge.
1186+
1187+### 18.1 Scoped labels
1188+
1189+Labels are per-repo, GitHub-style, and may be **scoped**: a label named `type: backend` has
1190+scope `type` and value `backend`. Scoped labels are **mutually exclusive within their scope**
1191+on a single issue/PR (assigning `type: frontend` removes `type: backend`). Unscoped labels
1192+(`bug`, `good first issue`) stack freely. A label carries a name, an optional description, and
1193+a colour (`#rrggbb`); the scope is parsed from the first `": "` in the name.
1194+
1195+Schema: `labels (id, repo_id, name, name_lower, color, description, created_at)` with a unique
1196+index on `(repo_id, name_lower)`; join table `issue_labels (issue_id, label_id)`.
1197+
1198+### 18.2 Issues
1199+
1200+An issue belongs to a repo, has an author, a monotonically-per-repo **number** (`#N`), a
1201+title, a markdown body, an open/closed state, optional assignee, labels, and threaded comments
1202+(markdown). Numbers are allocated from a per-repo counter (`repos.next_iid` bumped in the same
1203+transaction) shared with pull requests, so `#N` is unique across both.
1204+
1205+Schema: `issues (id, repo_id, number, author_id, title, body, state, is_pull, assignee_id,
1206+created_at, updated_at, closed_at)`; `comments (id, issue_id, author_id, body, created_at,
1207+updated_at)`. Pull requests are rows in `issues` with `is_pull = 1` plus a `pull_requests`
1208+side table (see below), so numbering, labels, and comments are shared.
1209+
1210+Routes: `/-/issues`, `/-/issues/new`, `/-/issues/{n}`; POST endpoints for create, comment,
1211+label toggle, assignee, and state change (all CSRF-guarded, gated on `Write`/author).
1212+
1213+### 18.3 Pull requests and merge machinery
1214+
1215+A pull request compares a **head** ref (branch) against a **base** ref in the same repo
1216+(cross-repo forks are a non-goal). It shows the commit list and the combined diff (reusing the
1217+diff renderer), supports review comments, and can be **merged server-side** by a `Write`+
1218+collaborator when mergeable, via one of three strategies: **merge commit**, **squash**, or
1219+**rebase**. Merge is performed with the spawned `git` subprocess (`git merge`/`git
1220+merge --squash`/`git rebase` into a work area, then update the base ref), never libgit2 —
1221+consistent with §3.1. A PR is auto-closed when its head is merged; conflicts block the merge
1222+with a clear message.
1223+
1224+Schema: `pull_requests (issue_id, base_ref, head_ref, merged_at, merged_by, merge_sha,
1225+merge_strategy)`.
1226+
1227+Routes: `/-/pulls`, `/-/pulls/new?base=…&head=…`, `/-/pulls/{n}` (with `/commits`, `/files`
1228+sub-views); POST for create, comment, review, and merge.
1229+
1230+## 19. Accounts, self-registration, and settings
1231+
1232+### 19.1 Self-registration
1233+
1234+Optional, off by default, controlled by `instance.allow_registration` (bool). When on, `/register`
1235+offers a sign-up form (username, email, password); on success the account is created directly
1236+(no invite) and signed in. An optional **captcha** guards the form: `captcha.provider`
1237+(`none` | `hcaptcha` | `recaptcha`), `captcha.site_key`, `captcha.secret_key`; when a provider
1238+is set, the widget is rendered and the token is server-verified against the provider before the
1239+account is created. Admin invites remain available regardless.
1240+
1241+### 19.2 Account settings
1242+
1243+`/settings` expands into sections: **Profile** (display name, pronouns, bio, location, links,
1244+avatar — already built); **Account** (change username, email, password — password change
1245+re-verifies the current password and invalidates other sessions); **SSH & GPG keys**
1246+(list/add/remove, mirroring the CLI `key` commands); **API tokens** (list/create/revoke, using
1247+the existing `api_tokens` table; the JWT is shown once on creation). Every mutation is
1248+CSRF-guarded and self-service (a user edits only their own account).