# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project status: MVP complete All 18 implementation phases are done and `nix flake check` is green. The workspace, flake, and every crate exist and are tested; the binary serves the web UI + HTTPS/SSH git transport + JSON API. `spec.md` remains the source of truth for behaviour — read it before changing anything. Where it says **MUST**, treat it as a hard requirement; **SHOULD** deviations are logged in `docs/decisions.md` (read it — it records every deviation, e.g. rpgp-not-sequoia, native-tls-not-rustls, russh's aws-lc-rs licenses, and the `tokio` opt-level=0 workaround for a nightly ICE). Known gaps are noted there too; the deferred **CI** (HCL + Docker via bollard) has not been started. `fabrica` is a self-hosted git server: a single Rust binary that serves repos over HTTPS (read) and SSH (read + write) with a fast, themeable, htmx-driven web UI. Private by default, no sign-up. Build order is fixed — see **Implementation phases** below. ## Scope: a complete forge, minus CI The project now aims to be a **complete forge minus CI** (feature-comparable to Forgejo/Gitea for the areas below). In scope: code browsing/diffs/signatures, nested groups, three-level visibility (public/internal/private) + collaborators, **issues and pull requests** with GitHub-style **scoped labels** (`kind: value`), assignees, comments, and **server-side merge machinery** (merge/squash/ rebase) — issues and PRs are **toggleable per repository**; optional web **self-registration** (toggleable, optional hCaptcha/reCAPTCHA) alongside admin invites; and a full **account settings** area (username/email/password, SSH/GPG keys, API tokens, profile). See `spec.md` §§18–19. Still **out of scope** (choose the simpler behaviour when ambiguous): GitHub-style orgs, forks, stars, followers, notifications, wikis, releases-as-a-feature, git-LFS, webhooks, mirroring, federation. CI remains **deferred** — design the seams (reserve the `runs` nav slot, `/-/runs` routes, `runs`/`jobs` tables, `post-receive` hook entry point), ship them inert behind `ui.show_runs = false`, and build CI last. ## Commands None run today (no code). Once the workspace exists, the **quality gate** below must pass after every unit of work — no exceptions, no deferrals: ``` cargo fmt --all -- --check cargo check --workspace --all-targets cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace ``` A `justfile` must expose `just check` running all four; use it. `nix flake check` is the superset (`checks.{clippy,fmt,test,deny,doc}`) and must be green before any phase is called done. Tests use `cargo-nextest` in the dev shell; keep the full suite under two minutes. `cargo test` alone must suffice with no external services (Postgres tests are feature-gated behind `postgres-tests` / `FABRICA_TEST_POSTGRES_URL`; integration tests gate on `git`/`ssh` being present and skip cleanly). Toolchain: Rust **nightly** via fenix (pinned in `flake.lock`), edition 2024, resolver 3. Nightly is a currency preference only — **no `#![feature(...)]` gates**; code must compile on stable. Workspace lints forbid `unsafe_code`; deny `clippy::all`, `unwrap_used`, `panic`, `todo`. `unwrap_used` may be locally allowed inside `#[cfg(test)]`. ## Architecture Single root package `fabrica` (`src/main.rs` = arg-parse → dispatch into `cli`, nothing else). All library code lives in `crates/` with **unprefixed** names. Dependency direction is strictly one-way and enforced: ``` model ← store / git / auth ← web / api / ssh / cli ``` `model` depends on nothing in-tree; **no crate may depend on `web`**. Never name a crate `core`, `std`, `alloc`, `test`, or `proc_macro` (sysroot collisions). Crates: `config` (figment TOML+env), `store` (sqlx, SQLite+Postgres), `model` (domain types, no I/O), `git` (libgit2 reads + subprocess pack transport), `highlight` (inkjet/tree-sitter), `auth` (argon2id, sessions, JWT), `mail` (lettre), `ssh` (russh), `web` (axum + maud + htmx), `api` (`/api/v1` JSON), `cli` (clap). ### The central architectural fact: git plumbing is split `libgit2` (via `git2`) is **client-only for transport** — it has no server-side `upload-pack`/ `receive-pack`. So: - **In-process `git2`**: browsing, log, diff, refs/trees/blobs, signature extraction, `init_bare`. - **Spawned `git` subprocess**: clone/fetch (`git upload-pack --stateless-rpc`), push (`git receive-pack`), maintenance (`git gc`/`git repack`). One code path: `git::pack::spawn(kind, repo_path, env, stdio)`. Therefore the `git` binary **MUST** be on `PATH` at runtime — the Nix derivation wraps the binary with git on PATH, the Dockerfile verifies `git --version` at build time. Resolve it once at startup (config `git.binary`, require ≥ 2.41), surface in `/healthz` and `fabrica doctor`. Pack transport rules that are easy to get wrong: **authorize before spawning** (the subprocess never makes an access decision); **stream both directions, never buffer a pack in memory**; HTTP push (`git-receive-pack`) returns a deliberate `403` pointing at the SSH URL; dumb HTTP is disabled. A reverse proxy in front **must not buffer** `/git-upload-pack` bodies or clones stall. ### Repos are stored by id, not name On disk: `{storage.repo_dir}/{id[0..2]}/{id}.git` where `id` is a lowercase ULID. The **database is the only name→path authority**, so renames and group moves are metadata-only. The tree is not human-browsable by design — `fabrica repo path ` resolves it for operators. ### Groups make repo paths variable-length → the `/-/` separator Group nesting means repo paths have unknown depth. Routes disambiguate with the GitLab `/-/` convention: everything before `/-/` is the repo path, everything after is the view (`/{owner}/{path..}/-/blob/{ref}/{p..}`). Path resolution: look up `repos` by `(owner_id, path)`; on miss look up `groups` and render a group listing; on miss `404`. ### Store portability (SQLite + Postgres, one schema) Do **not** use sqlx compile-time-checked macros against two dialects. Define a `Store` trait with `Sqlite`/`Postgres` impls (or runtime queries over `AnyPool`); every statement stays in the portable SQL subset. Timestamps are `BIGINT` unix **milliseconds** UTC (no `DATETIME`/`TIMESTAMPTZ`). Ids are `TEXT` ULIDs (26 chars, lexicographically sortable — use for ordering). No `AUTOINCREMENT`/ `SERIAL`. Case-insensitive uniqueness via a normalized `*_lower` column + unique index, not collation. Migrations: `migrations/{sqlite,postgres}/NNNN_name.sql`, selected at runtime via `sqlx::migrate!`. ### Permission model: one function, exhaustively tested ```rust pub enum Access { None, Read, Write, Admin } pub fn access(viewer: Option<&Viewer>, repo: &Repo) -> Access ``` Order: admin → owner → explicit collaborator row → public+anonymous-allowed = Read → None. `None` on a private repo **MUST render 404, not 403** — never leak existence (this holds for the web UI and the API alike). The `access()` test is the security-critical one: enumerate the full matrix (viewer kind × visibility × collaborator permission), don't sample it. ### Highlighting: line-addressable, per-side in diffs `inkjet`/tree-sitter. Two hard requirements drive the design: **every emitted line must be independently valid balanced HTML** (spans crossing a newline are split and the capture stack re-opened on the next line — see `fix(highlight): reopen capture stack across line breaks`), and diffs highlight the **pre- and post-image of each file as whole documents**, never per-hunk, then zip lines with hunks. Stable theme-driven class names (`hl-keyword`, `hl-string`, …) mapped from tree-sitter captures in one table. Highlighting **MUST NOT** be able to fail a page render — skip oversized/binary/minified files and fall back to escaped plain text on any grammar panic. Language resolution has a fixed precedence (§8.1): `.gitattributes fabrica-language` > `linguist- language` > shebang > modeline > exact filename > extension (longest match) > plain text. ### Web UI: server-rendered, htmx-enhanced, no build step `axum` + `maud` (compile-time-checked templates) + **htmx 2 vendored into `assets/`** (no CDN, no npm, no WASM, no SPA). Hand-written vanilla JS is capped at < 200 lines total in `assets/fabrica.js` (mobile drawer, theme picker, clipboard, keyboard shortcuts). **Every interactive element must work without JavaScript** — htmx enhances links/forms, it is not the substrate. Handlers detect the `HX-Request` header and return a partial, else the full page; write this as one `respond(page, partial)` helper so no route drifts. Theming re-brands with no rebuild: `assets/base.css` is structure-only with **no literal colours**, written entirely against custom properties. Themes are single CSS files defining `--fb-*` tokens; drop one in `ui.themes_dir` and `SIGHUP` to load it. A theme that sets only the documented `--fb-*` tokens MUST yield a complete UI — that is the themeability acceptance test. No web fonts, no network requests from any page. ## CLI operates directly on the store — no IPC with the server Every `fabrica` subcommand touches the store/filesystem directly (SQLite WAL and Postgres both tolerate concurrent writers); there is no IPC with a running `serve`. Mutations a live server caches (repo rename/delete, user disable) bump a `cache_epoch` row that `serve` polls. `serve` runs in the **foreground** by default (correct for systemd/Docker); there is no `stop` subcommand — signal the pid. Destructive commands prompt on a TTY and require `--yes` otherwise. ## Working conventions - **Commits**: Conventional Commits, one logical change each, committed **as work happens** (not batched). Scope is the crate/area: `feat(git):`, `fix(web):`, `build(nix):`. Imperative, lowercase, ≤ 72 chars, no trailing period. **Never commit with failing checks** — a needed `#[allow]` is justified in the commit body. - **Errors**: `thiserror` (one enum per crate) in libraries; `anyhow` only at the binary boundary (`main.rs`, CLI). `tracing` with structured fields — request-id span per HTTP request, session span per SSH connection. User-facing error pages are templated/themed, never expose internals (detail → log with the request id, page shows the id). - **Docs**: `docs/decisions.md` is a running log (date, decision, alternatives, rationale). Seed it with the decisions the spec already makes (subprocess pack transport, id-based repo dirs, `/-/` separator, epoch-ms timestamps, `serve` with no `stop`, linear search). Update on any deviation. ## Implementation phases (build in this order) Each phase ends with the full gate green and its own commits; do not start a phase before the previous is clean: 1. Scaffold (flake + fenix + crane, empty crates, lints, justfile, MPL-2.0) 2. config 3. store 4. auth 5. git reads 6. highlight 7. git signatures 8. cli core 9. mail 10. web shell 11. web repo views 12. web commit view 13. pack transport 14. ssh 15. search 16. api 17. deployment 18. polish. Then, and only then, CI (HCL config via `hcl-rs`, Docker execution via `bollard`).