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.
17and SSH (read + write) with a fast, themeable, htmx-driven web UI. Private by default, no sign-up.17and SSH (read + write) with a fast, themeable, htmx-driven web UI. Private by default, no sign-up.
18Build order is fixed — see **Implementation phases** below.18Build order is fixed — see **Implementation phases** below.
1919
20## What this project deliberately is NOT20## Scope: a complete forge, minus CI
2121
22Do not build: issues, pull/merge requests, merge machinery, GitHub-style orgs, web sign-up/self-22The project now aims to be a **complete forge minus CI** (feature-comparable to Forgejo/Gitea for
23registration, forks, stars, activity feeds, wikis, releases, git-LFS, webhooks, federation. When23the areas below). In scope: code browsing/diffs/signatures, nested groups, three-level visibility
24a feature is ambiguous, choose the simpler behaviour. CI is **deferred** — design the seams for it24(public/internal/private) + collaborators, **issues and pull requests** with GitHub-style **scoped
25(reserve the `runs` nav slot, `/-/runs` routes, `runs`/`jobs` tables, `post-receive` hook entry25labels** (`kind: value`), assignees, comments, and **server-side merge machinery** (merge/squash/
26point) but ship them inert behind `ui.show_runs = false`. Do not implement CI until phase 18 is done.26rebase) — issues and PRs are **toggleable per repository**; optional web **self-registration**
27(toggleable, optional hCaptcha/reCAPTCHA) alongside admin invites; and a full **account settings**
28area (username/email/password, SSH/GPG keys, API tokens, profile). See `spec.md` §§18–19.
29
30Still **out of scope** (choose the simpler behaviour when ambiguous): GitHub-style orgs, forks,
31stars, followers, notifications, wikis, releases-as-a-feature, git-LFS, webhooks, mirroring,
32federation. CI remains **deferred** — design the seams (reserve the `runs` nav slot, `/-/runs`
33routes, `runs`/`jobs` tables, `post-receive` hook entry point), ship them inert behind
34`ui.show_runs = false`, and build CI last.
2735
28## Commands36## Commands
2937
docs/decisions.md +23 −0
@@ -616,3 +616,26 @@ is what it always meant. The fixed `/repo-settings/{id}` prefix sidesteps the
616catch-all cleanly and keys on the id (rename-safe). Push-to-create matches the616catch-all cleanly and keys on the id (rename-safe). Push-to-create matches the
617muscle memory of every other forge and stays safe: only the authenticated owner617muscle memory of every other forge and stays safe: only the authenticated owner
618(or an admin) can trigger it, and only on receive-pack.618(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"
623posture. fabrica now targets a **complete forge minus CI**: issues and pull requests with
624GitHub-style **scoped labels** (`kind: value`, mutually exclusive within a scope), assignees,
625comments, 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`
627subprocess (never libgit2, per §3.1); issues and PRs **toggleable per repository**; optional
628web **self-registration** (`instance.allow_registration`, optional hCaptcha/reCAPTCHA)
629alongside admin invites; and expanded **account settings** (username/email/password, SSH/GPG
630keys, API tokens, profile). Still out of scope: orgs, forks, stars, wikis, releases, LFS,
631webhooks, 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
634across both; a PR is an `issues` row with `is_pull = 1` plus a `pull_requests` side table, so
635labels/comments/numbering are shared.
636
637**Rationale:** the maintainer wants the instance to be a full-featured forge for real use, not
638just a private browser. Sharing the issue/PR numbering and tables keeps labels and comments
639uniform and avoids a parallel implementation. Merge via subprocess preserves the one code path
640for write operations. spec.md §§1, 18–19 and CLAUDE.md are updated to match; this is a large,
641multi-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
15### Goals15### 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.
18- Browse code, history, diffs, branches, and tags in a UI that is fast on desktop and mobile.18- 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.
20- Show whether each commit is cryptographically signed, via SSH or GPG.20- Show whether each commit is cryptographically signed, via SSH or GPG.
21- Organize repositories under nested, user-owned groups.21- 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).
22- Re-theme and re-brand without recompiling.31- Re-theme and re-brand without recompiling.
23- One binary, one config file, one data directory.32- One binary, one config file, one data directory.
2433
34The aim is a **complete forge minus CI** — feature-comparable to Forgejo/Gitea for the areas
35above. When a feature is ambiguous, choose the simpler behaviour.
36
25### Non-goals (do not build these)37### Non-goals (do not build these)
2638
27- **Issues.** Not planned.
28- **Pull requests / merge requests.** Not planned. No merge machinery, no review flow.
29- **Organizations** in the GitHub sense. Repos belong to a user; groups provide the hierarchy.39- **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.40- Forks, stars, followers, activity feeds beyond the dashboard, notifications, wikis,
31- Forks, stars, followers, activity feeds, notifications, wikis, releases-as-a-feature,41 releases-as-a-feature, git-LFS, webhooks, mirroring, federation. (Not planned for now; may be
32 git-LFS, webhooks, mirroring, federation.42 revisited, but out of the current "complete minus CI" scope.)
33- Being Forgejo/Gitea/GitLab/GitHub. When a feature is ambiguous, choose the simpler behaviour.
3443
35### Deferred (design for, do not implement)44### Deferred (design for, do not implement)
3645
@@ -1164,3 +1173,76 @@ stages and jobs, log streaming into the reserved `runs` view.
1164- The UI is usable at 380px wide.1173- The UI is usable at 380px wide.
1165- Docker Compose brings up a working instance from the example config.1174- Docker Compose brings up a working instance from the example config.
1166- Every crate has tests; all four cargo checks pass; every commit is conventional.1175- Every crate has tests; all four cargo checks pass; every commit is conventional.
1176
1177---
1178
1179## 18. Collaboration: issues, pull requests, labels
1180
1181Issues 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
1183feature 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`
1185may manage labels and force-merge.
1186
1187### 18.1 Scoped labels
1188
1189Labels are per-repo, GitHub-style, and may be **scoped**: a label named `type: backend` has
1190scope `type` and value `backend`. Scoped labels are **mutually exclusive within their scope**
1191on 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
1193a colour (`#rrggbb`); the scope is parsed from the first `": "` in the name.
1194
1195Schema: `labels (id, repo_id, name, name_lower, color, description, created_at)` with a unique
1196index on `(repo_id, name_lower)`; join table `issue_labels (issue_id, label_id)`.
1197
1198### 18.2 Issues
1199
1200An issue belongs to a repo, has an author, a monotonically-per-repo **number** (`#N`), a
1201title, 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
1203transaction) shared with pull requests, so `#N` is unique across both.
1204
1205Schema: `issues (id, repo_id, number, author_id, title, body, state, is_pull, assignee_id,
1206created_at, updated_at, closed_at)`; `comments (id, issue_id, author_id, body, created_at,
1207updated_at)`. Pull requests are rows in `issues` with `is_pull = 1` plus a `pull_requests`
1208side table (see below), so numbering, labels, and comments are shared.
1209
1210Routes: `/-/issues`, `/-/issues/new`, `/-/issues/{n}`; POST endpoints for create, comment,
1211label toggle, assignee, and state change (all CSRF-guarded, gated on `Write`/author).
1212
1213### 18.3 Pull requests and merge machinery
1214
1215A 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
1217diff renderer), supports review comments, and can be **merged server-side** by a `Write`+
1218collaborator when mergeable, via one of three strategies: **merge commit**, **squash**, or
1219**rebase**. Merge is performed with the spawned `git` subprocess (`git merge`/`git
1220merge --squash`/`git rebase` into a work area, then update the base ref), never libgit2 —
1221consistent with §3.1. A PR is auto-closed when its head is merged; conflicts block the merge
1222with a clear message.
1223
1224Schema: `pull_requests (issue_id, base_ref, head_ref, merged_at, merged_by, merge_sha,
1225merge_strategy)`.
1226
1227Routes: `/-/pulls`, `/-/pulls/new?base=…&head=…`, `/-/pulls/{n}` (with `/commits`, `/files`
1228sub-views); POST for create, comment, review, and merge.
1229
1230## 19. Accounts, self-registration, and settings
1231
1232### 19.1 Self-registration
1233
1234Optional, off by default, controlled by `instance.allow_registration` (bool). When on, `/register`
1235offers 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
1238is set, the widget is rendered and the token is server-verified against the provider before the
1239account 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,
1244avatar — already built); **Account** (change username, email, password — password change
1245re-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
1247the existing `api_tokens` table; the JWT is shown once on creation). Every mutation is
1248CSRF-guarded and self-service (a user edits only their own account).